@@ -509,13 +509,17 @@ async fn stats_endpoint(State(state): State<AppState>) -> Response {
509509fn rewrite_request_body ( body : & [ u8 ] , state : & AppState ) -> Result < ( Bytes , RewriteOutcome ) > {
510510 let mut v: Value = serde_json:: from_slice ( body) ?;
511511 let mut out = RewriteOutcome :: default ( ) ;
512+ // v0.14.7-L: track hashes already seen this request so duplicates can
513+ // emit the bare-minimum `<omc:ref h="..."/>` form.
514+ let mut seen: std:: collections:: HashSet < i64 > = std:: collections:: HashSet :: new ( ) ;
512515
513516 // ---- system prompt (top-level field) ----
514517 if let Some ( system) = v. get_mut ( "system" ) {
515518 match system {
516519 Value :: String ( s) => {
517520 if s. len ( ) >= state. rewrite_threshold {
518- if let Ok ( marker) = make_marker ( s, state) {
521+ if let Ok ( marker) = make_marker_with_dedup (
522+ s, state, MarkerKind :: HistoricalText , Some ( & mut seen) ) {
519523 out. bytes_system += s. len ( ) ;
520524 out. rewritten_count += 1 ;
521525 * system = Value :: String ( marker) ;
@@ -527,7 +531,9 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
527531 if block. get ( "type" ) . and_then ( Value :: as_str) == Some ( "text" ) {
528532 let Some ( text) = block. get ( "text" ) . and_then ( Value :: as_str) else { continue } ;
529533 if text. len ( ) < state. rewrite_threshold { continue ; }
530- let Ok ( marker) = make_marker ( text, state) else { continue } ;
534+ let Ok ( marker) = make_marker_with_dedup (
535+ text, state, MarkerKind :: HistoricalText , Some ( & mut seen) )
536+ else { continue } ;
531537 out. bytes_system += text. len ( ) ;
532538 out. rewritten_count += 1 ;
533539 // Mutate ONLY the `text` field; preserve cache_control + everything else
@@ -555,7 +561,8 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
555561 match content {
556562 Value :: String ( s) => {
557563 if s. len ( ) >= state. rewrite_threshold {
558- if let Ok ( marker) = make_marker ( s, state) {
564+ if let Ok ( marker) = make_marker_with_dedup (
565+ s, state, MarkerKind :: HistoricalText , Some ( & mut seen) ) {
559566 out. bytes_messages_text += s. len ( ) ;
560567 out. rewritten_count += 1 ;
561568 * content = Value :: String ( marker) ;
@@ -569,14 +576,16 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
569576 "text" => {
570577 let Some ( text) = block. get ( "text" ) . and_then ( Value :: as_str) else { continue } ;
571578 if text. len ( ) < state. rewrite_threshold { continue ; }
572- let Ok ( marker) = make_marker ( text, state) else { continue } ;
579+ let Ok ( marker) = make_marker_with_dedup (
580+ text, state, MarkerKind :: HistoricalText , Some ( & mut seen) )
581+ else { continue } ;
573582 out. bytes_messages_text += text. len ( ) ;
574583 out. rewritten_count += 1 ;
575584 block[ "text" ] = Value :: String ( marker) ;
576585 }
577586 "tool_result" => {
578587 if let Some ( inner) = block. get_mut ( "content" ) {
579- rewrite_tool_result_content ( inner, state, & mut out) ;
588+ rewrite_tool_result_content ( inner, state, & mut out, & mut seen ) ;
580589 }
581590 }
582591 "tool_use" => {
@@ -585,7 +594,7 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
585594 // LLM doesn't see (and thus copy) a fake field name
586595 // when generating fresh tool calls in later turns.
587596 if let Some ( input) = block. get_mut ( "input" ) {
588- rewrite_strings_recursive ( input, state, & mut out) ;
597+ rewrite_strings_recursive ( input, state, & mut out, & mut seen ) ;
589598 }
590599 }
591600 _ => { }
@@ -603,7 +612,8 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
603612 if let Some ( desc) = tool. get_mut ( "description" ) {
604613 if let Value :: String ( s) = desc {
605614 if s. len ( ) >= state. rewrite_threshold {
606- if let Ok ( marker) = make_marker ( s, state) {
615+ if let Ok ( marker) = make_marker_with_dedup (
616+ s, state, MarkerKind :: HistoricalText , Some ( & mut seen) ) {
607617 out. bytes_tool_definitions += s. len ( ) ;
608618 out. rewritten_count += 1 ;
609619 * desc = Value :: String ( marker) ;
@@ -615,7 +625,7 @@ fn rewrite_request_body(body: &[u8], state: &AppState) -> Result<(Bytes, Rewrite
615625 // property descriptions, enums, etc. — preserves schema structure.
616626 if let Some ( schema) = tool. get_mut ( "input_schema" ) {
617627 let before_count = out. rewritten_count ;
618- rewrite_schema_strings ( schema, state, & mut out) ;
628+ rewrite_schema_strings ( schema, state, & mut out, & mut seen ) ;
619629 if out. rewritten_count > before_count {
620630 // already counted via rewrite_schema_strings into the
621631 // bytes_tool_definitions field
@@ -760,22 +770,24 @@ fn insert_cache_control_on_last_block(msg: &mut Value) -> bool {
760770/// fields, `enum` arrays of strings, and nested `properties` — all candidates.
761771fn rewrite_schema_strings (
762772 val : & mut Value , state : & AppState , out : & mut RewriteOutcome ,
773+ seen : & mut std:: collections:: HashSet < i64 > ,
763774) {
764775 match val {
765776 Value :: String ( s) => {
766777 if s. len ( ) >= state. rewrite_threshold {
767- if let Ok ( marker) = make_marker ( s, state) {
778+ if let Ok ( marker) = make_marker_with_dedup (
779+ s, state, MarkerKind :: HistoricalText , Some ( seen) ) {
768780 out. bytes_tool_definitions += s. len ( ) ;
769781 out. rewritten_count += 1 ;
770782 * val = Value :: String ( marker) ;
771783 }
772784 }
773785 }
774786 Value :: Object ( map) => {
775- for ( _k, v) in map. iter_mut ( ) { rewrite_schema_strings ( v, state, out) ; }
787+ for ( _k, v) in map. iter_mut ( ) { rewrite_schema_strings ( v, state, out, seen ) ; }
776788 }
777789 Value :: Array ( arr) => {
778- for v in arr. iter_mut ( ) { rewrite_schema_strings ( v, state, out) ; }
790+ for v in arr. iter_mut ( ) { rewrite_schema_strings ( v, state, out, seen ) ; }
779791 }
780792 _ => { }
781793 }
@@ -792,11 +804,13 @@ fn rewrite_schema_strings(
792804/// 3. Array elements that are strings → same rule, in place.
793805fn rewrite_strings_recursive (
794806 val : & mut Value , state : & AppState , out : & mut RewriteOutcome ,
807+ seen : & mut std:: collections:: HashSet < i64 > ,
795808) {
796809 match val {
797810 Value :: String ( s) => {
798811 if s. len ( ) >= state. rewrite_threshold {
799- if let Ok ( marker) = make_marker ( s, state) {
812+ if let Ok ( marker) = make_marker_with_dedup (
813+ s, state, MarkerKind :: ToolUseInput , Some ( seen) ) {
800814 out. bytes_tool_use_input += s. len ( ) ;
801815 out. rewritten_count += 1 ;
802816 * val = Value :: String ( marker) ;
@@ -805,12 +819,12 @@ fn rewrite_strings_recursive(
805819 }
806820 Value :: Object ( map) => {
807821 for ( _k, v) in map. iter_mut ( ) {
808- rewrite_strings_recursive ( v, state, out) ;
822+ rewrite_strings_recursive ( v, state, out, seen ) ;
809823 }
810824 }
811825 Value :: Array ( arr) => {
812826 for v in arr. iter_mut ( ) {
813- rewrite_strings_recursive ( v, state, out) ;
827+ rewrite_strings_recursive ( v, state, out, seen ) ;
814828 }
815829 }
816830 _ => { }
@@ -819,11 +833,13 @@ fn rewrite_strings_recursive(
819833
820834fn rewrite_tool_result_content (
821835 inner : & mut Value , state : & AppState , out : & mut RewriteOutcome ,
836+ seen : & mut std:: collections:: HashSet < i64 > ,
822837) {
823838 match inner {
824839 Value :: String ( s) => {
825840 if s. len ( ) >= state. rewrite_threshold {
826- if let Ok ( marker) = make_marker ( s, state) {
841+ if let Ok ( marker) = make_marker_with_dedup (
842+ s, state, MarkerKind :: ToolResult , Some ( seen) ) {
827843 out. bytes_tool_result += s. len ( ) ;
828844 out. rewritten_count += 1 ;
829845 * inner = Value :: String ( marker) ;
@@ -835,7 +851,9 @@ fn rewrite_tool_result_content(
835851 if part. get ( "type" ) . and_then ( Value :: as_str) == Some ( "text" ) {
836852 let Some ( text) = part. get ( "text" ) . and_then ( Value :: as_str) else { continue } ;
837853 if text. len ( ) < state. rewrite_threshold { continue ; }
838- let Ok ( marker) = make_marker ( text, state) else { continue } ;
854+ let Ok ( marker) = make_marker_with_dedup (
855+ text, state, MarkerKind :: ToolResult , Some ( seen) )
856+ else { continue } ;
839857 out. bytes_tool_result += text. len ( ) ;
840858 out. rewritten_count += 1 ;
841859 part[ "text" ] = Value :: String ( marker) ;
@@ -846,23 +864,68 @@ fn rewrite_tool_result_content(
846864 }
847865}
848866
849- fn make_marker ( text : & str , state : & AppState ) -> Result < String > {
867+ /// What category of content is being compressed. Drives whether the marker
868+ /// gets a `preview=` attribute (helpful for tool_result, wasted bytes for
869+ /// historical assistant text or tool_use inputs).
870+ #[ derive( Copy , Clone , Debug , PartialEq , Eq ) ]
871+ enum MarkerKind {
872+ /// `tool_result.content` — LLM benefits from preview to know if it
873+ /// needs to expand. Keep the full marker.
874+ ToolResult ,
875+ /// Historical assistant or user text block, system prompt, tool def.
876+ /// LLM has already "seen" this in a prior turn; preview is wasted.
877+ HistoricalText ,
878+ /// `tool_use.input` field value — LLM emitted this itself, doesn't
879+ /// need to re-read its own output. Preview is wasted.
880+ ToolUseInput ,
881+ }
882+
883+ fn make_marker ( text : & str , state : & AppState , kind : MarkerKind ) -> Result < String > {
884+ make_marker_with_dedup ( text, state, kind, None )
885+ }
886+
887+ /// v0.14.7-L: intra-request dedup. `seen_hashes` is `Some(set)` when we want
888+ /// to track repeated content within a single request — first occurrence
889+ /// emits the full marker, subsequent emit the bare `<omc:ref h="..."/>`
890+ /// form (~30 bytes instead of ~150 for the duplicates).
891+ fn make_marker_with_dedup (
892+ text : & str , state : & AppState , kind : MarkerKind ,
893+ seen_hashes : Option < & mut std:: collections:: HashSet < i64 > > ,
894+ ) -> Result < String > {
850895 let hash = state. store . store ( PROXY_CACHE_NAMESPACE , text)
851896 . map_err ( anyhow:: Error :: msg) ?;
852- // For very large blocks the LLM almost certainly wants either:
853- // (a) the full content (expand via tool), or (b) to move on.
854- // The preview adds no decision-quality. Drop it past 8 KB.
855- if text. len ( ) >= 8192 {
856- return Ok ( format ! ( "<omc:ref h=\" {}\" b=\" {}\" />" , hash, text. len( ) ) ) ;
897+
898+ // v0.14.7-L: if we've already emitted a full marker for this hash this
899+ // request, the subsequent ones can be the bare-minimum form.
900+ if let Some ( set) = seen_hashes {
901+ if !set. insert ( hash) {
902+ // already present
903+ return Ok ( format ! ( "<omc:ref h=\" {}\" />" , hash) ) ;
904+ }
905+ }
906+
907+ // v0.14.7-K: drop preview for content the LLM doesn't benefit from
908+ // previewing. Saves ~150 bytes/marker × 100s of markers per turn.
909+ match kind {
910+ MarkerKind :: HistoricalText | MarkerKind :: ToolUseInput => {
911+ Ok ( format ! ( "<omc:ref h=\" {}\" b=\" {}\" />" , hash, text. len( ) ) )
912+ }
913+ MarkerKind :: ToolResult => {
914+ // Keep the preview only when content is large enough that the
915+ // LLM might want to decide whether to expand.
916+ if text. len ( ) >= 8192 {
917+ return Ok ( format ! ( "<omc:ref h=\" {}\" b=\" {}\" />" , hash, text. len( ) ) ) ;
918+ }
919+ let preview: String = text. chars ( )
920+ . filter ( |c| !c. is_control ( ) )
921+ . take ( state. preview_bytes )
922+ . collect ( ) ;
923+ Ok ( format ! (
924+ "<omc:ref hash_str=\" {}\" bytes=\" {}\" preview={:?}/>" ,
925+ hash, text. len( ) , preview
926+ ) )
927+ }
857928 }
858- let preview: String = text. chars ( )
859- . filter ( |c| !c. is_control ( ) )
860- . take ( state. preview_bytes )
861- . collect ( ) ;
862- Ok ( format ! (
863- "<omc:ref hash_str=\" {}\" bytes=\" {}\" preview={:?}/>" ,
864- hash, text. len( ) , preview
865- ) )
866929}
867930
868931/// Add the omc_proxy_expand_ref tool to the request's tools array so the
@@ -993,7 +1056,7 @@ mod tests {
9931056 fn marker_round_trip_lossless ( ) {
9941057 let state = test_state ( 256 ) ;
9951058 let original = "abc🎯 ñ é 漢字\n line2\n \t indented\n " . repeat ( 50 ) ; // multi-byte, control chars
996- let marker = make_marker ( & original, & state) . unwrap ( ) ;
1059+ let marker = make_marker ( & original, & state, MarkerKind :: ToolResult ) . unwrap ( ) ;
9971060 // Extract hash_str from the marker
9981061 let hash_attr = marker. split ( "hash_str=\" " ) . nth ( 1 ) . unwrap ( ) ;
9991062 let hash_str = hash_attr. split ( '"' ) . next ( ) . unwrap ( ) ;
@@ -1228,6 +1291,97 @@ mod tests {
12281291 "user's cache_control preserved" ) ;
12291292 }
12301293
1294+ /// v0.14.7-K: markers for HistoricalText/ToolUseInput drop the
1295+ /// preview attribute (saves ~150 bytes/marker).
1296+ #[ test]
1297+ fn slim_markers_drop_preview_for_historical_text ( ) {
1298+ let state = test_state ( 256 ) ;
1299+ let big = "X" . repeat ( 1000 ) ;
1300+ // Historical text in messages[]
1301+ let req = json ! ( {
1302+ "model" : "test" , "max_tokens" : 10 ,
1303+ "messages" : [
1304+ { "role" : "assistant" , "content" : big. clone( ) } ,
1305+ { "role" : "user" , "content" : "ask" }
1306+ ]
1307+ } ) ;
1308+ let body = serde_json:: to_vec ( & req) . unwrap ( ) ;
1309+ let ( out, _) = rewrite_request_body ( & body, & state) . unwrap ( ) ;
1310+ let v: Value = serde_json:: from_slice ( & out) . unwrap ( ) ;
1311+ let marker = v[ "messages" ] [ 0 ] [ "content" ] . as_str ( ) . unwrap ( ) ;
1312+ // HistoricalText markers should be the slim form: <omc:ref h="..." b="N"/>
1313+ assert ! ( marker. contains( " h=\" " ) , "slim marker should use h= not hash_str=" ) ;
1314+ assert ! ( marker. contains( " b=\" " ) , "slim marker should use b= not bytes=" ) ;
1315+ assert ! ( !marker. contains( "preview=" ) ,
1316+ "slim marker for HistoricalText must not have preview" ) ;
1317+ // tool_result markers, by contrast, keep preview (for content < 8KB)
1318+ let req2 = json ! ( {
1319+ "model" : "test" , "max_tokens" : 10 ,
1320+ "messages" : [
1321+ { "role" : "user" , "content" : [
1322+ { "type" : "tool_result" , "tool_use_id" : "x" , "content" : big. clone( ) }
1323+ ] } ,
1324+ { "role" : "user" , "content" : "ask" }
1325+ ]
1326+ } ) ;
1327+ let ( out2, _) = rewrite_request_body ( & serde_json:: to_vec ( & req2) . unwrap ( ) , & state) . unwrap ( ) ;
1328+ let v2: Value = serde_json:: from_slice ( & out2) . unwrap ( ) ;
1329+ let tr_marker = v2[ "messages" ] [ 0 ] [ "content" ] [ 0 ] [ "content" ] . as_str ( ) . unwrap ( ) ;
1330+ assert ! ( tr_marker. contains( "preview=" ) ,
1331+ "tool_result marker should keep preview (LLM needs it to decide expansion)" ) ;
1332+ }
1333+
1334+ /// v0.14.7-L: when the same content appears twice in one request,
1335+ /// the second occurrence collapses to bare `<omc:ref h="..."/>`.
1336+ #[ test]
1337+ fn intra_request_dedup_collapses_repeats ( ) {
1338+ let state = test_state ( 256 ) ;
1339+ let big = "REPEATING CONTENT BLOCK " . repeat ( 50 ) ; // ~1200 bytes
1340+ let req = json ! ( {
1341+ "model" : "test" , "max_tokens" : 10 ,
1342+ "messages" : [
1343+ { "role" : "assistant" , "content" : big. clone( ) } ,
1344+ { "role" : "user" , "content" : big. clone( ) } ,
1345+ { "role" : "assistant" , "content" : big. clone( ) } ,
1346+ { "role" : "user" , "content" : "current" }
1347+ ]
1348+ } ) ;
1349+ let body = serde_json:: to_vec ( & req) . unwrap ( ) ;
1350+ let ( out, _) = rewrite_request_body ( & body, & state) . unwrap ( ) ;
1351+ let v: Value = serde_json:: from_slice ( & out) . unwrap ( ) ;
1352+ // Helper: pull marker text from a message regardless of whether
1353+ // it's string-form or array-form (cache_control insertion can
1354+ // convert string-form to array-form mid-pass).
1355+ let extract = |idx : usize | -> String {
1356+ let c = & v[ "messages" ] [ idx] [ "content" ] ;
1357+ if let Some ( s) = c. as_str ( ) { return s. to_string ( ) ; }
1358+ if let Some ( arr) = c. as_array ( ) {
1359+ if let Some ( first) = arr. first ( ) {
1360+ if let Some ( t) = first. get ( "text" ) . and_then ( Value :: as_str) {
1361+ return t. to_string ( ) ;
1362+ }
1363+ }
1364+ }
1365+ panic ! ( "could not extract marker from messages[{}]: {}" , idx, c)
1366+ } ;
1367+ let m0 = extract ( 0 ) ;
1368+ let m1 = extract ( 1 ) ;
1369+ let m2 = extract ( 2 ) ;
1370+ // First occurrence: full slim marker with b=
1371+ assert ! ( m0. contains( " b=\" " ) , "first marker should be full: {}" , m0) ;
1372+ // Second + third: bare form (no b= attr)
1373+ assert ! ( !m1. contains( " b=\" " ) ,
1374+ "second occurrence should be bare ref: {}" , m1) ;
1375+ assert ! ( !m2. contains( " b=\" " ) ,
1376+ "third occurrence should be bare ref: {}" , m2) ;
1377+ // All three reference the same hash
1378+ let extract_h = |m : & str | -> String {
1379+ m. split ( " h=\" " ) . nth ( 1 ) . unwrap ( ) . split ( '"' ) . next ( ) . unwrap ( ) . to_string ( )
1380+ } ;
1381+ assert_eq ! ( extract_h( & m0) , extract_h( & m1) ) ;
1382+ assert_eq ! ( extract_h( & m0) , extract_h( & m2) ) ;
1383+ }
1384+
12311385 /// Multi-turn dogfood simulation: walk a conversation, verify each turn's
12321386 /// rewrite preserves the LLM-emitted shape AND the markers expand cleanly
12331387 /// to the original bytes via the cache.
0 commit comments