openai_content_session_seed_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/stretchr/testify/require"
  7. "github.com/tidwall/gjson"
  8. )
  9. func TestDeriveOpenAIContentSessionSeed_EmptyInputs(t *testing.T) {
  10. require.Empty(t, deriveOpenAIContentSessionSeed(nil))
  11. require.Empty(t, deriveOpenAIContentSessionSeed([]byte{}))
  12. require.Empty(t, deriveOpenAIContentSessionSeed([]byte(`{}`)))
  13. }
  14. func TestDeriveOpenAIContentSessionSeed_ModelOnly(t *testing.T) {
  15. seed := deriveOpenAIContentSessionSeed([]byte(`{"model":"gpt-5.4"}`))
  16. require.Contains(t, seed, contentSessionSeedPrefix)
  17. require.Contains(t, seed, "model=gpt-5.4")
  18. }
  19. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_StableAcrossTurns(t *testing.T) {
  20. turn1 := []byte(`{
  21. "model": "gpt-5.4",
  22. "messages": [
  23. {"role": "system", "content": "You are helpful."},
  24. {"role": "user", "content": "Hello"}
  25. ]
  26. }`)
  27. turn2 := []byte(`{
  28. "model": "gpt-5.4",
  29. "messages": [
  30. {"role": "system", "content": "You are helpful."},
  31. {"role": "user", "content": "Hello"},
  32. {"role": "assistant", "content": "Hi there!"},
  33. {"role": "user", "content": "How are you?"}
  34. ]
  35. }`)
  36. s1 := deriveOpenAIContentSessionSeed(turn1)
  37. s2 := deriveOpenAIContentSessionSeed(turn2)
  38. require.Equal(t, s1, s2, "seed should be stable across later turns")
  39. require.NotEmpty(t, s1)
  40. }
  41. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_IgnoresLaterSystemMessages(t *testing.T) {
  42. turn1 := []byte(`{
  43. "model": "gpt-5.4",
  44. "messages": [
  45. {"role": "system", "content": "You are helpful."},
  46. {"role": "user", "content": "Hello"},
  47. {"role": "assistant", "content": "Hi there!"},
  48. {"role": "user", "content": "How are you?"}
  49. ]
  50. }`)
  51. turn2 := []byte(`{
  52. "model": "gpt-5.4",
  53. "messages": [
  54. {"role": "system", "content": "You are helpful."},
  55. {"role": "user", "content": "Hello"},
  56. {"role": "assistant", "content": "Hi there!"},
  57. {"role": "system", "content": "Return JSON for this turn."},
  58. {"role": "user", "content": "How are you?"}
  59. ]
  60. }`)
  61. require.Equal(t, deriveOpenAIContentSessionSeed(turn1), deriveOpenAIContentSessionSeed(turn2))
  62. }
  63. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_UsesLeadingSystemDeveloperPrefix(t *testing.T) {
  64. firstSystem := []byte(`{
  65. "model": "gpt-5.4",
  66. "messages": [
  67. {"role": "system", "content": "System A"},
  68. {"role": "developer", "content": "Developer B"},
  69. {"role": "user", "content": "Hello"}
  70. ]
  71. }`)
  72. changedLaterSystem := []byte(`{
  73. "model": "gpt-5.4",
  74. "messages": [
  75. {"role": "system", "content": "System A"},
  76. {"role": "developer", "content": "Developer C"},
  77. {"role": "user", "content": "Hello"}
  78. ]
  79. }`)
  80. seed := deriveOpenAIContentSessionSeed(firstSystem)
  81. require.Contains(t, seed, "System A")
  82. require.Contains(t, seed, "Developer B")
  83. require.NotEqual(t, seed, deriveOpenAIContentSessionSeed(changedLaterSystem))
  84. withLaterSystem := []byte(`{
  85. "model": "gpt-5.4",
  86. "messages": [
  87. {"role": "system", "content": "System A"},
  88. {"role": "developer", "content": "Developer B"},
  89. {"role": "user", "content": "Hello"},
  90. {"role": "system", "content": "Dynamic system"}
  91. ]
  92. }`)
  93. require.Equal(t, seed, deriveOpenAIContentSessionSeed(withLaterSystem))
  94. }
  95. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_DifferentFirstUserDiffers(t *testing.T) {
  96. req1 := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Question A"}]}`)
  97. req2 := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Question B"}]}`)
  98. s1 := deriveOpenAIContentSessionSeed(req1)
  99. s2 := deriveOpenAIContentSessionSeed(req2)
  100. require.NotEqual(t, s1, s2)
  101. }
  102. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_DifferentSystemDiffers(t *testing.T) {
  103. req1 := []byte(`{"model":"gpt-5.4","messages":[{"role":"system","content":"A"},{"role":"user","content":"Hi"}]}`)
  104. req2 := []byte(`{"model":"gpt-5.4","messages":[{"role":"system","content":"B"},{"role":"user","content":"Hi"}]}`)
  105. s1 := deriveOpenAIContentSessionSeed(req1)
  106. s2 := deriveOpenAIContentSessionSeed(req2)
  107. require.NotEqual(t, s1, s2)
  108. }
  109. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_DifferentModelDiffers(t *testing.T) {
  110. req1 := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Hi"}]}`)
  111. req2 := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"Hi"}]}`)
  112. s1 := deriveOpenAIContentSessionSeed(req1)
  113. s2 := deriveOpenAIContentSessionSeed(req2)
  114. require.NotEqual(t, s1, s2)
  115. }
  116. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_WithTools(t *testing.T) {
  117. withTools := []byte(`{
  118. "model": "gpt-5.4",
  119. "tools": [{"type":"function","function":{"name":"get_weather"}}],
  120. "messages": [{"role": "user", "content": "Hello"}]
  121. }`)
  122. withoutTools := []byte(`{
  123. "model": "gpt-5.4",
  124. "messages": [{"role": "user", "content": "Hello"}]
  125. }`)
  126. s1 := deriveOpenAIContentSessionSeed(withTools)
  127. s2 := deriveOpenAIContentSessionSeed(withoutTools)
  128. require.NotEqual(t, s1, s2, "tools should affect the seed")
  129. require.Contains(t, s1, "|tools=")
  130. }
  131. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_WithFunctions(t *testing.T) {
  132. body := []byte(`{
  133. "model": "gpt-5.4",
  134. "functions": [{"name":"get_weather","parameters":{}}],
  135. "messages": [{"role": "user", "content": "Hello"}]
  136. }`)
  137. seed := deriveOpenAIContentSessionSeed(body)
  138. require.Contains(t, seed, "|functions=")
  139. }
  140. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_DeveloperRole(t *testing.T) {
  141. body := []byte(`{
  142. "model": "gpt-5.4",
  143. "messages": [
  144. {"role": "developer", "content": "You are helpful."},
  145. {"role": "user", "content": "Hello"}
  146. ]
  147. }`)
  148. seed := deriveOpenAIContentSessionSeed(body)
  149. require.Contains(t, seed, "|system=")
  150. require.Contains(t, seed, "|first_user=")
  151. }
  152. func TestDeriveOpenAIContentSessionSeed_ChatCompletions_StructuredContent(t *testing.T) {
  153. body := []byte(`{
  154. "model": "gpt-5.4",
  155. "messages": [
  156. {"role": "user", "content": [{"type":"text","text":"Hello"}]}
  157. ]
  158. }`)
  159. seed := deriveOpenAIContentSessionSeed(body)
  160. require.NotEmpty(t, seed)
  161. require.Contains(t, seed, "|first_user=")
  162. }
  163. func TestDeriveOpenAIContentSessionSeed_ResponsesAPI_InputString(t *testing.T) {
  164. body := []byte(`{"model":"gpt-5.4","input":"Hello, how are you?"}`)
  165. seed := deriveOpenAIContentSessionSeed(body)
  166. require.Contains(t, seed, "|input=Hello, how are you?")
  167. }
  168. func TestDeriveOpenAIContentSessionSeed_ResponsesAPI_InputArray(t *testing.T) {
  169. body := []byte(`{
  170. "model": "gpt-5.4",
  171. "input": [
  172. {"role": "system", "content": "You are helpful."},
  173. {"role": "user", "content": "Hello"}
  174. ]
  175. }`)
  176. seed := deriveOpenAIContentSessionSeed(body)
  177. require.Contains(t, seed, "|system=")
  178. require.Contains(t, seed, "|first_user=")
  179. }
  180. func TestDeriveOpenAIContentSessionSeed_ResponsesAPI_WithInstructions(t *testing.T) {
  181. body := []byte(`{
  182. "model": "gpt-5.4",
  183. "instructions": "You are a coding assistant.",
  184. "input": "Write a hello world"
  185. }`)
  186. seed := deriveOpenAIContentSessionSeed(body)
  187. require.Contains(t, seed, "|instructions=You are a coding assistant.")
  188. require.Contains(t, seed, "|input=Write a hello world")
  189. }
  190. func TestDeriveOpenAIContentSessionSeed_Deterministic(t *testing.T) {
  191. body := []byte(`{
  192. "model": "gpt-5.4",
  193. "messages": [
  194. {"role": "system", "content": "You are helpful."},
  195. {"role": "user", "content": "Hello"}
  196. ]
  197. }`)
  198. s1 := deriveOpenAIContentSessionSeed(body)
  199. s2 := deriveOpenAIContentSessionSeed(body)
  200. require.Equal(t, s1, s2, "seed must be deterministic")
  201. }
  202. func TestDeriveOpenAIContentSessionSeed_PrefixPresent(t *testing.T) {
  203. body := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Hi"}]}`)
  204. seed := deriveOpenAIContentSessionSeed(body)
  205. require.True(t, len(seed) > len(contentSessionSeedPrefix))
  206. require.Equal(t, contentSessionSeedPrefix, seed[:len(contentSessionSeedPrefix)])
  207. }
  208. func TestDeriveOpenAIContentSessionSeed_EmptyToolsIgnored(t *testing.T) {
  209. body := []byte(`{"model":"gpt-5.4","tools":[],"messages":[{"role":"user","content":"Hi"}]}`)
  210. seed := deriveOpenAIContentSessionSeed(body)
  211. require.NotContains(t, seed, "|tools=")
  212. }
  213. func TestDeriveOpenAIContentSessionSeed_MessagesPreferredOverInput(t *testing.T) {
  214. body := []byte(`{
  215. "model": "gpt-5.4",
  216. "messages": [{"role": "user", "content": "from messages"}],
  217. "input": "from input"
  218. }`)
  219. seed := deriveOpenAIContentSessionSeed(body)
  220. require.Contains(t, seed, "|first_user=")
  221. require.NotContains(t, seed, "|input=")
  222. }
  223. func TestDeriveOpenAIContentSessionSeed_JSONCanonicalisation(t *testing.T) {
  224. compact := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","function":{"name":"get_weather","description":"Get weather"}}],"messages":[{"role":"user","content":"Hi"}]}`)
  225. spaced := []byte(`{
  226. "model": "gpt-5.4",
  227. "tools": [
  228. { "type" : "function", "function": { "description": "Get weather", "name": "get_weather" } }
  229. ],
  230. "messages": [ { "role": "user", "content": "Hi" } ]
  231. }`)
  232. s1 := deriveOpenAIContentSessionSeed(compact)
  233. s2 := deriveOpenAIContentSessionSeed(spaced)
  234. require.Equal(t, s1, s2, "different formatting of identical JSON should produce the same seed")
  235. }
  236. func TestDeriveOpenAIContentSessionSeed_SingleScanMatchesLegacyBytes(t *testing.T) {
  237. largeValue := strings.Repeat("payload", 1<<17)
  238. tests := []struct {
  239. name string
  240. body []byte
  241. }{
  242. {
  243. name: "large chat completions",
  244. body: []byte(`{"metadata":"` + largeValue + `","model":"gpt-5.4","tools":[{"type":"function","function":{"name":"lookup"}}],"functions":[{"name":"legacy_lookup"}],"messages":[{"role":"system","content":"System prompt"},{"role":"developer","content":[{"type":"text","text":"Developer prompt"}]},{"role":"user","content":"Hello"}]}`),
  245. },
  246. {
  247. name: "large responses",
  248. body: []byte(`{"metadata":"` + largeValue + `","model":"gpt-5.4","instructions":"Be concise.","tools":[{"type":"function","name":"lookup"}],"input":[{"role":"system","content":"System prompt"},{"role":"user","content":[{"type":"input_text","text":"Hello"}]}]}`),
  249. },
  250. {
  251. name: "fields in reverse order",
  252. body: []byte(`{"input":"fallback input","messages":[{"role":"user","content":"chat wins"}],"instructions":"Be concise.","functions":[{"name":"lookup"}],"tools":[{"type":"function","name":"lookup"}],"model":"gpt-5.4"}`),
  253. },
  254. {
  255. name: "missing and wrong type fields",
  256. body: []byte(`{"tools":[],"functions":null,"instructions":0,"messages":{},"input":[{"type":"input_text","text":"fallback"}]}`),
  257. },
  258. {
  259. name: "duplicate fields keep first value",
  260. body: []byte(`{"model":"first","model":"second","tools":[{"name":"first"}],"tools":[{"name":"second"}],"functions":[],"functions":[{"name":"second"}],"instructions":"first","instructions":"second","messages":null,"messages":[{"role":"user","content":"second"}],"input":"first input","input":"second input"}`),
  261. },
  262. {
  263. name: "escaped field names",
  264. body: []byte(`{"mo\u0064el":"gpt-5.4","mess\u0061ges":[{"role":"user","content":"Hello"}]}`),
  265. },
  266. {
  267. name: "trailing object fields are outside the root",
  268. body: []byte(`{"foo":1}{"model":"trailing","input":"trailing input"}`),
  269. },
  270. {
  271. name: "trailing quoted fields are outside the root",
  272. body: []byte(`{"model":"root"}"input":"trailing input"`),
  273. },
  274. {
  275. name: "leading garbage before the root",
  276. body: []byte(`garbage{"model":"gpt-5.4","input":"Hello"}`),
  277. },
  278. {
  279. name: "escaped braces remain inside string values",
  280. body: []byte(`{"metadata":"escaped } and [ and \" quote","model":"root","input":"Hello"}{"model":"trailing"}`),
  281. },
  282. {
  283. name: "nested braces do not end the root",
  284. body: []byte(`{"metadata":{"nested":"} ]"},"model":"root","input":"Hello"}{"model":"trailing"}`),
  285. },
  286. {
  287. name: "root array does not expose nested or trailing object fields",
  288. body: []byte(`[{"model":"nested"}]{"model":"trailing","input":"trailing input"}`),
  289. },
  290. {
  291. name: "trailing messages do not override root input",
  292. body: []byte(`{"input":"root input"}{"messages":[{"role":"user","content":"trailing"}]}`),
  293. },
  294. {
  295. name: "truncated string containing a closing brace",
  296. body: []byte(`{"model":"root","metadata":"still } inside`),
  297. },
  298. {
  299. name: "lenient truncated body",
  300. body: []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Hello"}]`),
  301. },
  302. }
  303. for _, test := range tests {
  304. t.Run(test.name, func(t *testing.T) {
  305. require.Equal(t, referenceDeriveOpenAIContentSessionSeed(test.body), deriveOpenAIContentSessionSeed(test.body))
  306. })
  307. }
  308. }
  309. func TestDeriveOpenAIContentSessionSeed_AllTruncationOffsetsMatchLegacyBytes(t *testing.T) {
  310. bodies := []string{
  311. `{"model":"gpt-5.4","tools":[{"type":"function","function":{"name":"lookup"}}],"functions":[{"name":"legacy"}],"instructions":"escaped \" } text","messages":[{"role":"system","content":"System"},{"role":"user","content":[{"type":"text","text":"Hello"}]}],"input":"fallback"}`,
  312. `{"model":"gpt-5.4","instructions":"Be concise.","tools":[{"type":"function","name":"lookup"}],"input":[{"role":"system","content":"System"},{"role":"user","content":[{"type":"input_text","text":"Hello"}]}]}`,
  313. }
  314. for bodyIndex, body := range bodies {
  315. for end := 1; end < len(body); end++ {
  316. truncated := []byte(body[:end])
  317. require.Equalf(t, referenceDeriveOpenAIContentSessionSeed(truncated), deriveOpenAIContentSessionSeed(truncated), "body %d truncated at byte %d", bodyIndex, end)
  318. }
  319. }
  320. }
  321. func referenceDeriveOpenAIContentSessionSeed(body []byte) string {
  322. if len(body) == 0 {
  323. return ""
  324. }
  325. var b strings.Builder
  326. if model := gjson.GetBytes(body, "model").String(); model != "" {
  327. _, _ = b.WriteString("model=")
  328. _, _ = b.WriteString(model)
  329. }
  330. if tools := gjson.GetBytes(body, "tools"); tools.Exists() && tools.IsArray() && tools.Raw != "[]" {
  331. _, _ = b.WriteString("|tools=")
  332. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(tools.Raw)))
  333. }
  334. if funcs := gjson.GetBytes(body, "functions"); funcs.Exists() && funcs.IsArray() && funcs.Raw != "[]" {
  335. _, _ = b.WriteString("|functions=")
  336. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(funcs.Raw)))
  337. }
  338. if instr := gjson.GetBytes(body, "instructions").String(); instr != "" {
  339. _, _ = b.WriteString("|instructions=")
  340. _, _ = b.WriteString(instr)
  341. }
  342. firstUserCaptured := false
  343. msgs := gjson.GetBytes(body, "messages")
  344. if msgs.Exists() && msgs.IsArray() {
  345. systemPrefixOpen := true
  346. msgs.ForEach(func(_, msg gjson.Result) bool {
  347. role := msg.Get("role").String()
  348. switch role {
  349. case "system", "developer":
  350. if systemPrefixOpen {
  351. _, _ = b.WriteString("|system=")
  352. if c := msg.Get("content"); c.Exists() {
  353. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
  354. }
  355. }
  356. case "user":
  357. systemPrefixOpen = false
  358. if !firstUserCaptured {
  359. _, _ = b.WriteString("|first_user=")
  360. if c := msg.Get("content"); c.Exists() {
  361. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
  362. }
  363. firstUserCaptured = true
  364. }
  365. default:
  366. systemPrefixOpen = false
  367. }
  368. return true
  369. })
  370. } else if inp := gjson.GetBytes(body, "input"); inp.Exists() {
  371. if inp.Type == gjson.String {
  372. _, _ = b.WriteString("|input=")
  373. _, _ = b.WriteString(inp.String())
  374. } else if inp.IsArray() {
  375. inp.ForEach(func(_, item gjson.Result) bool {
  376. role := item.Get("role").String()
  377. switch role {
  378. case "system", "developer":
  379. _, _ = b.WriteString("|system=")
  380. if c := item.Get("content"); c.Exists() {
  381. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
  382. }
  383. case "user":
  384. if !firstUserCaptured {
  385. _, _ = b.WriteString("|first_user=")
  386. if c := item.Get("content"); c.Exists() {
  387. _, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
  388. }
  389. firstUserCaptured = true
  390. }
  391. }
  392. if !firstUserCaptured && item.Get("type").String() == "input_text" {
  393. _, _ = b.WriteString("|first_user=")
  394. if text := item.Get("text").String(); text != "" {
  395. _, _ = b.WriteString(text)
  396. }
  397. firstUserCaptured = true
  398. }
  399. return true
  400. })
  401. }
  402. }
  403. if b.Len() == 0 {
  404. return ""
  405. }
  406. return contentSessionSeedPrefix + b.String()
  407. }
  408. func TestDeriveOpenAIContentSessionSeed_ResponsesAPI_InputTextTypedItem(t *testing.T) {
  409. body := []byte(`{
  410. "model": "gpt-5.4",
  411. "input": [{"type": "input_text", "text": "Hello world"}]
  412. }`)
  413. seed := deriveOpenAIContentSessionSeed(body)
  414. require.Contains(t, seed, "|first_user=")
  415. require.Contains(t, seed, "Hello world")
  416. }
  417. func TestDeriveOpenAIContentSessionSeed_ResponsesAPI_TypedMessageItem(t *testing.T) {
  418. body := []byte(`{
  419. "model": "gpt-5.4",
  420. "input": [{"type": "message", "role": "user", "content": "Hello from typed message"}]
  421. }`)
  422. seed := deriveOpenAIContentSessionSeed(body)
  423. require.Contains(t, seed, "|first_user=")
  424. require.Contains(t, seed, "Hello from typed message")
  425. }
  426. func TestDeriveOpenAIStablePrefixSessionSeed_IgnoresUserContent(t *testing.T) {
  427. first := []byte(`{
  428. "model": "grok",
  429. "instructions": "Be concise.",
  430. "tools": [{"type":"function","name":"lookup","parameters":{"type":"object"}}],
  431. "input": [{"role":"user","content":"Question A"}]
  432. }`)
  433. second := []byte(`{
  434. "model": "grok",
  435. "instructions": "Be concise.",
  436. "tools": [{"parameters":{"type":"object"},"name":"lookup","type":"function"}],
  437. "input": [{"role":"user","content":"Question B"}]
  438. }`)
  439. firstSeed := deriveOpenAIStablePrefixSessionSeed(first)
  440. secondSeed := deriveOpenAIStablePrefixSessionSeed(second)
  441. require.NotEmpty(t, firstSeed)
  442. require.Equal(t, firstSeed, secondSeed)
  443. require.NotContains(t, firstSeed, "Question A")
  444. require.NotContains(t, firstSeed, "first_user")
  445. }
  446. func TestDeriveOpenAIStablePrefixSessionSeed_IsolatesStablePrefixFields(t *testing.T) {
  447. base := []byte(`{
  448. "instructions":"Be concise.",
  449. "tools":[{"type":"function","name":"lookup"}],
  450. "input":[{"role":"system","content":"System A"},{"role":"user","content":"Question"}]
  451. }`)
  452. differentInstructions := []byte(`{
  453. "instructions":"Be detailed.",
  454. "tools":[{"type":"function","name":"lookup"}],
  455. "input":[{"role":"system","content":"System A"},{"role":"user","content":"Question"}]
  456. }`)
  457. differentTools := []byte(`{
  458. "instructions":"Be concise.",
  459. "tools":[{"type":"function","name":"search"}],
  460. "input":[{"role":"system","content":"System A"},{"role":"user","content":"Question"}]
  461. }`)
  462. differentSystem := []byte(`{
  463. "instructions":"Be concise.",
  464. "tools":[{"type":"function","name":"lookup"}],
  465. "input":[{"role":"system","content":"System B"},{"role":"user","content":"Question"}]
  466. }`)
  467. baseSeed := deriveOpenAIStablePrefixSessionSeed(base)
  468. require.NotEqual(t, baseSeed, deriveOpenAIStablePrefixSessionSeed(differentInstructions))
  469. require.NotEqual(t, baseSeed, deriveOpenAIStablePrefixSessionSeed(differentTools))
  470. require.NotEqual(t, baseSeed, deriveOpenAIStablePrefixSessionSeed(differentSystem))
  471. }
  472. func TestDeriveOpenAIStablePrefixSessionSeed_ChatSystemAndDeveloper(t *testing.T) {
  473. first := []byte(`{
  474. "messages":[
  475. {"role":"system","content":"System prompt"},
  476. {"role":"developer","content":[{"type":"text","text":"Developer prompt"}]},
  477. {"role":"user","content":"Question A"}
  478. ]
  479. }`)
  480. second := []byte(`{
  481. "messages":[
  482. {"role":"system","content":"System prompt"},
  483. {"role":"developer","content":[{"text":"Developer prompt","type":"text"}]},
  484. {"role":"user","content":"Question B"}
  485. ]
  486. }`)
  487. firstSeed := deriveOpenAIStablePrefixSessionSeed(first)
  488. require.Equal(t, firstSeed, deriveOpenAIStablePrefixSessionSeed(second))
  489. require.Contains(t, firstSeed, "System prompt")
  490. require.Contains(t, firstSeed, "Developer prompt")
  491. }
  492. func TestDeriveOpenAIStablePrefixSessionSeed_EncodesSystemAndDeveloperRoles(t *testing.T) {
  493. systemThenDeveloper := []byte(`{
  494. "messages":[
  495. {"role":"system","content":"Prompt A"},
  496. {"role":"developer","content":"Prompt B"}
  497. ]
  498. }`)
  499. developerThenSystem := []byte(`{
  500. "messages":[
  501. {"role":"developer","content":"Prompt A"},
  502. {"role":"system","content":"Prompt B"}
  503. ]
  504. }`)
  505. firstSeed := deriveOpenAIStablePrefixSessionSeed(systemThenDeveloper)
  506. secondSeed := deriveOpenAIStablePrefixSessionSeed(developerThenSystem)
  507. require.NotEqual(t, firstSeed, secondSeed)
  508. require.Contains(t, firstSeed, "|system=")
  509. require.Contains(t, firstSeed, "|developer=")
  510. }
  511. func TestDeriveOpenAIStablePrefixSessionSeed_EncodesInstructionDelimiters(t *testing.T) {
  512. instructionOnly := []byte(`{
  513. "instructions":"foo|system=\"bar\""
  514. }`)
  515. instructionAndSystem := []byte(`{
  516. "instructions":"foo",
  517. "input":[{"role":"system","content":"bar"}]
  518. }`)
  519. firstSeed := deriveOpenAIStablePrefixSessionSeed(instructionOnly)
  520. secondSeed := deriveOpenAIStablePrefixSessionSeed(instructionAndSystem)
  521. require.NotEmpty(t, firstSeed)
  522. require.NotEmpty(t, secondSeed)
  523. require.NotEqual(t, firstSeed, secondSeed)
  524. }
  525. func TestDeriveOpenAIAnchoredContentSessionSeed_RequiresMeaningfulAnchor(t *testing.T) {
  526. emptyAnchors := [][]byte{
  527. nil,
  528. []byte(`{"model":"grok"}`),
  529. []byte(`{"model":"grok","messages":[{"role":"assistant","content":"answer"}]}`),
  530. []byte(`{"model":"grok","messages":[{"role":"user","content":" "}]}`),
  531. []byte(`{"model":"grok","messages":[{"role":"user","content":[{"type":"text","text":""}]}]}`),
  532. []byte(`{"model":"grok","input":" "}`),
  533. []byte(`{"model":"grok","input":[{"type":"input_text","text":""}]}`),
  534. }
  535. for _, body := range emptyAnchors {
  536. require.Empty(t, deriveOpenAIAnchoredContentSessionSeed(body))
  537. }
  538. meaningfulAnchors := [][]byte{
  539. []byte(`{"model":"grok","messages":[{"role":"user","content":"question"}]}`),
  540. []byte(`{"model":"grok","messages":[{"role":"user","content":[{"type":"text","text":"question"}]}]}`),
  541. []byte(`{"model":"grok","input":"question"}`),
  542. []byte(`{"model":"grok","input":[{"type":"input_text","text":"question"}]}`),
  543. }
  544. for _, body := range meaningfulAnchors {
  545. require.NotEmpty(t, deriveOpenAIAnchoredContentSessionSeed(body))
  546. }
  547. }
  548. func TestDeriveOpenAIStablePrefixSessionSeed_RequiresMeaningfulPrefix(t *testing.T) {
  549. tests := [][]byte{
  550. nil,
  551. []byte(`{}`),
  552. []byte(`{"model":"grok","input":"Question A"}`),
  553. []byte(`{"model":"grok","tools":[],"input":"Question A"}`),
  554. []byte(`{"model":"grok","functions":[],"instructions":" ","messages":[{"role":"system","content":""},{"role":"user","content":"Question A"}]}`),
  555. }
  556. for _, body := range tests {
  557. require.Empty(t, deriveOpenAIStablePrefixSessionSeed(body))
  558. }
  559. }