gateway_service_bedrock_beta_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. )
  8. type betaPolicySettingRepoStub struct {
  9. values map[string]string
  10. }
  11. func (s *betaPolicySettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  12. panic("unexpected Get call")
  13. }
  14. func (s *betaPolicySettingRepoStub) GetValue(ctx context.Context, key string) (string, error) {
  15. if v, ok := s.values[key]; ok {
  16. return v, nil
  17. }
  18. return "", ErrSettingNotFound
  19. }
  20. func (s *betaPolicySettingRepoStub) Set(ctx context.Context, key, value string) error {
  21. panic("unexpected Set call")
  22. }
  23. func (s *betaPolicySettingRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  24. panic("unexpected GetMultiple call")
  25. }
  26. func (s *betaPolicySettingRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  27. panic("unexpected SetMultiple call")
  28. }
  29. func (s *betaPolicySettingRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  30. panic("unexpected GetAll call")
  31. }
  32. func (s *betaPolicySettingRepoStub) Delete(ctx context.Context, key string) error {
  33. panic("unexpected Delete call")
  34. }
  35. func TestResolveBedrockBetaTokensForRequest_BlocksOnOriginalAnthropicToken(t *testing.T) {
  36. settings := &BetaPolicySettings{
  37. Rules: []BetaPolicyRule{
  38. {
  39. BetaToken: "advanced-tool-use-2025-11-20",
  40. Action: BetaPolicyActionBlock,
  41. Scope: BetaPolicyScopeAll,
  42. ErrorMessage: "advanced tool use is blocked",
  43. },
  44. },
  45. }
  46. raw, err := json.Marshal(settings)
  47. if err != nil {
  48. t.Fatalf("marshal settings: %v", err)
  49. }
  50. svc := &GatewayService{
  51. settingService: NewSettingService(
  52. &betaPolicySettingRepoStub{values: map[string]string{
  53. SettingKeyBetaPolicySettings: string(raw),
  54. }},
  55. &config.Config{},
  56. ),
  57. }
  58. account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  59. _, err = svc.resolveBedrockBetaTokensForRequest(
  60. context.Background(),
  61. account,
  62. "advanced-tool-use-2025-11-20",
  63. []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
  64. "us.anthropic.claude-opus-4-6-v1",
  65. )
  66. if err == nil {
  67. t.Fatal("expected raw advanced-tool-use token to be blocked before Bedrock transform")
  68. }
  69. if err.Error() != "advanced tool use is blocked" {
  70. t.Fatalf("unexpected error: %v", err)
  71. }
  72. }
  73. func TestResolveBedrockBetaTokensForRequest_FiltersAfterBedrockTransform(t *testing.T) {
  74. settings := &BetaPolicySettings{
  75. Rules: []BetaPolicyRule{
  76. {
  77. BetaToken: "tool-search-tool-2025-10-19",
  78. Action: BetaPolicyActionFilter,
  79. Scope: BetaPolicyScopeAll,
  80. },
  81. },
  82. }
  83. raw, err := json.Marshal(settings)
  84. if err != nil {
  85. t.Fatalf("marshal settings: %v", err)
  86. }
  87. svc := &GatewayService{
  88. settingService: NewSettingService(
  89. &betaPolicySettingRepoStub{values: map[string]string{
  90. SettingKeyBetaPolicySettings: string(raw),
  91. }},
  92. &config.Config{},
  93. ),
  94. }
  95. account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  96. betaTokens, err := svc.resolveBedrockBetaTokensForRequest(
  97. context.Background(),
  98. account,
  99. "advanced-tool-use-2025-11-20",
  100. []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
  101. "us.anthropic.claude-opus-4-6-v1",
  102. )
  103. if err != nil {
  104. t.Fatalf("unexpected error: %v", err)
  105. }
  106. for _, token := range betaTokens {
  107. if token == "tool-search-tool-2025-10-19" {
  108. t.Fatalf("expected transformed Bedrock token to be filtered")
  109. }
  110. }
  111. }
  112. // TestResolveBedrockBetaTokensForRequest_BlocksBodyAutoInjectedComputerUse 验证:
  113. // 管理员 block 了 computer-use,客户端不在 header 中带该 token,
  114. // 但请求体包含 computer_use 工具 → 自动注入后应被 block。
  115. func TestResolveBedrockBetaTokensForRequest_BlocksBodyAutoInjectedComputerUse(t *testing.T) {
  116. settings := &BetaPolicySettings{
  117. Rules: []BetaPolicyRule{
  118. {
  119. BetaToken: "computer-use-2025-11-24",
  120. Action: BetaPolicyActionBlock,
  121. Scope: BetaPolicyScopeAll,
  122. ErrorMessage: "computer use is blocked",
  123. },
  124. },
  125. }
  126. raw, err := json.Marshal(settings)
  127. if err != nil {
  128. t.Fatalf("marshal settings: %v", err)
  129. }
  130. svc := &GatewayService{
  131. settingService: NewSettingService(
  132. &betaPolicySettingRepoStub{values: map[string]string{
  133. SettingKeyBetaPolicySettings: string(raw),
  134. }},
  135. &config.Config{},
  136. ),
  137. }
  138. account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  139. // header 中不带 beta token,但 body 中有 computer_use 工具
  140. _, err = svc.resolveBedrockBetaTokensForRequest(
  141. context.Background(),
  142. account,
  143. "", // 空 header
  144. []byte(`{"tools":[{"type":"computer_20250124","name":"computer"}],"messages":[{"role":"user","content":"hi"}]}`),
  145. "us.anthropic.claude-opus-4-6-v1",
  146. )
  147. if err == nil {
  148. t.Fatal("expected body-injected computer-use to be blocked")
  149. }
  150. if err.Error() != "computer use is blocked" {
  151. t.Fatalf("unexpected error: %v", err)
  152. }
  153. }
  154. // TestResolveBedrockBetaTokensForRequest_BlocksBodyAutoInjectedToolSearch 验证:
  155. // 管理员 block 了 tool-search-tool,客户端不在 header 中带 beta token,
  156. // 但请求体包含 tool search 工具 → 自动注入后应被 block。
  157. func TestResolveBedrockBetaTokensForRequest_BlocksBodyAutoInjectedToolSearch(t *testing.T) {
  158. settings := &BetaPolicySettings{
  159. Rules: []BetaPolicyRule{
  160. {
  161. BetaToken: "tool-search-tool-2025-10-19",
  162. Action: BetaPolicyActionBlock,
  163. Scope: BetaPolicyScopeAll,
  164. ErrorMessage: "tool search is blocked",
  165. },
  166. },
  167. }
  168. raw, err := json.Marshal(settings)
  169. if err != nil {
  170. t.Fatalf("marshal settings: %v", err)
  171. }
  172. svc := &GatewayService{
  173. settingService: NewSettingService(
  174. &betaPolicySettingRepoStub{values: map[string]string{
  175. SettingKeyBetaPolicySettings: string(raw),
  176. }},
  177. &config.Config{},
  178. ),
  179. }
  180. account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  181. // header 中不带 beta token,但 body 中有 tool_search_tool 工具
  182. _, err = svc.resolveBedrockBetaTokensForRequest(
  183. context.Background(),
  184. account,
  185. "",
  186. []byte(`{"tools":[{"type":"tool_search_tool_regex_20251119","name":"search"}],"messages":[{"role":"user","content":"hi"}]}`),
  187. "us.anthropic.claude-sonnet-4-6",
  188. )
  189. if err == nil {
  190. t.Fatal("expected body-injected tool-search-tool to be blocked")
  191. }
  192. if err.Error() != "tool search is blocked" {
  193. t.Fatalf("unexpected error: %v", err)
  194. }
  195. }
  196. // TestResolveBedrockBetaTokensForRequest_PassesWhenNoBlockRuleMatches 验证:
  197. // body 自动注入的 token 如果没有对应的 block 规则,应正常通过。
  198. func TestResolveBedrockBetaTokensForRequest_PassesWhenNoBlockRuleMatches(t *testing.T) {
  199. settings := &BetaPolicySettings{
  200. Rules: []BetaPolicyRule{
  201. {
  202. BetaToken: "context-1m-2025-08-07",
  203. Action: BetaPolicyActionBlock,
  204. Scope: BetaPolicyScopeAll,
  205. ErrorMessage: "context is blocked",
  206. },
  207. },
  208. }
  209. raw, err := json.Marshal(settings)
  210. if err != nil {
  211. t.Fatalf("marshal settings: %v", err)
  212. }
  213. svc := &GatewayService{
  214. settingService: NewSettingService(
  215. &betaPolicySettingRepoStub{values: map[string]string{
  216. SettingKeyBetaPolicySettings: string(raw),
  217. }},
  218. &config.Config{},
  219. ),
  220. }
  221. account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  222. // body 中有 computer_use 工具(会注入 computer-use token),但 block 规则只针对 context-1m
  223. tokens, err := svc.resolveBedrockBetaTokensForRequest(
  224. context.Background(),
  225. account,
  226. "",
  227. []byte(`{"tools":[{"type":"computer_20250124","name":"computer"}],"messages":[{"role":"user","content":"hi"}]}`),
  228. "us.anthropic.claude-opus-4-6-v1",
  229. )
  230. if err != nil {
  231. t.Fatalf("unexpected error: %v", err)
  232. }
  233. found := false
  234. for _, token := range tokens {
  235. if token == "computer-use-2025-11-24" {
  236. found = true
  237. }
  238. }
  239. if !found {
  240. t.Fatal("expected computer-use token to be present")
  241. }
  242. }