openai_passthrough_normalization_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. package service
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/require"
  5. "github.com/tidwall/gjson"
  6. )
  7. func TestNormalizeOpenAIPassthroughOAuthBody_RemovesUnsupportedUser(t *testing.T) {
  8. body := []byte(`{"model":"gpt-5.4","input":"hello","user":"user_123","metadata":{"user_id":"user_123"},"prompt_cache_retention":"24h","safety_identifier":"sid","stream_options":{"include_usage":true}}`)
  9. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  10. require.NoError(t, err)
  11. require.True(t, changed)
  12. for _, field := range openAIChatGPTInternalUnsupportedFields {
  13. require.False(t, gjson.GetBytes(normalized, field).Exists(), "%s should be stripped", field)
  14. }
  15. require.True(t, gjson.GetBytes(normalized, "stream").Bool())
  16. require.False(t, gjson.GetBytes(normalized, "store").Bool())
  17. }
  18. func TestNormalizeOpenAIPassthroughOAuthBody_NormalizesCompatibilityFields(t *testing.T) {
  19. body := []byte(`{"model":"gpt-5.5","prompt":"hello","commands":["unsupported"],"truncation":"auto","stop_sequences":["END"],"chat_template_kwargs":{"enable_thinking":true}}`)
  20. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  21. require.NoError(t, err)
  22. require.True(t, changed)
  23. require.Equal(t, "hello", gjson.GetBytes(normalized, "input.0.content").String())
  24. for _, field := range []string{"prompt", "commands", "truncation", "stop_sequences", "chat_template_kwargs"} {
  25. require.False(t, gjson.GetBytes(normalized, field).Exists(), field)
  26. }
  27. }
  28. func TestNormalizeOpenAIPassthroughOAuthBody_NormalizesReasoningMode(t *testing.T) {
  29. body := []byte(`{"model":"gpt-5.6-sol","input":"hello","reasoning":{"mode":"pro"}}`)
  30. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  31. require.NoError(t, err)
  32. require.True(t, changed)
  33. require.Equal(t, "max", gjson.GetBytes(normalized, "reasoning.effort").String())
  34. require.False(t, gjson.GetBytes(normalized, "reasoning.mode").Exists())
  35. }
  36. func TestNormalizeOpenAIOAuthResponsesCompatibilityBody_PreservesExplicitInput(t *testing.T) {
  37. body := []byte(`{"model":"gpt-5.5","input":"explicit","prompt":"legacy"}`)
  38. normalized, changed, err := normalizeOpenAIOAuthResponsesCompatibilityBody(body)
  39. require.NoError(t, err)
  40. require.True(t, changed)
  41. require.Equal(t, "explicit", gjson.GetBytes(normalized, "input").String())
  42. require.False(t, gjson.GetBytes(normalized, "prompt").Exists())
  43. }
  44. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_OnlyStripsOAuthFields(t *testing.T) {
  45. body := []byte(`{"type":"response.create","prompt":"hello","commands":{},"truncation":"auto","stop_sequences":["END"],"chat_template_kwargs":{"enable_thinking":true}}`)
  46. oauthBody, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, false)
  47. require.NoError(t, err)
  48. require.True(t, changed)
  49. require.Equal(t, "hello", gjson.GetBytes(oauthBody, "input").String())
  50. for _, field := range []string{"prompt", "commands", "truncation", "stop_sequences", "chat_template_kwargs"} {
  51. require.False(t, gjson.GetBytes(oauthBody, field).Exists(), field)
  52. }
  53. apiKeyBody, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, false)
  54. require.NoError(t, err)
  55. require.False(t, changed)
  56. require.JSONEq(t, string(body), string(apiKeyBody))
  57. }
  58. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_SanitizesNativeItemIDs(t *testing.T) {
  59. body := []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[` +
  60. `{"type":"custom_tool_call","id":"fc_wrong_custom","call_id":"call_custom_1","name":"apply_patch","input":"patch"},` +
  61. `{"type":"custom_tool_call","id":"ctc_valid","call_id":"call_custom_2","name":"apply_patch","input":"patch"},` +
  62. `{"type":"tool_search_call","id":"fc_wrong_search","call_id":"call_search_1","arguments":{"query":"docs"}},` +
  63. `{"type":"tool_search_call","id":"tsc_valid","call_id":"call_search_2","arguments":{"query":"docs"}}]}`)
  64. for _, oauth := range []bool{false, true} {
  65. accountType := AccountTypeAPIKey
  66. if oauth {
  67. accountType = AccountTypeOAuth
  68. }
  69. normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: accountType}, false)
  70. require.NoError(t, err)
  71. require.True(t, changed)
  72. require.Equal(t, "response.create", gjson.GetBytes(normalized, "type").String())
  73. require.False(t, gjson.GetBytes(normalized, "input.0.id").Exists())
  74. require.Equal(t, "ctc_valid", gjson.GetBytes(normalized, "input.1.id").String())
  75. require.False(t, gjson.GetBytes(normalized, "input.2.id").Exists())
  76. require.Equal(t, "tsc_valid", gjson.GetBytes(normalized, "input.3.id").String())
  77. // Native Responses call_id values are correlation keys, not item IDs.
  78. require.Equal(t, "call_custom_1", gjson.GetBytes(normalized, "input.0.call_id").String())
  79. require.Equal(t, "call_search_1", gjson.GetBytes(normalized, "input.2.call_id").String())
  80. }
  81. }
  82. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_APIKeyStoreFalseReplay(t *testing.T) {
  83. body := []byte(`{"type":"response.create","store":false,"parallel_tool_calls":true,"input":[` +
  84. `{"type":"reasoning","id":"rs_drop","summary":[]},` +
  85. `{"type":"reasoning","id":"rs_keep","call_id":"remove","encrypted_content":"cipher"},` +
  86. `{"type":"message","content":"continue"}` +
  87. `]}`)
  88. normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{
  89. Platform: PlatformOpenAI,
  90. Type: AccountTypeAPIKey,
  91. }, false)
  92. require.NoError(t, err)
  93. require.True(t, changed)
  94. require.False(t, gjson.GetBytes(normalized, "parallel_tool_calls").Exists())
  95. require.Equal(t, int64(2), gjson.GetBytes(normalized, "input.#").Int())
  96. require.False(t, gjson.GetBytes(normalized, "input.0.id").Exists())
  97. require.False(t, gjson.GetBytes(normalized, "input.0.call_id").Exists())
  98. require.True(t, gjson.GetBytes(normalized, "input.0.summary").IsArray())
  99. require.Equal(t, "message", gjson.GetBytes(normalized, "input.1.type").String())
  100. }
  101. func TestNormalizeOpenAIResponsesReasoningMode(t *testing.T) {
  102. tests := []struct {
  103. name string
  104. body string
  105. wantEffort string
  106. }{
  107. {name: "pro maps to max", body: `{"reasoning":{"mode":"pro"}}`, wantEffort: "max"},
  108. {name: "explicit effort wins", body: `{"reasoning":{"mode":"pro","effort":"high"}}`, wantEffort: "high"},
  109. {name: "other mode only removed", body: `{"reasoning":{"mode":"standard"}}`},
  110. }
  111. for _, tt := range tests {
  112. t.Run(tt.name, func(t *testing.T) {
  113. normalized, changed, err := normalizeOpenAIResponsesReasoningMode([]byte(tt.body))
  114. require.NoError(t, err)
  115. require.True(t, changed)
  116. require.False(t, gjson.GetBytes(normalized, "reasoning.mode").Exists())
  117. require.Equal(t, tt.wantEffort, gjson.GetBytes(normalized, "reasoning.effort").String())
  118. })
  119. }
  120. }
  121. func TestNormalizeOpenAIResponsesReasoningMode_AstraPreservesBody(t *testing.T) {
  122. // GPT-6 Astra 保留官方 reasoning.mode 与 reasoning.effort 各自原样:
  123. // 不删 mode、缺失 effort 也不补 max。非 Astra 的旧兼容行为不受影响。
  124. tests := []struct {
  125. name string
  126. body string
  127. }{
  128. {name: "pro + max preserved", body: `{"model":"gpt-6-astra","reasoning":{"mode":"pro","effort":"max"}}`},
  129. {name: "standard + max preserved", body: `{"model":"gpt-6-astra","reasoning":{"mode":"standard","effort":"max"}}`},
  130. {name: "missing mode + max preserved", body: `{"model":"gpt-6-astra","reasoning":{"effort":"max"}}`},
  131. {name: "pro + explicit high preserved", body: `{"model":"gpt-6-astra","reasoning":{"mode":"pro","effort":"high"}}`},
  132. {name: "pro without effort does not inject max", body: `{"model":"gpt-6-astra","reasoning":{"mode":"pro"}}`},
  133. }
  134. for _, tt := range tests {
  135. t.Run(tt.name, func(t *testing.T) {
  136. normalized, changed, err := normalizeOpenAIResponsesReasoningMode([]byte(tt.body))
  137. require.NoError(t, err)
  138. require.False(t, changed)
  139. require.JSONEq(t, tt.body, string(normalized))
  140. })
  141. }
  142. }
  143. func TestNormalizeOpenAIResponsesReasoningMode_NonAstraKeepsLegacyBehavior(t *testing.T) {
  144. // 确保 guard 只放过 Astra,非 Astra model 字段不影响既有 strip/pro->max 语义。
  145. tests := []struct {
  146. name string
  147. body string
  148. wantEffort string
  149. }{
  150. {name: "gpt-5.6-sol pro maps to max", body: `{"model":"gpt-5.6-sol","reasoning":{"mode":"pro"}}`, wantEffort: "max"},
  151. {name: "gpt-5.4 explicit effort wins", body: `{"model":"gpt-5.4","reasoning":{"mode":"pro","effort":"high"}}`, wantEffort: "high"},
  152. }
  153. for _, tt := range tests {
  154. t.Run(tt.name, func(t *testing.T) {
  155. normalized, changed, err := normalizeOpenAIResponsesReasoningMode([]byte(tt.body))
  156. require.NoError(t, err)
  157. require.True(t, changed)
  158. require.False(t, gjson.GetBytes(normalized, "reasoning.mode").Exists())
  159. require.Equal(t, tt.wantEffort, gjson.GetBytes(normalized, "reasoning.effort").String())
  160. })
  161. }
  162. }
  163. func TestNormalizeOpenAIPassthroughOAuthBody_AstraPreservesReasoningMode(t *testing.T) {
  164. body := []byte(`{"model":"gpt-6-astra","reasoning":{"mode":"pro","effort":"max"}}`)
  165. normalized, _, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  166. require.NoError(t, err)
  167. require.Equal(t, "pro", gjson.GetBytes(normalized, "reasoning.mode").String())
  168. require.Equal(t, "max", gjson.GetBytes(normalized, "reasoning.effort").String())
  169. }
  170. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_AstraPreservesReasoningMode(t *testing.T) {
  171. body := []byte(`{"type":"response.create","model":"gpt-6-astra","reasoning":{"mode":"pro","effort":"max"}}`)
  172. for _, accountType := range []string{AccountTypeOAuth, AccountTypeSetupToken} {
  173. normalized, _, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: accountType}, false)
  174. require.NoError(t, err)
  175. require.Equal(t, "pro", gjson.GetBytes(normalized, "reasoning.mode").String())
  176. require.Equal(t, "max", gjson.GetBytes(normalized, "reasoning.effort").String())
  177. }
  178. }
  179. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_ReasoningModeAccountScope(t *testing.T) {
  180. body := []byte(`{"type":"response.create","reasoning":{"mode":"pro"}}`)
  181. for _, accountType := range []string{AccountTypeOAuth, AccountTypeSetupToken} {
  182. normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: accountType}, false)
  183. require.NoError(t, err)
  184. require.True(t, changed)
  185. require.Equal(t, "max", gjson.GetBytes(normalized, "reasoning.effort").String())
  186. require.False(t, gjson.GetBytes(normalized, "reasoning.mode").Exists())
  187. }
  188. apiKeyBody, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, false)
  189. require.NoError(t, err)
  190. require.False(t, changed)
  191. require.JSONEq(t, string(body), string(apiKeyBody))
  192. }
  193. func TestNormalizeOpenAIResponsesWebSocketCompatibilityBody_SanitizesToolSchemas(t *testing.T) {
  194. body := []byte(`{"type":"response.create","tools":[{"type":"function","name":"search","parameters":{"type":null,"properties":{"q":{"type":"string","pattern":"^(?=.*foo).+$"}}}}]}`)
  195. for _, accountType := range []string{AccountTypeAPIKey, AccountTypeOAuth} {
  196. normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(body, &Account{Platform: PlatformOpenAI, Type: accountType}, false)
  197. require.NoError(t, err)
  198. require.True(t, changed)
  199. require.Equal(t, "object", gjson.GetBytes(normalized, "tools.0.parameters.type").String())
  200. require.False(t, gjson.GetBytes(normalized, "tools.0.parameters.properties.q.pattern").Exists())
  201. }
  202. }
  203. func TestNormalizeOpenAIResponseFormatSchemasBody_PreservesNonStrictOptionalFields(t *testing.T) {
  204. body := []byte(`{"text":{"format":{"type":"json_schema","strict":false,"schema":{"properties":{"tags":{"items":{"type":"string"},"uniqueItems":true}},"minProperties":1,"maxProperties":4}}}}`)
  205. normalized, changed, err := normalizeOpenAIResponseFormatSchemasBody(body)
  206. require.NoError(t, err)
  207. require.True(t, changed)
  208. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.type").String())
  209. require.Equal(t, "array", gjson.GetBytes(normalized, "text.format.schema.properties.tags.type").String())
  210. require.False(t, gjson.GetBytes(normalized, "text.format.schema.minProperties").Exists())
  211. require.False(t, gjson.GetBytes(normalized, "text.format.schema.properties.tags.uniqueItems").Exists())
  212. require.Equal(t, int64(4), gjson.GetBytes(normalized, "text.format.schema.maxProperties").Int())
  213. require.False(t, gjson.GetBytes(normalized, "text.format.schema.required").Exists())
  214. }
  215. func TestNormalizeOpenAIResponseFormatSchemasBody_DoesNotExpandStrictSchema(t *testing.T) {
  216. body := []byte(`{"response_format":{"type":"json_schema","json_schema":{"strict":true,"schema":{"properties":{"name":{"type":"string"}}}}}}`)
  217. normalized, changed, err := normalizeOpenAIResponseFormatSchemasBody(body)
  218. require.NoError(t, err)
  219. require.True(t, changed) // Safe type inference still applies.
  220. require.Equal(t, "object", gjson.GetBytes(normalized, "response_format.json_schema.schema.type").String())
  221. require.False(t, gjson.GetBytes(normalized, "response_format.json_schema.schema.required").Exists())
  222. require.False(t, gjson.GetBytes(normalized, "response_format.json_schema.schema.additionalProperties").Exists())
  223. }
  224. func TestNormalizeOpenAIResponseFormatSchemasBody_TraversesNestedSchemaContainers(t *testing.T) {
  225. body := []byte(`{
  226. "text":{"format":{"type":"json_schema","schema":{
  227. "$defs":{"entry":{"properties":{"name":{"type":"string"}},"minProperties":1,"maxProperties":3}},
  228. "additionalProperties":{"items":{"type":"string"},"uniqueItems":true},
  229. "prefixItems":[{"properties":{"id":{"type":"string"}},"minProperties":1}],
  230. "dependentSchemas":{"kind":{"properties":{"value":{"type":"string"}},"uniqueItems":true}},
  231. "not":{"items":{"type":"string"},"minProperties":1}
  232. }}}
  233. }`)
  234. normalized, changed, err := normalizeOpenAIResponseFormatSchemasBody(body)
  235. require.NoError(t, err)
  236. require.True(t, changed)
  237. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.$defs.entry.type").String())
  238. require.False(t, gjson.GetBytes(normalized, "text.format.schema.$defs.entry.minProperties").Exists())
  239. require.Equal(t, int64(3), gjson.GetBytes(normalized, "text.format.schema.$defs.entry.maxProperties").Int())
  240. require.Equal(t, "array", gjson.GetBytes(normalized, "text.format.schema.additionalProperties.type").String())
  241. require.False(t, gjson.GetBytes(normalized, "text.format.schema.additionalProperties.uniqueItems").Exists())
  242. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.prefixItems.0.type").String())
  243. require.False(t, gjson.GetBytes(normalized, "text.format.schema.prefixItems.0.minProperties").Exists())
  244. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.dependentSchemas.kind.type").String())
  245. require.False(t, gjson.GetBytes(normalized, "text.format.schema.dependentSchemas.kind.uniqueItems").Exists())
  246. require.Equal(t, "array", gjson.GetBytes(normalized, "text.format.schema.not.type").String())
  247. require.False(t, gjson.GetBytes(normalized, "text.format.schema.not.minProperties").Exists())
  248. require.False(t, gjson.GetBytes(normalized, "text.format.schema.required").Exists())
  249. }
  250. func TestNormalizeOpenAIResponseFormatSchemasBody_PreservesExistingTypeValues(t *testing.T) {
  251. body := []byte(`{"text":{"format":{"type":"json_schema","schema":{"properties":{"union":{"type":["object","null"],"properties":{"name":{"type":"string"}}},"custom":{"type":{"vendor":"shape"},"properties":{"id":{"type":"string"}}},"inferred":{"type":null,"items":{"type":"string"}}}}}}}`)
  252. normalized, changed, err := normalizeOpenAIResponseFormatSchemasBody(body)
  253. require.NoError(t, err)
  254. require.True(t, changed)
  255. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.type").String())
  256. require.Equal(t, "object", gjson.GetBytes(normalized, "text.format.schema.properties.union.type.0").String())
  257. require.Equal(t, "null", gjson.GetBytes(normalized, "text.format.schema.properties.union.type.1").String())
  258. require.True(t, gjson.GetBytes(normalized, "text.format.schema.properties.custom.type").IsObject())
  259. require.Equal(t, "array", gjson.GetBytes(normalized, "text.format.schema.properties.inferred.type").String())
  260. }
  261. func TestNormalizeOpenAIPassthroughOAuthBody_CompactRemovesUnsupportedUser(t *testing.T) {
  262. body := []byte(`{"model":"gpt-5.4","input":"hello","user":"user_123","metadata":{"user_id":"user_123"},"stream":true,"store":true}`)
  263. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, true)
  264. require.NoError(t, err)
  265. require.True(t, changed)
  266. require.False(t, gjson.GetBytes(normalized, "user").Exists())
  267. require.False(t, gjson.GetBytes(normalized, "metadata").Exists())
  268. require.False(t, gjson.GetBytes(normalized, "stream").Exists())
  269. require.False(t, gjson.GetBytes(normalized, "store").Exists())
  270. }
  271. func TestNormalizeOpenAIPassthroughOAuthBody_StringInputWrappedAsArray(t *testing.T) {
  272. body := []byte(`{"model":"gpt-5.4","input":"hello world"}`)
  273. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  274. require.NoError(t, err)
  275. require.True(t, changed)
  276. input := gjson.GetBytes(normalized, "input")
  277. require.True(t, input.IsArray(), "string input should be converted to array")
  278. items := input.Array()
  279. require.Len(t, items, 1)
  280. require.Equal(t, "message", items[0].Get("type").String())
  281. require.Equal(t, "user", items[0].Get("role").String())
  282. require.Equal(t, "hello world", items[0].Get("content").String())
  283. }
  284. func TestNormalizeOpenAIPassthroughOAuthBody_EmptyStringInputWrappedAsEmptyArray(t *testing.T) {
  285. body := []byte(`{"model":"gpt-5.4","input":" "}`)
  286. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  287. require.NoError(t, err)
  288. require.True(t, changed)
  289. input := gjson.GetBytes(normalized, "input")
  290. require.True(t, input.IsArray())
  291. require.Len(t, input.Array(), 0, "whitespace-only input should become empty array")
  292. }
  293. func TestNormalizeOpenAIPassthroughOAuthBody_ObjectInputWrappedAsArray(t *testing.T) {
  294. body := []byte(`{"model":"gpt-5.4","input":{"type":"message","role":"user","content":"hi"}}`)
  295. normalized, changed, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  296. require.NoError(t, err)
  297. require.True(t, changed)
  298. input := gjson.GetBytes(normalized, "input")
  299. require.True(t, input.IsArray(), "object input should be wrapped in array")
  300. items := input.Array()
  301. require.Len(t, items, 1)
  302. require.Equal(t, "message", items[0].Get("type").String())
  303. }
  304. func TestNormalizeOpenAIPassthroughOAuthBody_ArrayInputUnchanged(t *testing.T) {
  305. body := []byte(`{"model":"gpt-5.4","input":[{"type":"message","role":"user","content":"hi"}]}`)
  306. normalized, _, err := normalizeOpenAIPassthroughOAuthBody(body, false)
  307. require.NoError(t, err)
  308. input := gjson.GetBytes(normalized, "input")
  309. require.True(t, input.IsArray())
  310. require.Len(t, input.Array(), 1)
  311. require.Equal(t, "message", input.Array()[0].Get("type").String())
  312. }
  313. func TestDetectOpenAIPassthroughInstructionsRejectReason(t *testing.T) {
  314. for _, tt := range []struct {
  315. name string
  316. body string
  317. want string
  318. }{
  319. {name: "missing is optional", body: `{"model":"gpt-5.1-codex"}`, want: ""},
  320. {name: "non string remains rejected", body: `{"instructions":{"text":"invalid"}}`, want: "instructions_not_string"},
  321. {name: "empty remains rejected", body: `{"instructions":" "}`, want: "instructions_empty"},
  322. {name: "non empty remains accepted", body: `{"instructions":"client guidance"}`, want: ""},
  323. } {
  324. t.Run(tt.name, func(t *testing.T) {
  325. require.Equal(t, tt.want, detectOpenAIPassthroughInstructionsRejectReason("gpt-5.1-codex", []byte(tt.body)))
  326. })
  327. }
  328. }