openai_account_scheduler_compact_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // TestOpenAIGatewayService_SelectAccountWithScheduler_CompactPrefersSupportedOverUnknown
  10. // 验证 compact 调度时显式支持 (tier=2) 优先于未探测 (tier=1)。
  11. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactPrefersSupportedOverUnknown(t *testing.T) {
  12. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  13. ctx := context.Background()
  14. groupID := int64(91001)
  15. accounts := []Account{
  16. {
  17. ID: 71001,
  18. Platform: PlatformOpenAI,
  19. Type: AccountTypeAPIKey,
  20. Status: StatusActive,
  21. Schedulable: true,
  22. Concurrency: 1,
  23. Priority: 0,
  24. Extra: map[string]any{}, // unknown
  25. },
  26. {
  27. ID: 71002,
  28. Platform: PlatformOpenAI,
  29. Type: AccountTypeAPIKey,
  30. Status: StatusActive,
  31. Schedulable: true,
  32. Concurrency: 1,
  33. Priority: 0,
  34. Extra: map[string]any{"openai_compact_supported": true}, // tier=2
  35. },
  36. }
  37. cfg := &config.Config{}
  38. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  39. svc := &OpenAIGatewayService{
  40. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  41. cache: &schedulerTestGatewayCache{},
  42. cfg: cfg,
  43. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  44. }
  45. selection, _, err := svc.SelectAccountWithScheduler(
  46. ctx,
  47. &groupID,
  48. "",
  49. "",
  50. "gpt-5.4",
  51. nil,
  52. OpenAIUpstreamTransportAny,
  53. true,
  54. )
  55. require.NoError(t, err)
  56. require.NotNil(t, selection)
  57. require.NotNil(t, selection.Account)
  58. require.Equal(t, int64(71002), selection.Account.ID, "compact-supported account should win over unknown")
  59. }
  60. // TestOpenAIGatewayService_SelectAccountWithScheduler_CompactRejectsExplicitlyUnsupported
  61. // 验证 force_off / 已探测不支持 (tier=0) 的账号不会被 compact 请求选中。
  62. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactRejectsExplicitlyUnsupported(t *testing.T) {
  63. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  64. ctx := context.Background()
  65. groupID := int64(91002)
  66. accounts := []Account{
  67. {
  68. ID: 71010,
  69. Platform: PlatformOpenAI,
  70. Type: AccountTypeAPIKey,
  71. Status: StatusActive,
  72. Schedulable: true,
  73. Concurrency: 1,
  74. Priority: 0,
  75. Extra: map[string]any{"openai_compact_mode": OpenAICompactModeForceOff},
  76. },
  77. {
  78. ID: 71011,
  79. Platform: PlatformOpenAI,
  80. Type: AccountTypeAPIKey,
  81. Status: StatusActive,
  82. Schedulable: true,
  83. Concurrency: 1,
  84. Priority: 0,
  85. Extra: map[string]any{"openai_compact_supported": false},
  86. },
  87. }
  88. cfg := &config.Config{}
  89. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  90. svc := &OpenAIGatewayService{
  91. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  92. cache: &schedulerTestGatewayCache{},
  93. cfg: cfg,
  94. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  95. }
  96. selection, _, err := svc.SelectAccountWithScheduler(
  97. ctx,
  98. &groupID,
  99. "",
  100. "",
  101. "gpt-5.4",
  102. nil,
  103. OpenAIUpstreamTransportAny,
  104. true,
  105. )
  106. require.Error(t, err)
  107. require.True(t, errors.Is(err, ErrNoAvailableCompactAccounts), "compact-only accounts should rejected explicitly unsupported and return compact error")
  108. require.Nil(t, selection)
  109. }
  110. func newOpenAICompactionSchedulerTestService(accounts []Account, advanced bool) *OpenAIGatewayService {
  111. cfg := &config.Config{}
  112. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  113. svc := &OpenAIGatewayService{
  114. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  115. cache: &schedulerTestGatewayCache{},
  116. cfg: cfg,
  117. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  118. }
  119. if advanced {
  120. svc.rateLimitService = newOpenAIAdvancedSchedulerRateLimitService("true")
  121. }
  122. return svc
  123. }
  124. func selectOpenAICompactionSchedulerTestAccount(t *testing.T, svc *OpenAIGatewayService, groupID int64, requireCompact bool) (*AccountSelectionResult, error) {
  125. t.Helper()
  126. selection, _, err := svc.SelectAccountWithSchedulerForCapability(
  127. context.Background(),
  128. &groupID,
  129. "",
  130. "",
  131. "gpt-5.6-sol",
  132. nil,
  133. OpenAIUpstreamTransportAny,
  134. OpenAIEndpointCapabilityResponses,
  135. requireCompact,
  136. false,
  137. false,
  138. )
  139. return selection, err
  140. }
  141. func TestOpenAIGatewayService_SelectAccountWithScheduler_NativeCompactionIgnoresLegacyCompactProbe(t *testing.T) {
  142. for _, advanced := range []bool{false, true} {
  143. t.Run(map[bool]string{false: "legacy_scheduler", true: "advanced_scheduler"}[advanced], func(t *testing.T) {
  144. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  145. svc := newOpenAICompactionSchedulerTestService([]Account{{
  146. ID: 71012,
  147. Platform: PlatformOpenAI,
  148. Type: AccountTypeAPIKey,
  149. Status: StatusActive,
  150. Schedulable: true,
  151. Concurrency: 1,
  152. Extra: map[string]any{
  153. "openai_compact_supported": false,
  154. "openai_responses_supported": true,
  155. },
  156. }}, advanced)
  157. selection, err := selectOpenAICompactionSchedulerTestAccount(t, svc, 91007, false)
  158. require.NoError(t, err)
  159. require.NotNil(t, selection)
  160. require.Equal(t, int64(71012), selection.Account.ID)
  161. })
  162. }
  163. }
  164. func TestOpenAIGatewayService_SelectAccountWithScheduler_NativeCompactionAllowsForceOff(t *testing.T) {
  165. for _, advanced := range []bool{false, true} {
  166. t.Run(map[bool]string{false: "legacy_scheduler", true: "advanced_scheduler"}[advanced], func(t *testing.T) {
  167. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  168. svc := newOpenAICompactionSchedulerTestService([]Account{{
  169. ID: 71013,
  170. Platform: PlatformOpenAI,
  171. Type: AccountTypeAPIKey,
  172. Status: StatusActive,
  173. Schedulable: true,
  174. Concurrency: 1,
  175. Extra: map[string]any{
  176. "openai_compact_mode": OpenAICompactModeForceOff,
  177. "openai_responses_supported": true,
  178. },
  179. }}, advanced)
  180. selection, err := selectOpenAICompactionSchedulerTestAccount(t, svc, 91008, false)
  181. require.NoError(t, err)
  182. require.NotNil(t, selection)
  183. require.Equal(t, int64(71013), selection.Account.ID)
  184. })
  185. }
  186. }
  187. func TestOpenAIGatewayService_SelectAccountWithScheduler_NativeCompactionRequiresResponsesCapability(t *testing.T) {
  188. for _, advanced := range []bool{false, true} {
  189. t.Run(map[bool]string{false: "legacy_scheduler", true: "advanced_scheduler"}[advanced], func(t *testing.T) {
  190. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  191. svc := newOpenAICompactionSchedulerTestService([]Account{{
  192. ID: 71014,
  193. Platform: PlatformOpenAI,
  194. Type: AccountTypeAPIKey,
  195. Status: StatusActive,
  196. Schedulable: true,
  197. Concurrency: 1,
  198. Extra: map[string]any{
  199. "openai_compact_mode": OpenAICompactModeForceOn,
  200. "openai_responses_supported": false,
  201. },
  202. }}, advanced)
  203. selection, err := selectOpenAICompactionSchedulerTestAccount(t, svc, 91009, false)
  204. require.ErrorIs(t, err, ErrNoAvailableAccounts)
  205. require.NotErrorIs(t, err, ErrNoAvailableCompactAccounts)
  206. require.NotContains(t, err.Error(), "/responses/compact")
  207. require.Nil(t, selection)
  208. })
  209. }
  210. }
  211. func TestOpenAIGatewayService_SelectAccountWithScheduler_LegacyCompactionKeepsCompactEligibility(t *testing.T) {
  212. for _, advanced := range []bool{false, true} {
  213. t.Run(map[bool]string{false: "legacy_scheduler", true: "advanced_scheduler"}[advanced], func(t *testing.T) {
  214. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  215. svc := newOpenAICompactionSchedulerTestService([]Account{
  216. {
  217. ID: 71015,
  218. Platform: PlatformOpenAI,
  219. Type: AccountTypeAPIKey,
  220. Status: StatusActive,
  221. Schedulable: true,
  222. Concurrency: 1,
  223. Extra: map[string]any{
  224. "openai_compact_supported": false,
  225. "openai_responses_supported": true,
  226. },
  227. },
  228. {
  229. ID: 71016,
  230. Platform: PlatformOpenAI,
  231. Type: AccountTypeAPIKey,
  232. Status: StatusActive,
  233. Schedulable: true,
  234. Concurrency: 1,
  235. Extra: map[string]any{
  236. "openai_compact_mode": OpenAICompactModeForceOff,
  237. "openai_responses_supported": true,
  238. },
  239. },
  240. }, advanced)
  241. selection, err := selectOpenAICompactionSchedulerTestAccount(t, svc, 91010, true)
  242. require.ErrorIs(t, err, ErrNoAvailableCompactAccounts)
  243. require.Contains(t, err.Error(), "/responses/compact")
  244. require.Nil(t, selection)
  245. })
  246. }
  247. }
  248. // TestOpenAIGatewayService_SelectAccountWithScheduler_CompactRequiresResponsesCapability
  249. // prevents a confirmed Chat Completions-only API key from silently dropping the
  250. // compaction trigger through the Responses-to-Chat fallback.
  251. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactRequiresResponsesCapability(t *testing.T) {
  252. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  253. ctx := context.Background()
  254. groupID := int64(91005)
  255. accounts := []Account{{
  256. ID: 71050,
  257. Platform: PlatformOpenAI,
  258. Type: AccountTypeAPIKey,
  259. Status: StatusActive,
  260. Schedulable: true,
  261. Concurrency: 1,
  262. Extra: map[string]any{
  263. "openai_compact_supported": true,
  264. "openai_responses_supported": false,
  265. },
  266. }}
  267. cfg := &config.Config{}
  268. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  269. svc := &OpenAIGatewayService{
  270. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  271. cache: &schedulerTestGatewayCache{},
  272. cfg: cfg,
  273. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  274. }
  275. selection, _, err := svc.SelectAccountWithSchedulerForCapability(
  276. ctx,
  277. &groupID,
  278. "",
  279. "",
  280. "gpt-5.4",
  281. nil,
  282. OpenAIUpstreamTransportAny,
  283. OpenAIEndpointCapabilityResponses,
  284. true,
  285. false,
  286. false,
  287. )
  288. require.Error(t, err)
  289. require.True(t, errors.Is(err, ErrNoAvailableAccounts))
  290. require.Nil(t, selection)
  291. }
  292. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactSkipsChatOnlyAccount(t *testing.T) {
  293. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  294. ctx := context.Background()
  295. groupID := int64(91006)
  296. accounts := []Account{
  297. {
  298. ID: 71060,
  299. Platform: PlatformOpenAI,
  300. Type: AccountTypeAPIKey,
  301. Status: StatusActive,
  302. Schedulable: true,
  303. Concurrency: 1,
  304. Priority: 10,
  305. Extra: map[string]any{
  306. "openai_compact_supported": true,
  307. "openai_responses_supported": false,
  308. },
  309. },
  310. {
  311. ID: 71061,
  312. Platform: PlatformOpenAI,
  313. Type: AccountTypeAPIKey,
  314. Status: StatusActive,
  315. Schedulable: true,
  316. Concurrency: 1,
  317. Priority: 0,
  318. Extra: map[string]any{
  319. "openai_compact_supported": true,
  320. "openai_responses_supported": true,
  321. },
  322. },
  323. }
  324. cfg := &config.Config{}
  325. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  326. svc := &OpenAIGatewayService{
  327. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  328. cache: &schedulerTestGatewayCache{},
  329. cfg: cfg,
  330. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  331. }
  332. selection, _, err := svc.SelectAccountWithSchedulerForCapability(
  333. ctx,
  334. &groupID,
  335. "",
  336. "",
  337. "gpt-5.6-sol",
  338. nil,
  339. OpenAIUpstreamTransportAny,
  340. OpenAIEndpointCapabilityResponses,
  341. true,
  342. false,
  343. false,
  344. )
  345. require.NoError(t, err)
  346. require.NotNil(t, selection)
  347. require.NotNil(t, selection.Account)
  348. require.Equal(t, int64(71061), selection.Account.ID)
  349. }
  350. // TestOpenAIGatewayService_SelectAccountWithScheduler_CompactFallsBackToUnknown
  351. // 验证当没有"已知支持"账号时,compact 请求会回退到"未探测"账号。
  352. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactFallsBackToUnknown(t *testing.T) {
  353. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  354. ctx := context.Background()
  355. groupID := int64(91003)
  356. accounts := []Account{
  357. {
  358. ID: 71020,
  359. Platform: PlatformOpenAI,
  360. Type: AccountTypeAPIKey,
  361. Status: StatusActive,
  362. Schedulable: true,
  363. Concurrency: 1,
  364. Priority: 0,
  365. Extra: map[string]any{"openai_compact_supported": false}, // tier=0
  366. },
  367. {
  368. ID: 71021,
  369. Platform: PlatformOpenAI,
  370. Type: AccountTypeAPIKey,
  371. Status: StatusActive,
  372. Schedulable: true,
  373. Concurrency: 1,
  374. Priority: 0,
  375. Extra: map[string]any{}, // unknown -> tier=1
  376. },
  377. }
  378. cfg := &config.Config{}
  379. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  380. svc := &OpenAIGatewayService{
  381. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  382. cache: &schedulerTestGatewayCache{},
  383. cfg: cfg,
  384. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  385. }
  386. selection, _, err := svc.SelectAccountWithScheduler(
  387. ctx,
  388. &groupID,
  389. "",
  390. "",
  391. "gpt-5.4",
  392. nil,
  393. OpenAIUpstreamTransportAny,
  394. true,
  395. )
  396. require.NoError(t, err)
  397. require.NotNil(t, selection)
  398. require.NotNil(t, selection.Account)
  399. require.Equal(t, int64(71021), selection.Account.ID, "unknown account should be picked when no supported account available")
  400. }
  401. // TestOpenAIGatewayService_SelectAccountWithScheduler_CompactAllowsGrok verifies
  402. // that OpenAI-compatible compact routing does not reject Grok accounts.
  403. func TestOpenAIGatewayService_SelectAccountWithScheduler_CompactAllowsGrok(t *testing.T) {
  404. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  405. ctx := context.Background()
  406. groupID := int64(91004)
  407. accounts := []Account{
  408. {
  409. ID: 71030,
  410. Platform: PlatformGrok,
  411. Type: AccountTypeOAuth,
  412. Status: StatusActive,
  413. Schedulable: true,
  414. Concurrency: 1,
  415. Priority: 0,
  416. Credentials: map[string]any{
  417. "model_mapping": map[string]any{"grok-4.5": "grok-4.5"},
  418. },
  419. },
  420. }
  421. cfg := &config.Config{}
  422. cfg.Gateway.Scheduling.LoadBatchEnabled = false
  423. svc := &OpenAIGatewayService{
  424. accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
  425. cache: &schedulerTestGatewayCache{},
  426. cfg: cfg,
  427. concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
  428. }
  429. selection, _, err := svc.SelectAccountWithSchedulerForCapability(
  430. ctx,
  431. &groupID,
  432. "",
  433. "",
  434. "grok-4.5",
  435. nil,
  436. OpenAIUpstreamTransportAny,
  437. OpenAIEndpointCapabilityChatCompletions,
  438. true,
  439. false,
  440. true,
  441. PlatformGrok,
  442. )
  443. require.NoError(t, err)
  444. require.NotNil(t, selection)
  445. require.NotNil(t, selection.Account)
  446. require.Equal(t, int64(71030), selection.Account.ID)
  447. }
  448. // TestOpenAICompactSupportTier 验证 tier 分类逻辑。
  449. func TestOpenAICompactSupportTier(t *testing.T) {
  450. tests := []struct {
  451. name string
  452. account *Account
  453. want int
  454. }{
  455. {name: "nil", account: nil, want: 0},
  456. {name: "non openai", account: &Account{Platform: PlatformAnthropic}, want: 0},
  457. {name: "grok", account: &Account{Platform: PlatformGrok}, want: 2},
  458. {name: "openai unknown", account: &Account{Platform: PlatformOpenAI, Extra: map[string]any{}}, want: 1},
  459. {name: "openai supported", account: &Account{Platform: PlatformOpenAI, Extra: map[string]any{"openai_compact_supported": true}}, want: 2},
  460. {name: "openai unsupported", account: &Account{Platform: PlatformOpenAI, Extra: map[string]any{"openai_compact_supported": false}}, want: 0},
  461. {name: "force on", account: &Account{Platform: PlatformOpenAI, Extra: map[string]any{"openai_compact_mode": OpenAICompactModeForceOn}}, want: 2},
  462. {name: "force off overrides probe true", account: &Account{Platform: PlatformOpenAI, Extra: map[string]any{"openai_compact_mode": OpenAICompactModeForceOff, "openai_compact_supported": true}}, want: 0},
  463. }
  464. for _, tt := range tests {
  465. t.Run(tt.name, func(t *testing.T) {
  466. if got := openAICompactSupportTier(tt.account); got != tt.want {
  467. t.Fatalf("openAICompactSupportTier(...) = %d, want %d", got, tt.want)
  468. }
  469. })
  470. }
  471. }