@@ -3,10 +3,13 @@ import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
33import { buildModelsRequest } from "../src/oauth" ;
44import {
55 resolveProviderTransport ,
6+ deriveXaiConvId ,
7+ XAI_CONV_ID_HEADER ,
68 XAI_GROK_CLI_BASE_URL ,
79 XAI_GROK_CLIENT_VERSION ,
810} from "../src/providers/xai-transport" ;
9- import type { OcxParsedRequest , OcxProviderConfig } from "../src/types" ;
11+ import { getProviderRegistryEntry } from "../src/providers/registry" ;
12+ import type { OcxAssistantMessage , OcxParsedRequest , OcxProviderConfig } from "../src/types" ;
1013
1114function provider ( authMode : "oauth" | "key" ) : OcxProviderConfig {
1215 return {
@@ -83,3 +86,100 @@ describe("xAI auth-mode transport selection", () => {
8386 } ) ;
8487 } ) ;
8588} ) ;
89+
90+ describe ( "xAI prompt-cache conv-id affinity" , ( ) => {
91+ test ( "promptCacheKey derives a stable hashed x-grok-conv-id in oauth mode" , ( ) => {
92+ const effective = resolveProviderTransport ( "xai" , provider ( "oauth" ) , "codex-session-abc" ) ;
93+ const again = resolveProviderTransport ( "xai" , provider ( "oauth" ) , "codex-session-abc" ) ;
94+
95+ expect ( effective . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBe ( deriveXaiConvId ( "codex-session-abc" ) ) ;
96+ expect ( effective . headers ?. [ XAI_CONV_ID_HEADER ] ) . toMatch ( / ^ [ 0 - 9 a - f ] { 32 } $ / ) ;
97+ // Stable across requests (cache affinity) and never the raw session id.
98+ expect ( again . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBe ( effective . headers ?. [ XAI_CONV_ID_HEADER ] ) ;
99+ expect ( effective . headers ?. [ XAI_CONV_ID_HEADER ] ) . not . toContain ( "codex-session-abc" ) ;
100+ } ) ;
101+
102+ test ( "key mode gains conv-id affinity without touching baseUrl or CLI headers" , ( ) => {
103+ const configured = provider ( "key" ) ;
104+ const effective = resolveProviderTransport ( "xai" , configured , "codex-session-abc" ) ;
105+ const request = createOpenAIChatAdapter ( effective ) . buildRequest ( parsed ( ) ) ;
106+
107+ expect ( effective . baseUrl ) . toBe ( "https://api.x.ai/v1" ) ;
108+ expect ( request . url ) . toBe ( "https://api.x.ai/v1/chat/completions" ) ;
109+ expect ( effective . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBe ( deriveXaiConvId ( "codex-session-abc" ) ) ;
110+ expect ( effective . headers ?. [ "x-grok-client-identifier" ] ) . toBeUndefined ( ) ;
111+ expect ( effective . headers ?. [ "x-xai-token-auth" ] ) . toBeUndefined ( ) ;
112+ } ) ;
113+
114+ test ( "missing, empty, and whitespace-only cache keys never emit a conv-id" , ( ) => {
115+ const noKeyOauth = resolveProviderTransport ( "xai" , provider ( "oauth" ) ) ;
116+ const emptyKeyOauth = resolveProviderTransport ( "xai" , provider ( "oauth" ) , "" ) ;
117+ const blankKeyOauth = resolveProviderTransport ( "xai" , provider ( "oauth" ) , " " ) ;
118+ const configuredKey = provider ( "key" ) ;
119+ const emptyKeyApi = resolveProviderTransport ( "xai" , configuredKey , "" ) ;
120+
121+ expect ( noKeyOauth . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBeUndefined ( ) ;
122+ expect ( emptyKeyOauth . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBeUndefined ( ) ;
123+ expect ( blankKeyOauth . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBeUndefined ( ) ;
124+ // Key mode without a conv-id stays the exact configured object (no clone churn).
125+ expect ( emptyKeyApi ) . toBe ( configuredKey ) ;
126+ } ) ;
127+
128+ test ( "user-configured conv-id header wins in any casing (no duplicate header pair)" , ( ) => {
129+ const lower = provider ( "oauth" ) ;
130+ lower . headers = { [ XAI_CONV_ID_HEADER ] : "user-pinned" } ;
131+ const mixed = provider ( "key" ) ;
132+ mixed . headers = { "X-Grok-Conv-Id" : "user-pinned-mixed" } ;
133+
134+ const lowerResolved = resolveProviderTransport ( "xai" , lower , "codex-session-abc" ) ;
135+ const mixedResolved = resolveProviderTransport ( "xai" , mixed , "codex-session-abc" ) ;
136+
137+ expect ( lowerResolved . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBe ( "user-pinned" ) ;
138+ // Mixed casing: the generated lowercase header must be suppressed entirely.
139+ expect ( mixedResolved . headers ?. [ "X-Grok-Conv-Id" ] ) . toBe ( "user-pinned-mixed" ) ;
140+ expect ( mixedResolved . headers ?. [ XAI_CONV_ID_HEADER ] ) . toBeUndefined ( ) ;
141+ const convIdKeys = Object . keys ( mixedResolved . headers ?? { } ) . filter ( k => k . toLowerCase ( ) === XAI_CONV_ID_HEADER ) ;
142+ expect ( convIdKeys ) . toHaveLength ( 1 ) ;
143+ } ) ;
144+ } ) ;
145+
146+ describe ( "xAI reasoning_content cache preservation" , ( ) => {
147+ test ( "registry preset replays reasoning_content for grok reasoning models only" , ( ) => {
148+ const entry = getProviderRegistryEntry ( "xai" ) ;
149+ expect ( entry ?. preserveReasoningContentModels ) . toEqual ( [
150+ "grok-4.5" ,
151+ "grok-4.3" ,
152+ "grok-4.20-multi-agent-0309" ,
153+ "grok-4.20-0309-reasoning" ,
154+ ] ) ;
155+ for ( const noReasoning of entry ?. noReasoningModels ?? [ ] ) {
156+ expect ( entry ?. preserveReasoningContentModels ) . not . toContain ( noReasoning ) ;
157+ }
158+ } ) ;
159+
160+ test ( "assistant thinking parts round-trip as reasoning_content on grok-4.5 history" , ( ) => {
161+ // xAI docs: dropped reasoning_content is the top cause of multi-turn cache misses
162+ // (docs.x.ai prompt-caching/multi-turn, 2026-07-13).
163+ const prov : OcxProviderConfig = {
164+ ...provider ( "oauth" ) ,
165+ preserveReasoningContentModels : getProviderRegistryEntry ( "xai" ) ?. preserveReasoningContentModels ?? [ ] ,
166+ } ;
167+ const assistant : OcxAssistantMessage = {
168+ role : "assistant" ,
169+ content : [
170+ { type : "thinking" , thinking : "cached chain" } ,
171+ { type : "text" , text : "answer" } ,
172+ ] ,
173+ timestamp : 0 ,
174+ } ;
175+ const req : OcxParsedRequest = {
176+ modelId : "grok-4.5" ,
177+ context : { messages : [ { role : "user" , content : "q1" , timestamp : 0 } , assistant , { role : "user" , content : "q2" , timestamp : 0 } ] } ,
178+ stream : false ,
179+ options : { } ,
180+ } ;
181+ const body = JSON . parse ( createOpenAIChatAdapter ( prov ) . buildRequest ( req ) . body as string ) as { messages : Array < Record < string , unknown > > } ;
182+ const replayed = body . messages . find ( m => m . role === "assistant" ) ;
183+ expect ( replayed ?. reasoning_content ) . toBe ( "cached chain" ) ;
184+ } ) ;
185+ } ) ;
0 commit comments