openai_compat_prompt_cache_key_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func mustRawJSON(t *testing.T, s string) json.RawMessage {
  10. t.Helper()
  11. return json.RawMessage(s)
  12. }
  13. func TestShouldAutoInjectPromptCacheKeyForCompat(t *testing.T) {
  14. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.5"))
  15. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.5-pro"))
  16. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.4"))
  17. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.4-mini"))
  18. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.2"))
  19. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.3"))
  20. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.3-codex"))
  21. require.True(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-5.3-codex-spark"))
  22. require.False(t, shouldAutoInjectPromptCacheKeyForCompat("gpt-4o"))
  23. }
  24. func TestShouldAutoInjectPromptCacheKeyForCompat_GPT6AstraForms(t *testing.T) {
  25. for _, model := range []string{
  26. "gpt-6",
  27. "gpt-6-astra",
  28. "openai/gpt-6",
  29. "openai/gpt-6-astra",
  30. "OPENAI/GPT-6_ASTRA",
  31. "provider/gpt-6-astra",
  32. } {
  33. require.True(t, shouldAutoInjectPromptCacheKeyForCompat(model), model)
  34. }
  35. for _, model := range []string{
  36. "gpt-6-terra",
  37. "gpt-6.1",
  38. "gpt-6-astra-preview",
  39. "gpt-6-astra-unrelated-name",
  40. "gpt-6-astra-2026-09-01",
  41. "gpt-6-astra-2026-09-01-extra",
  42. "gpt-6-astra-v",
  43. "gpt-6-astra-v1.beta",
  44. "claude-sonnet-4-5",
  45. "gpt-4o",
  46. } {
  47. require.False(t, shouldAutoInjectPromptCacheKeyForCompat(model), model)
  48. }
  49. }
  50. func TestDeriveCompatPromptCacheKey_StableAcrossLaterTurns(t *testing.T) {
  51. base := &apicompat.ChatCompletionsRequest{
  52. Model: "gpt-5.4",
  53. Messages: []apicompat.ChatMessage{
  54. {Role: "system", Content: mustRawJSON(t, `"You are helpful."`)},
  55. {Role: "user", Content: mustRawJSON(t, `"Hello"`)},
  56. },
  57. }
  58. extended := &apicompat.ChatCompletionsRequest{
  59. Model: "gpt-5.4",
  60. Messages: []apicompat.ChatMessage{
  61. {Role: "system", Content: mustRawJSON(t, `"You are helpful."`)},
  62. {Role: "user", Content: mustRawJSON(t, `"Hello"`)},
  63. {Role: "assistant", Content: mustRawJSON(t, `"Hi there!"`)},
  64. {Role: "user", Content: mustRawJSON(t, `"How are you?"`)},
  65. },
  66. }
  67. k1 := deriveCompatPromptCacheKey(base, "gpt-5.4")
  68. k2 := deriveCompatPromptCacheKey(extended, "gpt-5.4")
  69. require.Equal(t, k1, k2, "cache key should be stable across later turns")
  70. require.NotEmpty(t, k1)
  71. }
  72. func TestDeriveCompatPromptCacheKey_DiffersAcrossSessions(t *testing.T) {
  73. req1 := &apicompat.ChatCompletionsRequest{
  74. Model: "gpt-5.4",
  75. Messages: []apicompat.ChatMessage{
  76. {Role: "user", Content: mustRawJSON(t, `"Question A"`)},
  77. },
  78. }
  79. req2 := &apicompat.ChatCompletionsRequest{
  80. Model: "gpt-5.4",
  81. Messages: []apicompat.ChatMessage{
  82. {Role: "user", Content: mustRawJSON(t, `"Question B"`)},
  83. },
  84. }
  85. k1 := deriveCompatPromptCacheKey(req1, "gpt-5.4")
  86. k2 := deriveCompatPromptCacheKey(req2, "gpt-5.4")
  87. require.NotEqual(t, k1, k2, "different first user messages should yield different keys")
  88. }
  89. func TestDeriveCompatPromptCacheKey_UsesResolvedSparkFamily(t *testing.T) {
  90. req := &apicompat.ChatCompletionsRequest{
  91. Model: "gpt-5.3-codex-spark",
  92. Messages: []apicompat.ChatMessage{
  93. {Role: "user", Content: mustRawJSON(t, `"Question A"`)},
  94. },
  95. }
  96. k1 := deriveCompatPromptCacheKey(req, "gpt-5.3-codex-spark")
  97. k2 := deriveCompatPromptCacheKey(req, " openai/gpt-5.3-codex-spark ")
  98. require.NotEmpty(t, k1)
  99. require.Equal(t, k1, k2, "resolved spark family should derive a stable compat cache key")
  100. }
  101. func TestDeriveAnthropicCompatPromptCacheKey_StableAcrossLaterTurns(t *testing.T) {
  102. base := &apicompat.AnthropicRequest{
  103. Model: "claude-sonnet-4-5",
  104. System: mustRawJSON(t, `"You are helpful."`),
  105. Messages: []apicompat.AnthropicMessage{
  106. {Role: "user", Content: mustRawJSON(t, `"Open repo"`)},
  107. },
  108. }
  109. extended := &apicompat.AnthropicRequest{
  110. Model: "claude-sonnet-4-5",
  111. System: mustRawJSON(t, `"You are helpful."`),
  112. Messages: []apicompat.AnthropicMessage{
  113. {Role: "user", Content: mustRawJSON(t, `"Open repo"`)},
  114. {Role: "assistant", Content: mustRawJSON(t, `"Opened."`)},
  115. {Role: "user", Content: mustRawJSON(t, `"Run tests"`)},
  116. },
  117. }
  118. k1 := deriveAnthropicCompatPromptCacheKey(base, "gpt-5.3-codex")
  119. k2 := deriveAnthropicCompatPromptCacheKey(extended, "gpt-5.3-codex")
  120. require.NotEmpty(t, k1)
  121. require.Equal(t, k1, k2, "cache key should stay stable as later Claude Code turns append history")
  122. }
  123. func TestDeriveAnthropicCompatPromptCacheKey_UsesCacheControlAnchors(t *testing.T) {
  124. base := &apicompat.AnthropicRequest{
  125. Model: "claude-sonnet-4-5",
  126. System: mustRawJSON(t, `[
  127. {"type":"text","text":"project instructions","cache_control":{"type":"ephemeral"}}
  128. ]`),
  129. Messages: []apicompat.AnthropicMessage{
  130. {Role: "user", Content: mustRawJSON(t, `[
  131. {"type":"text","text":"repo anchor","cache_control":{"type":"ephemeral"}}
  132. ]`)},
  133. },
  134. }
  135. extended := &apicompat.AnthropicRequest{
  136. Model: base.Model,
  137. System: base.System,
  138. Messages: []apicompat.AnthropicMessage{
  139. base.Messages[0],
  140. {Role: "assistant", Content: mustRawJSON(t, `[{"type":"text","text":"Opened."}]`)},
  141. {Role: "user", Content: mustRawJSON(t, `[{"type":"text","text":"Run tests"}]`)},
  142. },
  143. }
  144. k1 := deriveAnthropicCompatPromptCacheKey(base, "gpt-5.4")
  145. k2 := deriveAnthropicCompatPromptCacheKey(extended, "gpt-5.4")
  146. require.NotEmpty(t, k1)
  147. require.Equal(t, k1, k2)
  148. require.True(t, strings.HasPrefix(k1, "anthropic-cache-"))
  149. require.False(t, strings.HasPrefix(k1, compatPromptCacheKeyPrefix))
  150. }