openai_ws_forwarder_ingress_test.go 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "io"
  8. "net"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. coderws "github.com/coder/websocket"
  12. "github.com/stretchr/testify/require"
  13. "github.com/tidwall/gjson"
  14. )
  15. func TestIsOpenAIWSClientDisconnectError(t *testing.T) {
  16. t.Parallel()
  17. tests := []struct {
  18. name string
  19. err error
  20. want bool
  21. }{
  22. {name: "nil", err: nil, want: false},
  23. {name: "io_eof", err: io.EOF, want: true},
  24. {name: "net_closed", err: net.ErrClosed, want: true},
  25. {name: "context_canceled", err: context.Canceled, want: true},
  26. {name: "ws_normal_closure", err: coderws.CloseError{Code: coderws.StatusNormalClosure}, want: true},
  27. {name: "ws_going_away", err: coderws.CloseError{Code: coderws.StatusGoingAway}, want: true},
  28. {name: "ws_no_status", err: coderws.CloseError{Code: coderws.StatusNoStatusRcvd}, want: true},
  29. {name: "ws_abnormal_1006", err: coderws.CloseError{Code: coderws.StatusAbnormalClosure}, want: true},
  30. {name: "ws_policy_violation", err: coderws.CloseError{Code: coderws.StatusPolicyViolation}, want: false},
  31. {name: "wrapped_eof_message", err: errors.New("failed to get reader: failed to read frame header: EOF"), want: true},
  32. {name: "connection_reset_by_peer", err: errors.New("failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: read: connection reset by peer"), want: true},
  33. {name: "windows_connection_reset", err: errors.New("failed to get reader: failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: wsarecv: An existing connection was forcibly closed by the remote host."), want: true},
  34. {name: "broken_pipe", err: errors.New("write tcp 127.0.0.1:1234->127.0.0.1:5678: write: broken pipe"), want: true},
  35. }
  36. for _, tt := range tests {
  37. tt := tt
  38. t.Run(tt.name, func(t *testing.T) {
  39. t.Parallel()
  40. require.Equal(t, tt.want, isOpenAIWSClientDisconnectError(tt.err))
  41. })
  42. }
  43. }
  44. func TestIsOpenAIWSIngressPreviousResponseNotFound(t *testing.T) {
  45. t.Parallel()
  46. require.False(t, isOpenAIWSIngressPreviousResponseNotFound(nil))
  47. require.False(t, isOpenAIWSIngressPreviousResponseNotFound(errors.New("plain error")))
  48. require.False(t, isOpenAIWSIngressPreviousResponseNotFound(
  49. wrapOpenAIWSIngressTurnError("read_upstream", errors.New("upstream read failed"), false),
  50. ))
  51. require.False(t, isOpenAIWSIngressPreviousResponseNotFound(
  52. wrapOpenAIWSIngressTurnError(openAIWSIngressStagePreviousResponseNotFound, errors.New("previous response not found"), true),
  53. ))
  54. require.True(t, isOpenAIWSIngressPreviousResponseNotFound(
  55. wrapOpenAIWSIngressTurnError(openAIWSIngressStagePreviousResponseNotFound, errors.New("previous response not found"), false),
  56. ))
  57. }
  58. func TestOpenAIWSIngressPreviousResponseRecoveryEnabled(t *testing.T) {
  59. t.Parallel()
  60. var nilService *OpenAIGatewayService
  61. require.True(t, nilService.openAIWSIngressPreviousResponseRecoveryEnabled(), "nil service should default to enabled")
  62. svcWithNilCfg := &OpenAIGatewayService{}
  63. require.True(t, svcWithNilCfg.openAIWSIngressPreviousResponseRecoveryEnabled(), "nil config should default to enabled")
  64. svc := &OpenAIGatewayService{
  65. cfg: &config.Config{},
  66. }
  67. require.False(t, svc.openAIWSIngressPreviousResponseRecoveryEnabled(), "explicit config default should be false")
  68. svc.cfg.Gateway.OpenAIWS.IngressPreviousResponseRecoveryEnabled = true
  69. require.True(t, svc.openAIWSIngressPreviousResponseRecoveryEnabled())
  70. }
  71. func TestDropPreviousResponseIDFromRawPayload(t *testing.T) {
  72. t.Parallel()
  73. t.Run("empty_payload", func(t *testing.T) {
  74. updated, removed, err := dropPreviousResponseIDFromRawPayload(nil)
  75. require.NoError(t, err)
  76. require.False(t, removed)
  77. require.Empty(t, updated)
  78. })
  79. t.Run("payload_without_previous_response_id", func(t *testing.T) {
  80. payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
  81. updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
  82. require.NoError(t, err)
  83. require.False(t, removed)
  84. require.Equal(t, string(payload), string(updated))
  85. })
  86. t.Run("normal_delete_success", func(t *testing.T) {
  87. payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
  88. updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
  89. require.NoError(t, err)
  90. require.True(t, removed)
  91. require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
  92. })
  93. t.Run("duplicate_keys_are_removed", func(t *testing.T) {
  94. payload := []byte(`{"type":"response.create","previous_response_id":"resp_a","input":[],"previous_response_id":"resp_b"}`)
  95. updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
  96. require.NoError(t, err)
  97. require.True(t, removed)
  98. require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
  99. })
  100. t.Run("nil_delete_fn_uses_default_delete_logic", func(t *testing.T) {
  101. payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
  102. updated, removed, err := dropPreviousResponseIDFromRawPayloadWithDeleteFn(payload, nil)
  103. require.NoError(t, err)
  104. require.True(t, removed)
  105. require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
  106. })
  107. t.Run("delete_error", func(t *testing.T) {
  108. payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
  109. updated, removed, err := dropPreviousResponseIDFromRawPayloadWithDeleteFn(payload, func(_ []byte, _ string) ([]byte, error) {
  110. return nil, errors.New("delete failed")
  111. })
  112. require.Error(t, err)
  113. require.False(t, removed)
  114. require.Equal(t, string(payload), string(updated))
  115. })
  116. t.Run("malformed_json_is_still_best_effort_deleted", func(t *testing.T) {
  117. payload := []byte(`{"type":"response.create","previous_response_id":"resp_abc"`)
  118. require.True(t, gjson.GetBytes(payload, "previous_response_id").Exists())
  119. updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
  120. require.NoError(t, err)
  121. require.True(t, removed)
  122. require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
  123. })
  124. }
  125. func TestStripCodexSparkImageGenerationToolFromRawPayload(t *testing.T) {
  126. t.Run("strips_image_generation_for_spark", func(t *testing.T) {
  127. payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex-spark","tools":[{"type":"function","name":"shell"},{"type":"image_generation","output_format":"png"}]}`)
  128. updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
  129. require.NoError(t, err)
  130. require.True(t, changed)
  131. require.False(t, gjson.GetBytes(updated, `tools.#(type=="image_generation")`).Exists())
  132. require.True(t, gjson.GetBytes(updated, `tools.#(type=="function")`).Exists())
  133. })
  134. t.Run("strips_namespace_tools_for_spark", func(t *testing.T) {
  135. payload := []byte(`{
  136. "type":"response.create",
  137. "model":"gpt-5.3-codex-spark",
  138. "input":[
  139. {"type":"message","role":"user","content":"hello"},
  140. {"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}
  141. ],
  142. "tool_choice":{"type":"namespace","name":"image_gen"}
  143. }`)
  144. updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
  145. require.NoError(t, err)
  146. require.True(t, changed)
  147. require.False(t, IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.3-codex-spark", updated))
  148. require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.content").String())
  149. require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
  150. })
  151. t.Run("keeps_image_generation_for_non_spark", func(t *testing.T) {
  152. payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","tools":[{"type":"image_generation","output_format":"png"}]}`)
  153. updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex")
  154. require.NoError(t, err)
  155. require.False(t, changed)
  156. require.Equal(t, string(payload), string(updated))
  157. })
  158. t.Run("noop_when_no_image_tool", func(t *testing.T) {
  159. payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex-spark","tools":[{"type":"function","name":"shell"}]}`)
  160. updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
  161. require.NoError(t, err)
  162. require.False(t, changed)
  163. require.Equal(t, string(payload), string(updated))
  164. })
  165. }
  166. func TestStripOpenAIImageGenerationToolsFromRawPayload(t *testing.T) {
  167. t.Run("flat image tool", func(t *testing.T) {
  168. payload := []byte(`{
  169. "type":"response.create",
  170. "model":"gpt-5.4",
  171. "tools":[
  172. {"type":"function","name":"shell"},
  173. {"type":"image_generation","output_format":"png"}
  174. ],
  175. "tool_choice":{"type":"image_generation"}
  176. }`)
  177. updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
  178. require.NoError(t, err)
  179. require.True(t, changed)
  180. require.False(t, gjson.GetBytes(updated, `tools.#(type=="image_generation")`).Exists())
  181. require.True(t, gjson.GetBytes(updated, `tools.#(type=="function")`).Exists())
  182. require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
  183. })
  184. t.Run("namespace and Responses Lite tools", func(t *testing.T) {
  185. payload := []byte(`{
  186. "type":"response.create",
  187. "model":"gpt-5.5",
  188. "tools":[
  189. {"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]},
  190. {"type":"namespace","name":"code_tools","tools":[{"type":"function","name":"run"}]}
  191. ],
  192. "input":[
  193. {"type":"message","role":"user","content":"hello"},
  194. {"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}
  195. ],
  196. "tool_choice":{"type":"namespace","name":"image_gen"}
  197. }`)
  198. updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
  199. require.NoError(t, err)
  200. require.True(t, changed)
  201. require.False(t, IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.5", updated))
  202. require.True(t, gjson.GetBytes(updated, `tools.#(name=="code_tools")`).Exists())
  203. require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.content").String())
  204. require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
  205. })
  206. t.Run("non-image namespace is unchanged", func(t *testing.T) {
  207. payload := []byte(`{"type":"response.create","model":"gpt-5.5","tools":[{"type":"namespace","name":"code_tools"}]}`)
  208. updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
  209. require.NoError(t, err)
  210. require.False(t, changed)
  211. require.Equal(t, payload, updated)
  212. })
  213. }
  214. func TestAlignStoreDisabledPreviousResponseID(t *testing.T) {
  215. t.Parallel()
  216. t.Run("empty_payload", func(t *testing.T) {
  217. updated, changed, err := alignStoreDisabledPreviousResponseID(nil, "resp_target")
  218. require.NoError(t, err)
  219. require.False(t, changed)
  220. require.Empty(t, updated)
  221. })
  222. t.Run("empty_expected", func(t *testing.T) {
  223. payload := []byte(`{"type":"response.create","previous_response_id":"resp_old"}`)
  224. updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "")
  225. require.NoError(t, err)
  226. require.False(t, changed)
  227. require.Equal(t, string(payload), string(updated))
  228. })
  229. t.Run("missing_previous_response_id", func(t *testing.T) {
  230. payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
  231. updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
  232. require.NoError(t, err)
  233. require.False(t, changed)
  234. require.Equal(t, string(payload), string(updated))
  235. })
  236. t.Run("already_aligned", func(t *testing.T) {
  237. payload := []byte(`{"type":"response.create","previous_response_id":"resp_target"}`)
  238. updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
  239. require.NoError(t, err)
  240. require.False(t, changed)
  241. require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
  242. })
  243. t.Run("mismatch_rewrites_to_expected", func(t *testing.T) {
  244. payload := []byte(`{"type":"response.create","previous_response_id":"resp_old","input":[]}`)
  245. updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
  246. require.NoError(t, err)
  247. require.True(t, changed)
  248. require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
  249. })
  250. t.Run("duplicate_keys_rewrites_to_single_expected", func(t *testing.T) {
  251. payload := []byte(`{"type":"response.create","previous_response_id":"resp_old_1","input":[],"previous_response_id":"resp_old_2"}`)
  252. updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
  253. require.NoError(t, err)
  254. require.True(t, changed)
  255. require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
  256. })
  257. }
  258. func TestSetPreviousResponseIDToRawPayload(t *testing.T) {
  259. t.Parallel()
  260. t.Run("empty_payload", func(t *testing.T) {
  261. updated, err := setPreviousResponseIDToRawPayload(nil, "resp_target")
  262. require.NoError(t, err)
  263. require.Empty(t, updated)
  264. })
  265. t.Run("empty_previous_response_id", func(t *testing.T) {
  266. payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
  267. updated, err := setPreviousResponseIDToRawPayload(payload, "")
  268. require.NoError(t, err)
  269. require.Equal(t, string(payload), string(updated))
  270. })
  271. t.Run("set_previous_response_id_when_missing", func(t *testing.T) {
  272. payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
  273. updated, err := setPreviousResponseIDToRawPayload(payload, "resp_target")
  274. require.NoError(t, err)
  275. require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
  276. require.Equal(t, "gpt-5.1", gjson.GetBytes(updated, "model").String())
  277. })
  278. t.Run("overwrite_existing_previous_response_id", func(t *testing.T) {
  279. payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_old"}`)
  280. updated, err := setPreviousResponseIDToRawPayload(payload, "resp_new")
  281. require.NoError(t, err)
  282. require.Equal(t, "resp_new", gjson.GetBytes(updated, "previous_response_id").String())
  283. })
  284. }
  285. func TestShouldInferIngressFunctionCallOutputPreviousResponseID(t *testing.T) {
  286. t.Parallel()
  287. tests := []struct {
  288. name string
  289. storeDisabled bool
  290. turn int
  291. signals ToolContinuationSignals
  292. currentPreviousResponse string
  293. expectedPrevious string
  294. want bool
  295. }{
  296. {
  297. name: "infer_when_all_conditions_match",
  298. storeDisabled: true,
  299. turn: 2,
  300. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  301. expectedPrevious: "resp_1",
  302. want: true,
  303. },
  304. {
  305. name: "skip_when_store_enabled",
  306. storeDisabled: false,
  307. turn: 2,
  308. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  309. expectedPrevious: "resp_1",
  310. want: false,
  311. },
  312. {
  313. name: "skip_on_first_turn",
  314. storeDisabled: true,
  315. turn: 1,
  316. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  317. expectedPrevious: "resp_1",
  318. want: false,
  319. },
  320. {
  321. name: "skip_without_function_call_output",
  322. storeDisabled: true,
  323. turn: 2,
  324. signals: ToolContinuationSignals{},
  325. expectedPrevious: "resp_1",
  326. want: false,
  327. },
  328. {
  329. name: "skip_when_request_already_has_previous_response_id",
  330. storeDisabled: true,
  331. turn: 2,
  332. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  333. currentPreviousResponse: "resp_client",
  334. expectedPrevious: "resp_1",
  335. want: false,
  336. },
  337. {
  338. name: "skip_when_last_turn_response_id_missing",
  339. storeDisabled: true,
  340. turn: 2,
  341. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  342. expectedPrevious: "",
  343. want: false,
  344. },
  345. {
  346. name: "trim_whitespace_before_judgement",
  347. storeDisabled: true,
  348. turn: 2,
  349. signals: ToolContinuationSignals{HasFunctionCallOutput: true},
  350. expectedPrevious: " resp_2 ",
  351. want: true,
  352. },
  353. {
  354. name: "skip_when_tool_call_context_already_present",
  355. storeDisabled: true,
  356. turn: 2,
  357. signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasToolCallContext: true},
  358. expectedPrevious: "resp_2",
  359. want: false,
  360. },
  361. {
  362. name: "infer_when_only_item_reference_covers_call_ids",
  363. storeDisabled: true,
  364. turn: 2,
  365. signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasItemReferenceForAllCallIDs: true},
  366. expectedPrevious: "resp_2",
  367. want: true,
  368. },
  369. {
  370. name: "skip_when_function_call_output_missing_call_id",
  371. storeDisabled: true,
  372. turn: 2,
  373. signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasFunctionCallOutputMissingCallID: true},
  374. expectedPrevious: "resp_2",
  375. want: false,
  376. },
  377. }
  378. for _, tt := range tests {
  379. tt := tt
  380. t.Run(tt.name, func(t *testing.T) {
  381. t.Parallel()
  382. got := shouldInferIngressFunctionCallOutputPreviousResponseID(
  383. tt.storeDisabled,
  384. tt.turn,
  385. tt.signals,
  386. tt.currentPreviousResponse,
  387. tt.expectedPrevious,
  388. )
  389. require.Equal(t, tt.want, got)
  390. })
  391. }
  392. }
  393. func TestOpenAIWSInputIsPrefixExtended(t *testing.T) {
  394. t.Parallel()
  395. tests := []struct {
  396. name string
  397. previous []byte
  398. current []byte
  399. want bool
  400. expectErr bool
  401. }{
  402. {
  403. name: "both_missing_input",
  404. previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
  405. current: []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_1"}`),
  406. want: true,
  407. },
  408. {
  409. name: "previous_missing_current_empty_array",
  410. previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
  411. current: []byte(`{"type":"response.create","model":"gpt-5.1","input":[]}`),
  412. want: true,
  413. },
  414. {
  415. name: "previous_missing_current_non_empty_array",
  416. previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
  417. current: []byte(`{"type":"response.create","model":"gpt-5.1","input":[{"type":"input_text","text":"hello"}]}`),
  418. want: false,
  419. },
  420. {
  421. name: "array_prefix_match",
  422. previous: []byte(`{"input":[{"type":"input_text","text":"hello"}]}`),
  423. current: []byte(`{"input":[{"text":"hello","type":"input_text"},{"type":"input_text","text":"world"}]}`),
  424. want: true,
  425. },
  426. {
  427. name: "array_prefix_mismatch",
  428. previous: []byte(`{"input":[{"type":"input_text","text":"hello"}]}`),
  429. current: []byte(`{"input":[{"type":"input_text","text":"different"}]}`),
  430. want: false,
  431. },
  432. {
  433. name: "current_shorter_than_previous",
  434. previous: []byte(`{"input":[{"type":"input_text","text":"a"},{"type":"input_text","text":"b"}]}`),
  435. current: []byte(`{"input":[{"type":"input_text","text":"a"}]}`),
  436. want: false,
  437. },
  438. {
  439. name: "previous_has_input_current_missing",
  440. previous: []byte(`{"input":[{"type":"input_text","text":"a"}]}`),
  441. current: []byte(`{"model":"gpt-5.1"}`),
  442. want: false,
  443. },
  444. {
  445. name: "input_string_treated_as_single_item",
  446. previous: []byte(`{"input":"hello"}`),
  447. current: []byte(`{"input":"hello"}`),
  448. want: true,
  449. },
  450. {
  451. name: "current_invalid_input_json",
  452. previous: []byte(`{"input":[]}`),
  453. current: []byte(`{"input":[}`),
  454. expectErr: true,
  455. },
  456. {
  457. name: "invalid_input_json",
  458. previous: []byte(`{"input":[}`),
  459. current: []byte(`{"input":[]}`),
  460. expectErr: true,
  461. },
  462. }
  463. for _, tt := range tests {
  464. tt := tt
  465. t.Run(tt.name, func(t *testing.T) {
  466. t.Parallel()
  467. got, err := openAIWSInputIsPrefixExtended(tt.previous, tt.current)
  468. if tt.expectErr {
  469. require.Error(t, err)
  470. return
  471. }
  472. require.NoError(t, err)
  473. require.Equal(t, tt.want, got)
  474. })
  475. }
  476. }
  477. func TestNormalizeOpenAIWSJSONForCompare(t *testing.T) {
  478. t.Parallel()
  479. normalized, err := normalizeOpenAIWSJSONForCompare([]byte(`{"b":2,"a":1}`))
  480. require.NoError(t, err)
  481. require.Equal(t, `{"a":1,"b":2}`, string(normalized))
  482. _, err = normalizeOpenAIWSJSONForCompare([]byte(" "))
  483. require.Error(t, err)
  484. _, err = normalizeOpenAIWSJSONForCompare([]byte(`{"a":`))
  485. require.Error(t, err)
  486. }
  487. func TestNormalizeOpenAIWSJSONForCompareOrRaw(t *testing.T) {
  488. t.Parallel()
  489. require.Equal(t, `{"a":1,"b":2}`, string(normalizeOpenAIWSJSONForCompareOrRaw([]byte(`{"b":2,"a":1}`))))
  490. require.Equal(t, `{"a":`, string(normalizeOpenAIWSJSONForCompareOrRaw([]byte(`{"a":`))))
  491. }
  492. func TestNormalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(t *testing.T) {
  493. t.Parallel()
  494. normalized, err := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(
  495. []byte(`{"model":"gpt-5.1","input":[1],"previous_response_id":"resp_x","client_metadata":{"request_start_ms":"1"},"stream_options":{"include_usage":true},"generate":false,"metadata":{"b":2,"a":1}}`),
  496. )
  497. require.NoError(t, err)
  498. require.False(t, gjson.GetBytes(normalized, "input").Exists())
  499. require.False(t, gjson.GetBytes(normalized, "previous_response_id").Exists())
  500. require.False(t, gjson.GetBytes(normalized, "client_metadata").Exists())
  501. require.False(t, gjson.GetBytes(normalized, "stream_options").Exists())
  502. require.False(t, gjson.GetBytes(normalized, "generate").Exists())
  503. require.Equal(t, float64(1), gjson.GetBytes(normalized, "metadata.a").Float())
  504. normalized, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(
  505. []byte(`{"model":"gpt-5.1","generate":true}`),
  506. )
  507. require.NoError(t, err)
  508. require.True(t, gjson.GetBytes(normalized, "generate").Bool())
  509. _, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(nil)
  510. require.Error(t, err)
  511. _, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID([]byte(`[]`))
  512. require.Error(t, err)
  513. }
  514. func TestOpenAIWSExtractNormalizedInputSequence(t *testing.T) {
  515. t.Parallel()
  516. t.Run("empty_payload", func(t *testing.T) {
  517. items, exists, err := openAIWSExtractNormalizedInputSequence(nil)
  518. require.NoError(t, err)
  519. require.False(t, exists)
  520. require.Nil(t, items)
  521. })
  522. t.Run("input_missing", func(t *testing.T) {
  523. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"type":"response.create"}`))
  524. require.NoError(t, err)
  525. require.False(t, exists)
  526. require.Nil(t, items)
  527. })
  528. t.Run("input_array", func(t *testing.T) {
  529. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":[{"type":"input_text","text":"hello"}]}`))
  530. require.NoError(t, err)
  531. require.True(t, exists)
  532. require.Len(t, items, 1)
  533. })
  534. t.Run("input_object", func(t *testing.T) {
  535. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":{"type":"input_text","text":"hello"}}`))
  536. require.NoError(t, err)
  537. require.True(t, exists)
  538. require.Len(t, items, 1)
  539. })
  540. t.Run("input_string", func(t *testing.T) {
  541. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":"hello"}`))
  542. require.NoError(t, err)
  543. require.True(t, exists)
  544. require.Len(t, items, 1)
  545. require.Equal(t, `"hello"`, string(items[0]))
  546. })
  547. t.Run("input_number", func(t *testing.T) {
  548. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":42}`))
  549. require.NoError(t, err)
  550. require.True(t, exists)
  551. require.Len(t, items, 1)
  552. require.Equal(t, "42", string(items[0]))
  553. })
  554. t.Run("input_bool", func(t *testing.T) {
  555. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":true}`))
  556. require.NoError(t, err)
  557. require.True(t, exists)
  558. require.Len(t, items, 1)
  559. require.Equal(t, "true", string(items[0]))
  560. })
  561. t.Run("input_null", func(t *testing.T) {
  562. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":null}`))
  563. require.NoError(t, err)
  564. require.True(t, exists)
  565. require.Len(t, items, 1)
  566. require.Equal(t, "null", string(items[0]))
  567. })
  568. t.Run("input_invalid_array_json", func(t *testing.T) {
  569. items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":[}`))
  570. require.Error(t, err)
  571. require.True(t, exists)
  572. require.Nil(t, items)
  573. })
  574. }
  575. func TestShouldKeepIngressPreviousResponseID(t *testing.T) {
  576. t.Parallel()
  577. previousPayload := []byte(`{
  578. "type":"response.create",
  579. "model":"gpt-5.1",
  580. "store":false,
  581. "tools":[{"type":"function","name":"tool_a"}],
  582. "input":[{"type":"input_text","text":"hello"}]
  583. }`)
  584. currentStrictPayload := []byte(`{
  585. "type":"response.create",
  586. "model":"gpt-5.1",
  587. "store":false,
  588. "tools":[{"name":"tool_a","type":"function"}],
  589. "previous_response_id":"resp_turn_1",
  590. "input":[{"text":"hello","type":"input_text"},{"type":"input_text","text":"world"}]
  591. }`)
  592. t.Run("strict_incremental_keep", func(t *testing.T) {
  593. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "resp_turn_1", false)
  594. require.NoError(t, err)
  595. require.True(t, keep)
  596. require.Equal(t, "strict_incremental_ok", reason)
  597. })
  598. t.Run("codex_prewarm_to_business_keep", func(t *testing.T) {
  599. prewarmPayload := []byte(`{
  600. "type":"response.create",
  601. "model":"gpt-5.1",
  602. "store":false,
  603. "generate":false,
  604. "client_metadata":{"x-codex-ws-stream-request-start-ms":"100"},
  605. "stream_options":{"include_usage":true},
  606. "input":[{"type":"input_text","text":"hello"}]
  607. }`)
  608. businessPayload := []byte(`{
  609. "type":"response.create",
  610. "model":"gpt-5.1",
  611. "store":false,
  612. "client_metadata":{"x-codex-ws-stream-request-start-ms":"200"},
  613. "previous_response_id":"resp_prewarm",
  614. "input":[{"type":"input_text","text":"hello"}]
  615. }`)
  616. keep, reason, err := shouldKeepIngressPreviousResponseID(
  617. prewarmPayload,
  618. businessPayload,
  619. "resp_prewarm",
  620. false,
  621. )
  622. require.NoError(t, err)
  623. require.True(t, keep)
  624. require.Equal(t, "strict_incremental_ok", reason)
  625. })
  626. t.Run("missing_previous_response_id", func(t *testing.T) {
  627. payload := []byte(`{"type":"response.create","model":"gpt-5.1","input":[]}`)
  628. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
  629. require.NoError(t, err)
  630. require.False(t, keep)
  631. require.Equal(t, "missing_previous_response_id", reason)
  632. })
  633. t.Run("missing_last_turn_response_id", func(t *testing.T) {
  634. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "", false)
  635. require.NoError(t, err)
  636. require.False(t, keep)
  637. require.Equal(t, "missing_last_turn_response_id", reason)
  638. })
  639. t.Run("previous_response_id_mismatch", func(t *testing.T) {
  640. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "resp_turn_other", false)
  641. require.NoError(t, err)
  642. require.False(t, keep)
  643. require.Equal(t, "previous_response_id_mismatch", reason)
  644. })
  645. t.Run("missing_previous_turn_payload", func(t *testing.T) {
  646. keep, reason, err := shouldKeepIngressPreviousResponseID(nil, currentStrictPayload, "resp_turn_1", false)
  647. require.NoError(t, err)
  648. require.False(t, keep)
  649. require.Equal(t, "missing_previous_turn_payload", reason)
  650. })
  651. t.Run("non_input_changed", func(t *testing.T) {
  652. payload := []byte(`{
  653. "type":"response.create",
  654. "model":"gpt-5.1-mini",
  655. "store":false,
  656. "tools":[{"type":"function","name":"tool_a"}],
  657. "previous_response_id":"resp_turn_1",
  658. "input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]
  659. }`)
  660. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
  661. require.NoError(t, err)
  662. require.False(t, keep)
  663. require.Equal(t, "non_input_changed", reason)
  664. })
  665. t.Run("delta_input_keeps_previous_response_id", func(t *testing.T) {
  666. payload := []byte(`{
  667. "type":"response.create",
  668. "model":"gpt-5.1",
  669. "store":false,
  670. "tools":[{"type":"function","name":"tool_a"}],
  671. "previous_response_id":"resp_turn_1",
  672. "input":[{"type":"input_text","text":"different"}]
  673. }`)
  674. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
  675. require.NoError(t, err)
  676. require.True(t, keep)
  677. require.Equal(t, "strict_incremental_ok", reason)
  678. })
  679. t.Run("function_call_output_keeps_previous_response_id", func(t *testing.T) {
  680. payload := []byte(`{
  681. "type":"response.create",
  682. "model":"gpt-5.1",
  683. "store":false,
  684. "previous_response_id":"resp_external",
  685. "input":[{"type":"function_call_output","call_id":"call_1","output":"ok"}]
  686. }`)
  687. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", true)
  688. require.NoError(t, err)
  689. require.True(t, keep)
  690. require.Equal(t, "has_function_call_output", reason)
  691. })
  692. t.Run("non_input_compare_error", func(t *testing.T) {
  693. keep, reason, err := shouldKeepIngressPreviousResponseID([]byte(`[]`), currentStrictPayload, "resp_turn_1", false)
  694. require.Error(t, err)
  695. require.False(t, keep)
  696. require.Equal(t, "non_input_compare_error", reason)
  697. })
  698. t.Run("current_payload_compare_error", func(t *testing.T) {
  699. keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, []byte(`{"previous_response_id":"resp_turn_1","input":[}`), "resp_turn_1", false)
  700. require.Error(t, err)
  701. require.False(t, keep)
  702. require.Equal(t, "non_input_compare_error", reason)
  703. })
  704. }
  705. func TestBuildOpenAIWSReplayInputSequence(t *testing.T) {
  706. t.Parallel()
  707. lastFull := []json.RawMessage{
  708. json.RawMessage(`{"type":"input_text","text":"hello"}`),
  709. }
  710. t.Run("no_previous_response_id_use_current", func(t *testing.T) {
  711. items, exists, err := buildOpenAIWSReplayInputSequence(
  712. lastFull,
  713. true,
  714. []byte(`{"input":[{"type":"input_text","text":"new"}]}`),
  715. false,
  716. )
  717. require.NoError(t, err)
  718. require.True(t, exists)
  719. require.Len(t, items, 1)
  720. require.Equal(t, "new", gjson.GetBytes(items[0], "text").String())
  721. })
  722. t.Run("no_previous_response_id_custom_tool_history_does_not_accumulate", func(t *testing.T) {
  723. previousFull := []json.RawMessage{
  724. json.RawMessage(`{"type":"input_text","text":"stale"}`),
  725. json.RawMessage(`{"type":"custom_tool_call","id":"stale_item","call_id":"stale_call","name":"exec","input":"stale"}`),
  726. }
  727. currentPayload := []byte(`{"input":[
  728. {"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"},
  729. {"type":"custom_tool_call_output","call_id":"call_1","output":"/tmp"},
  730. {"type":"input_text","text":"continue"}
  731. ]}`)
  732. for range 3 {
  733. items, exists, err := buildOpenAIWSReplayInputSequence(
  734. previousFull,
  735. true,
  736. currentPayload,
  737. false,
  738. )
  739. require.NoError(t, err)
  740. require.True(t, exists)
  741. require.Len(t, items, 3)
  742. require.Equal(t, "custom_tool_call", gjson.GetBytes(items[0], "type").String())
  743. require.Equal(t, "call_1", gjson.GetBytes(items[0], "call_id").String())
  744. require.Equal(t, "custom_tool_call_output", gjson.GetBytes(items[1], "type").String())
  745. require.Equal(t, "call_1", gjson.GetBytes(items[1], "call_id").String())
  746. previousFull = append(items, json.RawMessage(`{"type":"custom_tool_call","id":"replayed_item","call_id":"replayed_call","name":"exec","input":"ignored"}`))
  747. }
  748. })
  749. t.Run("previous_response_id_delta_append", func(t *testing.T) {
  750. items, exists, err := buildOpenAIWSReplayInputSequence(
  751. lastFull,
  752. true,
  753. []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"world"}]}`),
  754. true,
  755. )
  756. require.NoError(t, err)
  757. require.True(t, exists)
  758. require.Len(t, items, 2)
  759. require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
  760. require.Equal(t, "world", gjson.GetBytes(items[1], "text").String())
  761. })
  762. t.Run("previous_response_id_filters_orphan_historical_custom_tool_call", func(t *testing.T) {
  763. previousFull := []json.RawMessage{
  764. json.RawMessage(`{"type":"input_text","text":"hello"}`),
  765. json.RawMessage(`{"type":"custom_tool_call","id":"item_orphan","call_id":"call_orphan","name":"exec","input":"pwd"}`),
  766. }
  767. items, exists, err := buildOpenAIWSReplayInputSequence(
  768. previousFull,
  769. true,
  770. []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
  771. true,
  772. )
  773. require.NoError(t, err)
  774. require.True(t, exists)
  775. require.Len(t, items, 2)
  776. require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
  777. require.Equal(t, "user", gjson.GetBytes(items[1], "role").String())
  778. })
  779. t.Run("previous_response_id_preserves_paired_historical_function_call", func(t *testing.T) {
  780. previousFull := []json.RawMessage{
  781. json.RawMessage(`{"type":"function_call","id":"item_1","call_id":"call_1","name":"lookup","arguments":"{}"}`),
  782. json.RawMessage(`{"type":"function_call_output","call_id":"call_1","output":"ok"}`),
  783. }
  784. items, exists, err := buildOpenAIWSReplayInputSequence(
  785. previousFull,
  786. true,
  787. []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
  788. true,
  789. )
  790. require.NoError(t, err)
  791. require.True(t, exists)
  792. require.Len(t, items, 3)
  793. require.Equal(t, "function_call", gjson.GetBytes(items[0], "type").String())
  794. require.Equal(t, "function_call_output", gjson.GetBytes(items[1], "type").String())
  795. })
  796. t.Run("previous_response_id_preserves_paired_historical_custom_tool_call", func(t *testing.T) {
  797. previousFull := []json.RawMessage{
  798. json.RawMessage(`{"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"}`),
  799. json.RawMessage(`{"type":"custom_tool_call_output","call_id":"call_1","output":"/tmp"}`),
  800. }
  801. items, exists, err := buildOpenAIWSReplayInputSequence(
  802. previousFull,
  803. true,
  804. []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
  805. true,
  806. )
  807. require.NoError(t, err)
  808. require.True(t, exists)
  809. require.Len(t, items, 3)
  810. require.Equal(t, "custom_tool_call", gjson.GetBytes(items[0], "type").String())
  811. require.Equal(t, "custom_tool_call_output", gjson.GetBytes(items[1], "type").String())
  812. })
  813. t.Run("item_reference_does_not_complete_historical_call", func(t *testing.T) {
  814. previousFull := []json.RawMessage{
  815. json.RawMessage(`{"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"}`),
  816. }
  817. items, exists, err := buildOpenAIWSReplayInputSequence(
  818. previousFull,
  819. true,
  820. []byte(`{"previous_response_id":"resp_1","input":[{"type":"item_reference","id":"call_1"},{"role":"user","content":"continue"}]}`),
  821. true,
  822. )
  823. require.NoError(t, err)
  824. require.True(t, exists)
  825. require.Len(t, items, 2)
  826. require.Equal(t, "item_reference", gjson.GetBytes(items[0], "type").String())
  827. require.Equal(t, "user", gjson.GetBytes(items[1], "role").String())
  828. })
  829. t.Run("previous_response_id_preserves_current_orphan_custom_tool_call", func(t *testing.T) {
  830. items, exists, err := buildOpenAIWSReplayInputSequence(
  831. lastFull,
  832. true,
  833. []byte(`{"previous_response_id":"resp_1","input":[{"type":"custom_tool_call","id":"item_live","call_id":"call_live","name":"exec","input":"pwd"}]}`),
  834. true,
  835. )
  836. require.NoError(t, err)
  837. require.True(t, exists)
  838. require.Len(t, items, 2)
  839. require.Equal(t, "custom_tool_call", gjson.GetBytes(items[1], "type").String())
  840. require.Equal(t, "call_live", gjson.GetBytes(items[1], "call_id").String())
  841. })
  842. t.Run("previous_response_id_full_input_replace", func(t *testing.T) {
  843. items, exists, err := buildOpenAIWSReplayInputSequence(
  844. lastFull,
  845. true,
  846. []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`),
  847. true,
  848. )
  849. require.NoError(t, err)
  850. require.True(t, exists)
  851. require.Len(t, items, 2)
  852. require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
  853. require.Equal(t, "world", gjson.GetBytes(items[1], "text").String())
  854. })
  855. }
  856. func TestOpenAIWSRawPayloadHasToolCallOutput(t *testing.T) {
  857. t.Parallel()
  858. for _, typ := range []string{
  859. "function_call_output",
  860. "tool_search_output",
  861. "custom_tool_call_output",
  862. "mcp_tool_call_output",
  863. } {
  864. typ := typ
  865. t.Run(typ, func(t *testing.T) {
  866. t.Parallel()
  867. payload := []byte(`{"input":[{"type":"` + typ + `","call_id":"call_1","output":"ok"}]}`)
  868. require.True(t, openAIWSRawPayloadHasToolCallOutput(payload))
  869. })
  870. }
  871. t.Run("object_input", func(t *testing.T) {
  872. t.Parallel()
  873. payload := []byte(`{"input":{"type":"tool_search_output","call_id":"call_1","output":"ok"}}`)
  874. require.True(t, openAIWSRawPayloadHasToolCallOutput(payload))
  875. })
  876. t.Run("non_tool_output", func(t *testing.T) {
  877. t.Parallel()
  878. payload := []byte(`{"input":[{"type":"input_text","text":"hello"}]}`)
  879. require.False(t, openAIWSRawPayloadHasToolCallOutput(payload))
  880. })
  881. }
  882. func TestSetOpenAIWSPayloadInputSequence(t *testing.T) {
  883. t.Parallel()
  884. t.Run("set_items", func(t *testing.T) {
  885. original := []byte(`{"type":"response.create","previous_response_id":"resp_1"}`)
  886. items := []json.RawMessage{
  887. json.RawMessage(`{"type":"input_text","text":"hello"}`),
  888. json.RawMessage(`{"type":"input_text","text":"world"}`),
  889. }
  890. updated, err := setOpenAIWSPayloadInputSequence(original, items, true)
  891. require.NoError(t, err)
  892. require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.text").String())
  893. require.Equal(t, "world", gjson.GetBytes(updated, "input.1.text").String())
  894. })
  895. t.Run("preserve_empty_array_not_null", func(t *testing.T) {
  896. original := []byte(`{"type":"response.create","previous_response_id":"resp_1"}`)
  897. updated, err := setOpenAIWSPayloadInputSequence(original, nil, true)
  898. require.NoError(t, err)
  899. require.True(t, gjson.GetBytes(updated, "input").IsArray())
  900. require.Len(t, gjson.GetBytes(updated, "input").Array(), 0)
  901. require.False(t, gjson.GetBytes(updated, "input").Type == gjson.Null)
  902. })
  903. }
  904. func TestCombineOpenAIWSReplayItems(t *testing.T) {
  905. t.Parallel()
  906. t.Run("empty_delta_returns_history", func(t *testing.T) {
  907. history := []json.RawMessage{json.RawMessage(`{"a":1}`)}
  908. require.Nil(t, combineOpenAIWSReplayItems(nil, nil))
  909. combined := combineOpenAIWSReplayItems(history, nil)
  910. require.Len(t, combined, 1)
  911. })
  912. t.Run("new_header_shares_bodies", func(t *testing.T) {
  913. history := []json.RawMessage{json.RawMessage(`{"a":1}`)}
  914. delta := []json.RawMessage{json.RawMessage(`{"b":2}`)}
  915. combined := combineOpenAIWSReplayItems(history, delta)
  916. require.Len(t, combined, 2)
  917. // 头数组必须是新建的:对 combined 追加不影响 history。
  918. require.NotSame(t, &history[0], &combined[0])
  919. // 正文共享:不发生字节级深拷贝。
  920. require.Same(t, &history[0][0], &combined[0][0])
  921. require.Same(t, &delta[0][0], &combined[1][0])
  922. })
  923. }
  924. func TestOpenAIWSReplaySequenceSharesBodies(t *testing.T) {
  925. t.Parallel()
  926. t.Run("extract_shares_payload_backing_array", func(t *testing.T) {
  927. payload := []byte(`{"input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`)
  928. items, exists, err := openAIWSExtractNormalizedInputSequence(payload)
  929. require.NoError(t, err)
  930. require.True(t, exists)
  931. require.Len(t, items, 2)
  932. for _, item := range items {
  933. start := bytes.Index(payload, []byte(item))
  934. require.GreaterOrEqual(t, start, 0)
  935. require.Same(t, &payload[start], &item[0], "extract 应零拷贝共享 payload 底层数组")
  936. }
  937. })
  938. t.Run("build_transfers_current_items_ownership", func(t *testing.T) {
  939. payload := []byte(`{"input":[{"type":"input_text","text":"hello"}]}`)
  940. items, exists, err := buildOpenAIWSReplayInputSequence(nil, false, payload, false)
  941. require.NoError(t, err)
  942. require.True(t, exists)
  943. require.Len(t, items, 1)
  944. start := bytes.Index(payload, []byte(items[0]))
  945. require.GreaterOrEqual(t, start, 0)
  946. require.Same(t, &payload[start], &items[0][0])
  947. })
  948. t.Run("build_merge_shares_history_bodies", func(t *testing.T) {
  949. history := []json.RawMessage{json.RawMessage(`{"type":"input_text","text":"hello"}`)}
  950. items, exists, err := buildOpenAIWSReplayInputSequence(
  951. history,
  952. true,
  953. []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"world"}]}`),
  954. true,
  955. )
  956. require.NoError(t, err)
  957. require.True(t, exists)
  958. require.Len(t, items, 2)
  959. require.Same(t, &history[0][0], &items[0][0], "历史正文应共享而非深拷贝")
  960. })
  961. t.Run("build_prefix_hit_transfers_current_items", func(t *testing.T) {
  962. history := []json.RawMessage{json.RawMessage(`{"type":"input_text","text":"hello"}`)}
  963. payload := []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`)
  964. items, exists, err := buildOpenAIWSReplayInputSequence(history, true, payload, true)
  965. require.NoError(t, err)
  966. require.True(t, exists)
  967. require.Len(t, items, 2)
  968. start := bytes.Index(payload, []byte(items[1]))
  969. require.GreaterOrEqual(t, start, 0)
  970. require.Same(t, &payload[start], &items[1][0], "prefix 命中应转移当前 items 所有权并共享 payload 底层数组")
  971. })
  972. }