package responsecache import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func paramsHashOf(body, path string, excludeSystem bool) string { return computeParamsHash([]byte(body), path, nil, "", "embed", excludeSystem) } func TestComputeParamsHash_OutputShapingFieldsChangeHash(t *testing.T) { const ( chatBase = `{"model":"m","messages":[{"role":"user","content":"same"}]}` responsesBase = `{"model":"m","input":"same"}` messagesBase = `{"model":"m","max_tokens":64,"messages":[{"role":"user","content":"same"}]}` ) tests := []struct { name string path string base string changed string }{ {name: "chat stop", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","stop":["\n"],"messages":[{"role":"user","content":"same"}]}`}, {name: "chat seed", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","seed":7,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat n", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","n":3,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat tool_choice", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","tool_choice":"required","messages":[{"role":"user","content":"same"}]}`}, {name: "chat parallel_tool_calls", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","parallel_tool_calls":false,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat frequency_penalty", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","frequency_penalty":1,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat presence_penalty", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","presence_penalty":1,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat logit_bias", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","logit_bias":{"50256":-100},"messages":[{"role":"user","content":"same"}]}`}, {name: "chat logprobs", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","logprobs":true,"top_logprobs":2,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat max_completion_tokens", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","max_completion_tokens":16,"messages":[{"role":"user","content":"same"}]}`}, {name: "chat reasoning_effort", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","reasoning_effort":"high","messages":[{"role":"user","content":"same"}]}`}, {name: "chat unknown field", path: "/v1/chat/completions", base: chatBase, changed: `{"model":"m","future_knob":1,"messages":[{"role":"user","content":"same"}]}`}, {name: "responses text format", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","text":{"format":{"type":"json_object"}}}`}, {name: "responses tool_choice", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","tool_choice":"none"}`}, {name: "responses truncation", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","truncation":"auto"}`}, {name: "responses instructions", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","instructions":"be terse"}`}, {name: "messages system", path: "/v1/messages", base: messagesBase, changed: `{"model":"m","max_tokens":64,"system":"be terse","messages":[{"role":"user","content":"same"}]}`}, {name: "messages thinking", path: "/v1/messages", base: messagesBase, changed: `{"model":"m","max_tokens":64,"thinking":{"type":"enabled","budget_tokens":32},"messages":[{"role":"user","content":"same"}]}`}, {name: "messages stop_sequences", path: "/v1/messages", base: messagesBase, changed: `{"model":"m","max_tokens":64,"stop_sequences":["END"],"messages":[{"role":"user","content":"same"}]}`}, {name: "messages top_k", path: "/v1/messages", base: messagesBase, changed: `{"model":"m","max_tokens":64,"top_k":5,"messages":[{"role":"user","content":"same"}]}`}, {name: "seed beyond float64 precision", path: "/v1/chat/completions", base: `{"model":"m","seed":9007199254740992,"messages":[]}`, changed: `{"model":"m","seed":9007199254740993,"messages":[]}`}, {name: "input beside messages", path: "/v1/responses", base: `{"model":"m","messages":[{"role":"user","content":"same"}],"input":"first"}`, changed: `{"model":"m","messages":[{"role":"user","content":"same"}],"input":"second"}`}, {name: "messages beside input", path: "/v1/responses", base: `{"model":"m","messages":[{"role":"user","content":"same"}],"input":"same"}`, changed: `{"model":"m","messages":[{"role":"user","content":"other"}],"input":"same"}`}, {name: "mixed tool list order", path: "/v1/chat/completions", base: `{"model":"m","tools":[{"type":"function","function":{"name":"b"}},{"type":"custom","custom":{"name":"x"}},{"type":"function","function":{"name":"a"}}]}`, changed: `{"model":"m","tools":[{"type":"function","function":{"name":"a"}},{"type":"custom","custom":{"name":"x"}},{"type":"function","function":{"name":"b"}}]}`}, {name: "duplicate tool name order", path: "/v1/chat/completions", base: `{"model":"m","tools":[{"type":"function","function":{"name":"a","description":"1"}},{"type":"function","function":{"name":"a","description":"2"}}]}`, changed: `{"model":"m","tools":[{"type":"function","function":{"name":"a","description":"2"}},{"type":"function","function":{"name":"a","description":"1"}}]}`}, {name: "key holding a NUL is not two members", path: "/v1/chat/completions", base: `{"model":"m","temperature":0.5,"top_p":1,"messages":[]}`, changed: `{"model":"m","temperature\u00000.5\u0000top_p":1,"messages":[]}`}, {name: "responses metadata is echoed", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","metadata":{"team":"a"}}`}, {name: "responses user is echoed", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","user":"u1"}`}, {name: "responses safety_identifier is echoed", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","safety_identifier":"s1"}`}, {name: "responses prompt_cache_key is echoed", path: "/v1/responses", base: responsesBase, changed: `{"model":"m","input":"same","prompt_cache_key":"k"}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.NotEqual(t, paramsHashOf(tt.base, tt.path, false), paramsHashOf(tt.changed, tt.path, false)) }) } } func TestComputeParamsHash_IgnoresNonShapingDifferences(t *testing.T) { const path = "/v1/chat/completions" tests := []struct { name string path string first string other string }{ {name: "messages caller identity", path: "/v1/messages", first: `{"model":"m","max_tokens":8,"messages":[]}`, other: `{"model":"m","max_tokens":8,"metadata":{"user_id":"u1"},"messages":[]}`}, {name: "member order and whitespace", first: `{"model":"m","temperature":0.5,"messages":[]}`, other: "{ \"messages\": [],\n \"temperature\": 0.5, \"model\": \"m\" }"}, {name: "nested member order", first: `{"model":"m","response_format":{"type":"json_schema","json_schema":{"name":"a","strict":true}}}`, other: `{"model":"m","response_format":{"json_schema":{"strict":true,"name":"a"},"type":"json_schema"}}`}, {name: "prompt text", first: `{"model":"m","messages":[{"role":"user","content":"hello"}]}`, other: `{"model":"m","messages":[{"role":"user","content":"hi there"}]}`}, {name: "responses input", first: `{"model":"m","input":"hello"}`, other: `{"model":"m","input":[{"role":"user","content":"hi"}]}`}, {name: "caller identity", first: `{"model":"m","messages":[]}`, other: `{"model":"m","user":"u1","safety_identifier":"s1","metadata":{"team":"a"},"messages":[]}`}, {name: "provider prompt cache hints", first: `{"model":"m","messages":[]}`, other: `{"model":"m","prompt_cache_key":"k","prompt_cache_retention":"24h","messages":[]}`}, {name: "stream false is absent", first: `{"model":"m","messages":[]}`, other: `{"model":"m","stream":false,"messages":[]}`}, {name: "stream_options without stream", first: `{"model":"m","messages":[]}`, other: `{"model":"m","stream_options":{"include_usage":true},"messages":[]}`}, {name: "tool order", first: `{"model":"m","tools":[{"type":"function","function":{"name":"a"}},{"type":"function","function":{"name":"b"}}]}`, other: `{"model":"m","tools":[{"type":"function","function":{"name":"b"}},{"type":"function","function":{"name":"a"}}]}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := path if tt.path != "" { p = tt.path } assert.Equal(t, paramsHashOf(tt.first, p, false), paramsHashOf(tt.other, p, false)) }) } } func TestComputeParamsHash_TopLevelSystemFollowsExcludeSystem(t *testing.T) { const ( path = "/v1/messages" terse = `{"model":"m","system":"be terse","messages":[{"role":"user","content":"same"}]}` long = `{"model":"m","system":"be thorough","messages":[{"role":"user","content":"same"}]}` ) assert.NotEqual(t, paramsHashOf(terse, path, false), paramsHashOf(long, path, false), "system shapes output by default") assert.Equal(t, paramsHashOf(terse, path, true), paramsHashOf(long, path, true), "exclude_system_prompt leaves it out") } func TestSemanticCacheMiddleware_ExcludeSystemPromptIgnoresTopLevelSystem(t *testing.T) { m, store, _ := newTestSemanticMiddleware(0.90, 10, true) terse := []byte(`{"model":"gpt-4","system":"be terse","messages":[{"role":"user","content":"what is 2+2"}]}`) thorough := []byte(`{"model":"gpt-4","system":"be thorough","messages":[{"role":"user","content":"what is 2+2"}]}`) serveSemanticRequest(t, m, terse, "") m.wg.Wait() require.Equal(t, 1, store.Len()) rec := serveSemanticRequest(t, m, thorough, "") assert.Equal(t, "HIT (semantic)", rec.Header().Get("X-Cache")) } func TestComputeParamsHash_DuplicateMemberNamesHashVerbatim(t *testing.T) { const path = "/v1/chat/completions" duplicated := `{"model":"m","tool_choice":"none","tool_choice":"required","messages":[]}` deduplicated := `{"model":"m","tool_choice":"required","messages":[]}` assert.NotEqual(t, paramsHashOf(deduplicated, path, false), paramsHashOf(duplicated, path, false)) } func TestSemanticCacheMiddleware_ParamsHashIsolation_ToolChoice(t *testing.T) { m, store, _ := newTestSemanticMiddleware(0.90, 10, false) auto := []byte(`{"model":"gpt-4","tool_choice":"auto","messages":[{"role":"user","content":"same prompt"}]}`) required := []byte(`{"model":"gpt-4","tool_choice":"required","messages":[{"role":"user","content":"same prompt"}]}`) serveSemanticRequest(t, m, auto, "") m.wg.Wait() require.Equal(t, 1, store.Len()) rec := serveSemanticRequest(t, m, required, "") assert.NotEqual(t, "HIT (semantic)", rec.Header().Get("X-Cache")) rec = serveSemanticRequest(t, m, auto, "") assert.Equal(t, "HIT (semantic)", rec.Header().Get("X-Cache")) }