@bitkyc08/opencodex 2.32.1 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/gui/dist/assets/{index-8HxacKlZ.js → index-23-Lf7jR.js} +14 -14
  2. package/gui/dist/assets/index-DxJMDyOr.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +18 -4
  6. package/src/adapters/kiro-tools.ts +20 -9
  7. package/src/bridge.ts +12 -8
  8. package/src/claude/context-windows.ts +16 -9
  9. package/src/cli/doctor.ts +2 -2
  10. package/src/cli/index.ts +9 -2
  11. package/src/cli/status.ts +23 -0
  12. package/src/codex/auth-api.ts +4 -2
  13. package/src/codex/autostart-health.ts +16 -0
  14. package/src/codex/catalog/aggregation.ts +12 -0
  15. package/src/codex/catalog/effort.ts +18 -3
  16. package/src/codex/catalog/metadata.ts +27 -1
  17. package/src/codex/catalog/parsing.ts +38 -27
  18. package/src/codex/catalog/provider-fetch.ts +143 -25
  19. package/src/codex/catalog/sync.ts +1 -1
  20. package/src/codex/convergence.ts +5 -0
  21. package/src/codex/shim.ts +56 -3
  22. package/src/config.ts +24 -0
  23. package/src/generated/compatibility-version.json +43 -35
  24. package/src/oauth/callback-server.ts +22 -2
  25. package/src/oauth/kimi.ts +9 -1
  26. package/src/oauth/open-browser-choice.ts +26 -0
  27. package/src/providers/auto-compact-budget.ts +65 -0
  28. package/src/providers/xai-transport.ts +21 -0
  29. package/src/server/auth-cors.ts +14 -0
  30. package/src/server/management/agent-settings-routes.ts +13 -7
  31. package/src/server/management/config-routes.ts +31 -5
  32. package/src/server/management/model-rows.ts +4 -0
  33. package/src/server/management/oauth-account-routes.ts +10 -4
  34. package/src/server/management/provider-routes.ts +75 -11
  35. package/src/server/responses/core.ts +12 -1
  36. package/src/server/responses/empty-completion-guard.ts +28 -6
  37. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  38. package/src/types/config.ts +13 -0
  39. package/src/types/provider.ts +5 -0
  40. package/src/types/tools.ts +27 -0
  41. package/src/types.ts +1 -0
  42. package/gui/dist/assets/index-DcBbHIAz.css +0 -1
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "04296a57ecf1045f16342e12ccba46e204ba2991cf33660531ea0924b4dbbcc1"
13
+ "sha256": "b2e14672cf7df2f0eadb7e4327c50c6ff2768e6fdc023927fe22da8fc336e249"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "path": "src/adapters/anthropic.ts",
37
- "sha256": "af1369074d4639e8e6863184734837cd6969a6cd5e1c51ceb9f258fcf761a859"
37
+ "sha256": "8e3a0ab54d16da6a290d7574e5360b0c55e63d359d107a448c440eb80d935df0"
38
38
  },
39
39
  {
40
40
  "path": "src/adapters/azure.ts",
@@ -274,7 +274,7 @@
274
274
  },
275
275
  {
276
276
  "path": "src/adapters/kiro-tools.ts",
277
- "sha256": "74820aaa19ca00a9b28d8d862b068a29fd489c74b739acaacaa68f3ec1b0ab35"
277
+ "sha256": "a176dac1e44959607f817c88c777df5394da3006f68fe9d5b759393600633da6"
278
278
  },
279
279
  {
280
280
  "path": "src/adapters/kiro-truncation.ts",
@@ -338,7 +338,7 @@
338
338
  },
339
339
  {
340
340
  "path": "src/bridge.ts",
341
- "sha256": "ed2f284f6eec13422a0ccd8fb6d1621776c3b24ed1e38bfba8e4add16d96a7e8"
341
+ "sha256": "9a50601ff7b592204713845db8631bc01cf4bb0e9d8e8a3a7e226ce19148e03b"
342
342
  },
343
343
  {
344
344
  "path": "src/chat/inbound.ts",
@@ -370,7 +370,7 @@
370
370
  },
371
371
  {
372
372
  "path": "src/claude/context-windows.ts",
373
- "sha256": "4c49d8efeec1d1060e9e61201370d4dce268b7826ca3fcff773d1d53c4c548b4"
373
+ "sha256": "c400f4b76319ec14855128af40fe52b60d31d217b89e291641522a51d2583d97"
374
374
  },
375
375
  {
376
376
  "path": "src/claude/desktop-3p-guard.ts",
@@ -498,7 +498,7 @@
498
498
  },
499
499
  {
500
500
  "path": "src/cli/doctor.ts",
501
- "sha256": "9aa9c6470bb69b32db20fa7b48f193fc21d08d303cf340b55d2b7a3335eab5c6"
501
+ "sha256": "3809a5be6f5223ffcfc9225187dcca8e1f4be0b01d281126fe3722f89a754767"
502
502
  },
503
503
  {
504
504
  "path": "src/cli/ensure-desired-integrations.ts",
@@ -514,7 +514,7 @@
514
514
  },
515
515
  {
516
516
  "path": "src/cli/index.ts",
517
- "sha256": "40abaa584326962536f4b00ef6a5af3319c58b1d2bcee900a7303b9bd986761d"
517
+ "sha256": "64b43bdeed4ba242bfbf2531f49da3dc2c2292ed2a6a0cef0bb93cbc1222075e"
518
518
  },
519
519
  {
520
520
  "path": "src/cli/init.ts",
@@ -594,7 +594,7 @@
594
594
  },
595
595
  {
596
596
  "path": "src/cli/status.ts",
597
- "sha256": "5388615c35af1f140c5fa78d891fb030ea82aa0382dcafbb3086cd8cae616640"
597
+ "sha256": "809fe8cf65f422b1405c67a4a5739f79d988f8b421d8bdb8003c9dffd4c51d56"
598
598
  },
599
599
  {
600
600
  "path": "src/cli/system-command.ts",
@@ -678,7 +678,7 @@
678
678
  },
679
679
  {
680
680
  "path": "src/codex/auth-api.ts",
681
- "sha256": "e9e1dcd8c0ac39c93a1ed7977398bd5ce518669f5cc04b9f1331d2d1f54d9ed1"
681
+ "sha256": "b1b5dcece7e1f817a134361f6895c35550afc66d5db11602c6d25d0c40c2f0b8"
682
682
  },
683
683
  {
684
684
  "path": "src/codex/auth-collision.ts",
@@ -690,7 +690,7 @@
690
690
  },
691
691
  {
692
692
  "path": "src/codex/autostart-health.ts",
693
- "sha256": "b91e91d7dd2a0373b4da9a6955e714ba79326046dc1d45b4ce129e955bc069e0"
693
+ "sha256": "e15bd998f1598bf6fe8a1bffdbb836225503baae36e6dc58357b4bb4bde425b6"
694
694
  },
695
695
  {
696
696
  "path": "src/codex/catalog-admission.ts",
@@ -714,7 +714,7 @@
714
714
  },
715
715
  {
716
716
  "path": "src/codex/catalog/aggregation.ts",
717
- "sha256": "4c3ee463d05c005b0127926d756f81e7d01adbbd68be3130724410c428bda6f7"
717
+ "sha256": "aa80a049a216ffde1d39a221281c9f5666330f05cadd93a11399eac36c6f9404"
718
718
  },
719
719
  {
720
720
  "path": "src/codex/catalog/bundled.ts",
@@ -722,7 +722,7 @@
722
722
  },
723
723
  {
724
724
  "path": "src/codex/catalog/effort.ts",
725
- "sha256": "e9efbb137522a8952150a75cc04061af43e161203f57b2d02650973119e2316c"
725
+ "sha256": "b837a78528f944ece136efc155aee4ff819f44f57d1affc65db1b36f47048ba6"
726
726
  },
727
727
  {
728
728
  "path": "src/codex/catalog/filesystem-evidence.ts",
@@ -734,7 +734,7 @@
734
734
  },
735
735
  {
736
736
  "path": "src/codex/catalog/metadata.ts",
737
- "sha256": "e1c798b07f69a0c6130581096a1bc99ecad597e835895df58d1d76ab8f855f3e"
737
+ "sha256": "227871625347429067da8b8057412934f366900feba2e4be20749e6cb1dacaa3"
738
738
  },
739
739
  {
740
740
  "path": "src/codex/catalog/native-models.ts",
@@ -742,15 +742,15 @@
742
742
  },
743
743
  {
744
744
  "path": "src/codex/catalog/parsing.ts",
745
- "sha256": "f066aa552765c7bd439516d58b9a5781ed606beb8525d8bb8cc967003441d06a"
745
+ "sha256": "ce7a1e2713ddcb2f2774d9641b014db88e92ca9e70e12cbde82b91ef5c0bd9db"
746
746
  },
747
747
  {
748
748
  "path": "src/codex/catalog/provider-fetch.ts",
749
- "sha256": "8e1fff8e630df65179833156b8affd84643dcb3630218e1cd6d0457a9c1922ef"
749
+ "sha256": "f8f03db782a29e9c995d198780aa2c9279f6b4f81261f296d061788c3cc513e5"
750
750
  },
751
751
  {
752
752
  "path": "src/codex/catalog/sync.ts",
753
- "sha256": "9b2d579bf93782bd4496b1fc5ade7def4f4bc4d55bebee395404da4e0cf857bd"
753
+ "sha256": "616a707b30b3e7c02334da5df2d35e490d758990916272e16fb0c7d5aeb2e38e"
754
754
  },
755
755
  {
756
756
  "path": "src/codex/codex-write-lock.ts",
@@ -762,7 +762,7 @@
762
762
  },
763
763
  {
764
764
  "path": "src/codex/convergence.ts",
765
- "sha256": "4ab2e8828a32a1b6235528d0b9be18b24c7457c485972a00492a4a1675bb9c0a"
765
+ "sha256": "699abdffcecb97ea147e8514755d2d667fffea283d6146298af34bf6fda6502f"
766
766
  },
767
767
  {
768
768
  "path": "src/codex/coordinator-doctor.ts",
@@ -1026,7 +1026,7 @@
1026
1026
  },
1027
1027
  {
1028
1028
  "path": "src/codex/shim.ts",
1029
- "sha256": "f779c71050a1b2c3d6c08c22dbb6597d393a106f20a27566cc949203dbdad837"
1029
+ "sha256": "0ffddafd8f9c10344a213e67363318598894a113ae8e5d383440d935a9c3132f"
1030
1030
  },
1031
1031
  {
1032
1032
  "path": "src/codex/subagent-defaults.ts",
@@ -1098,7 +1098,7 @@
1098
1098
  },
1099
1099
  {
1100
1100
  "path": "src/config.ts",
1101
- "sha256": "13dea38a22b2a511adf830f9ddfcb66ff7e384dc52028891585d2e8572cf75c7"
1101
+ "sha256": "875dc438cd7bbbebe3645abac09b85efcaf5829548da154c09f116bc72329ee8"
1102
1102
  },
1103
1103
  {
1104
1104
  "path": "src/config/atomic-write.ts",
@@ -2026,7 +2026,7 @@
2026
2026
  },
2027
2027
  {
2028
2028
  "path": "src/oauth/callback-server.ts",
2029
- "sha256": "35c7f440cec98b32fd62a065e23370916d0d61118bbb54ae4542a2c74e22669f"
2029
+ "sha256": "3c481c4b26bcaaee246aff098be3fa402e1c0f813adf0f00a4122af92d8d5264"
2030
2030
  },
2031
2031
  {
2032
2032
  "path": "src/oauth/chatgpt.ts",
@@ -2062,7 +2062,7 @@
2062
2062
  },
2063
2063
  {
2064
2064
  "path": "src/oauth/kimi.ts",
2065
- "sha256": "58fb57aab767941d89247e6d38b23e0dc6c4f15693504dc4040a5402265780af"
2065
+ "sha256": "cbbd772bfcf74c1bf6357d494b1b87034c59fbc9ebeda0ccc93e31df431d4a30"
2066
2066
  },
2067
2067
  {
2068
2068
  "path": "src/oauth/kiro-credentials.ts",
@@ -2088,6 +2088,10 @@
2088
2088
  "path": "src/oauth/nous.ts",
2089
2089
  "sha256": "49fb089b01a8a118e575069d2308912b7fd661ef882913193b0e9f4d8df28201"
2090
2090
  },
2091
+ {
2092
+ "path": "src/oauth/open-browser-choice.ts",
2093
+ "sha256": "f4ef3a89b0df0db5d5ae853378029e4983cd3a239cc97a36535350d939066bbb"
2094
+ },
2091
2095
  {
2092
2096
  "path": "src/oauth/pkce.ts",
2093
2097
  "sha256": "8e2798e44de278bc4db99ceaeded806e4d80a141f4d81bd81e4ab6fd22189509"
@@ -2128,6 +2132,10 @@
2128
2132
  "path": "src/providers/api-keys.ts",
2129
2133
  "sha256": "53be86c657fa5f4ad3605f0098312df7186072637f8fe4d16667a64ec853e19e"
2130
2134
  },
2135
+ {
2136
+ "path": "src/providers/auto-compact-budget.ts",
2137
+ "sha256": "574d38c410f507f590a2285086b78b34318b01f233fabf322a6cef0b817b5eb9"
2138
+ },
2131
2139
  {
2132
2140
  "path": "src/providers/base-url-choices.ts",
2133
2141
  "sha256": "f75c050dd851d38fd90335def782699aea43f89930dfff95060fa88d9724a9a6"
@@ -2254,7 +2262,7 @@
2254
2262
  },
2255
2263
  {
2256
2264
  "path": "src/providers/xai-transport.ts",
2257
- "sha256": "469bae4776ca5a4e49c2e1c55b4dcc5c38918d9dd69d179266fdfe79704a644c"
2265
+ "sha256": "1ac6638465ad2b2a36603ee36aa53176474f9c6ca3253db0a43933ed7b20371d"
2258
2266
  },
2259
2267
  {
2260
2268
  "path": "src/reasoning-effort.ts",
@@ -2434,7 +2442,7 @@
2434
2442
  },
2435
2443
  {
2436
2444
  "path": "src/server/auth-cors.ts",
2437
- "sha256": "8b4ce3cd26e0960b71dc671a8b638b2cbd0e760ee5d1fab90aafc64b80d81ba7"
2445
+ "sha256": "8ae32bbf202ae3271139ebe5594f939fa81687316d006da088d064cb44521556"
2438
2446
  },
2439
2447
  {
2440
2448
  "path": "src/server/background-lifecycle.ts",
@@ -2510,7 +2518,7 @@
2510
2518
  },
2511
2519
  {
2512
2520
  "path": "src/server/management/agent-settings-routes.ts",
2513
- "sha256": "0ddaa9c208323d3c59abeed44cb05bf662828238e2ce8d7ad2495992fcf1435a"
2521
+ "sha256": "d41b02b59c071c86f32829ec769110320b2b994efb724cfe2bcf64352645096a"
2514
2522
  },
2515
2523
  {
2516
2524
  "path": "src/server/management/api-access.ts",
@@ -2530,7 +2538,7 @@
2530
2538
  },
2531
2539
  {
2532
2540
  "path": "src/server/management/config-routes.ts",
2533
- "sha256": "7f6c34a95c4c5380c5e1fc48988836fc9e58868111e009621431025768488ced"
2541
+ "sha256": "54b4936caf99e8df7cdf38afe9290615d9c9ae83acaeb6a42da192936bd1706d"
2534
2542
  },
2535
2543
  {
2536
2544
  "path": "src/server/management/context.ts",
@@ -2558,7 +2566,7 @@
2558
2566
  },
2559
2567
  {
2560
2568
  "path": "src/server/management/model-rows.ts",
2561
- "sha256": "8222455775c291b704d7b3fd807de115e7bab596d9183a8f0a0393aa4954054b"
2569
+ "sha256": "ef61d365e116a718e46f73925315c244e0dab66191f1f449e5b281476dc009c7"
2562
2570
  },
2563
2571
  {
2564
2572
  "path": "src/server/management/native-integration-routes.ts",
@@ -2566,7 +2574,7 @@
2566
2574
  },
2567
2575
  {
2568
2576
  "path": "src/server/management/oauth-account-routes.ts",
2569
- "sha256": "b45cb4a832968edbdc3e60c09d8ba0945f430a619907b330fd930c9baeab0943"
2577
+ "sha256": "a59494a55de7245e9bb37578437d085255ed67f8de9c3887042db5284b4691d2"
2570
2578
  },
2571
2579
  {
2572
2580
  "path": "src/server/management/provider-capability-config.ts",
@@ -2574,7 +2582,7 @@
2574
2582
  },
2575
2583
  {
2576
2584
  "path": "src/server/management/provider-routes.ts",
2577
- "sha256": "cab630320710e9931b8ac11aa171610c38d6f98766213ca1053d17f3f20e9831"
2585
+ "sha256": "aea775e35c8da60e4d5e487f0515da72a16df03defb9227c410fcee0b3e2a4b9"
2578
2586
  },
2579
2587
  {
2580
2588
  "path": "src/server/management/request-history-routes.ts",
@@ -2706,7 +2714,7 @@
2706
2714
  },
2707
2715
  {
2708
2716
  "path": "src/server/responses-undeclared-tool-guard.ts",
2709
- "sha256": "63b70defd6739ef416ce8b37190f630ad86799fc480b98dcdf6f7ee4b7ff4e48"
2717
+ "sha256": "6e023eec3dc7b2f81ff00b40c4dd8f2203c48daaff58fd0696a75ce2f4e3c1a6"
2710
2718
  },
2711
2719
  {
2712
2720
  "path": "src/server/responses.ts",
@@ -2738,11 +2746,11 @@
2738
2746
  },
2739
2747
  {
2740
2748
  "path": "src/server/responses/core.ts",
2741
- "sha256": "bf7fe92c249b5d683331a4513eebf63feb2dc32d6c3c9aa92cbb04faaf1c786a"
2749
+ "sha256": "d7c3664cbdfd358c4f7a6be202eae41a612a8c7c8f7bc5f921aaef743221d22b"
2742
2750
  },
2743
2751
  {
2744
2752
  "path": "src/server/responses/empty-completion-guard.ts",
2745
- "sha256": "7df9d606c9072e9b72f4063403c507a0945f83be84188a37a0fe918f966d658f"
2753
+ "sha256": "7ba65c37a10ab1c2a6c57b2d373e4bc601e092c4600aefe33ff97ebe4ead4415"
2746
2754
  },
2747
2755
  {
2748
2756
  "path": "src/server/responses/encrypted-payload.ts",
@@ -2910,7 +2918,7 @@
2910
2918
  },
2911
2919
  {
2912
2920
  "path": "src/types.ts",
2913
- "sha256": "aa0197bb8f8e30eadd0f658a8b5f4ab96a1bedcdda33a38f07d1b2a4437c14ae"
2921
+ "sha256": "71bf1c05ce762244905ad304d674754ac1a759bd0d62164c402fb3c090e21a5d"
2914
2922
  },
2915
2923
  {
2916
2924
  "path": "src/types/accounts.ts",
@@ -2918,11 +2926,11 @@
2918
2926
  },
2919
2927
  {
2920
2928
  "path": "src/types/config.ts",
2921
- "sha256": "473fde6d5b11a71b4e7712b3bc2863841d0ecb327f335c3979328850afa0700f"
2929
+ "sha256": "926fb0ee733520d9f756a1b54192193a255a078bd5fdf5c7511f47d1f033bbf3"
2922
2930
  },
2923
2931
  {
2924
2932
  "path": "src/types/provider.ts",
2925
- "sha256": "6a37ade0ad24851f61705584e0c67f1c64335541ffcd5f337465ddd618249f39"
2933
+ "sha256": "0edbdc5f84f07d6c1412da8cc89958156c96782f3fd9fed082a41d6239f63e18"
2926
2934
  },
2927
2935
  {
2928
2936
  "path": "src/types/request.ts",
@@ -2930,7 +2938,7 @@
2930
2938
  },
2931
2939
  {
2932
2940
  "path": "src/types/tools.ts",
2933
- "sha256": "f78adbc5df38abc606972296ead76bb562ca6d491f5c738a8ecf678b15c8e77c"
2941
+ "sha256": "a5c36b0dc61a88d395783ca613e06caddce54bfb9fc44fe9f24c5fe9f59b7a64"
2934
2942
  },
2935
2943
  {
2936
2944
  "path": "src/types/wire.ts",
@@ -276,10 +276,30 @@ export function parseCallbackInput(input: string): { kind: "url" | "query" | "ra
276
276
 
277
277
  try {
278
278
  const url = new URL(value);
279
+ // Also read the fragment. No provider configured here returns one — every
280
+ // OAuthCallbackFlow asks for response_type=code without response_mode, so
281
+ // the parameters land in the query — but a full URL whose parameters sit in
282
+ // the hash parses as a valid URL with no code, and reading only the query
283
+ // rejects it as "no authorization code found in input". This is the one
284
+ // place a fragment-returning provider would land, and the raw branch below
285
+ // already understands `code#state`.
286
+ //
287
+ // The query wins as a WHOLE when it carries the response, and the two
288
+ // fields are never mixed across collections. Reading `code` and `state`
289
+ // independently would accept `?state=<expected>#code=<other>` — one
290
+ // response assembled from two sources — which is exactly the confusion a
291
+ // state check exists to prevent. An authorization response arrives in one
292
+ // place; treat it that way.
293
+ //
294
+ // Only `code` and `state` are read — never a token. This repo does not
295
+ // implement the implicit grant and a paste field must not become the place
296
+ // it appears.
297
+ const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
298
+ const source = url.searchParams.has("code") ? url.searchParams : fragment;
279
299
  return {
280
300
  kind: "url",
281
- code: url.searchParams.get("code") ?? undefined,
282
- state: url.searchParams.get("state") ?? undefined,
301
+ code: source.get("code") ?? undefined,
302
+ state: source.get("state") ?? undefined,
283
303
  };
284
304
  } catch {
285
305
  // Not a URL - check for query string format
package/src/oauth/kimi.ts CHANGED
@@ -209,7 +209,15 @@ async function pollForToken(deviceCode: string, intervalMs: number, expiresInMs:
209
209
 
210
210
  export async function loginKimi(ctrl: OAuthController): Promise<OAuthCredentials> {
211
211
  const device = await requestDeviceAuthorization();
212
- ctrl.onAuth?.({ url: device.verificationUriComplete, instructions: `Enter code: ${device.userCode}` });
212
+ // `deviceCode` carries the human-typed user code, matching the other device
213
+ // providers. It is also what tells the management login route this is a device
214
+ // flow, so a provider-supplied verification URI is no longer handed to a local
215
+ // browser spawn — the URL stays visible and copyable in every login surface.
216
+ ctrl.onAuth?.({
217
+ url: device.verificationUriComplete,
218
+ instructions: `Enter code: ${device.userCode}`,
219
+ deviceCode: device.userCode,
220
+ });
213
221
  return pollForToken(device.deviceCode, device.intervalMs, device.expiresInMs, ctrl.signal);
214
222
  }
215
223
 
@@ -0,0 +1,26 @@
1
+ import type { OcxConfig } from "../types";
2
+
3
+ /**
4
+ * Whether a login should open a browser on the machine running the proxy.
5
+ *
6
+ * Both login routes share this so the two surfaces cannot drift: a per-request
7
+ * `openBrowser` beats the persisted setting, and the persisted setting beats
8
+ * the historical default. It lives in `src/oauth/` rather than beside either
9
+ * route because `src/codex/auth-api.ts` and
10
+ * `src/server/management/oauth-account-routes.ts` both already import from
11
+ * here, so no new edge is added to the import graph.
12
+ *
13
+ * The default is the important part. An operator who upgrades and configures
14
+ * nothing must see exactly what they saw before, so only an explicit `false`
15
+ * declines — `undefined`, `true`, and a malformed value all open.
16
+ *
17
+ * A malformed request field is ignored rather than rejected on purpose: this is
18
+ * a display preference, and no login should fail because of one.
19
+ */
20
+ export function shouldOpenBrowserForLogin(
21
+ requested: unknown,
22
+ config: Pick<OcxConfig, "oauthOpenBrowser">,
23
+ ): boolean {
24
+ if (typeof requested === "boolean") return requested;
25
+ return config.oauthOpenBrowser !== false;
26
+ }
@@ -0,0 +1,65 @@
1
+ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
2
+ import { redactSecretString } from "../lib/redact";
3
+
4
+ const RESERVED_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
5
+
6
+ function positiveSafeInteger(value: unknown): value is number {
7
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
8
+ }
9
+
10
+ /**
11
+ * Resolve a client-facing soft compaction budget without changing any hard
12
+ * model limit. Configuration and measured input ceilings may only lower the
13
+ * default 90% envelope.
14
+ */
15
+ export function clampAutoCompactTokenLimit(
16
+ contextWindow: number,
17
+ maxInputTokens?: number,
18
+ configuredLimit?: number,
19
+ ): number {
20
+ const candidates = [Math.floor(contextWindow * 0.9), contextWindow];
21
+ if (positiveSafeInteger(maxInputTokens)) candidates.push(maxInputTokens);
22
+ if (positiveSafeInteger(configuredLimit)) candidates.push(configuredLimit);
23
+ return Math.min(...candidates);
24
+ }
25
+
26
+ export type AutoCompactBudgetValidationOptions = Readonly<{
27
+ /** PATCH accepts null for whole-map and per-key deletion. */
28
+ allowTombstones?: boolean;
29
+ /** The canonical ChatGPT provider accepts only exact supported native ids. */
30
+ requireNativeIds?: boolean;
31
+ }>;
32
+
33
+ /** Shared config/load/management boundary for per-model soft budgets. */
34
+ export function modelAutoCompactTokenLimitsConfigError(
35
+ value: unknown,
36
+ options: AutoCompactBudgetValidationOptions = {},
37
+ ): string | null {
38
+ const field = "modelAutoCompactTokenLimits";
39
+ if (value === undefined || (options.allowTombstones && value === null)) return null;
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
41
+ return `${field} must be a plain object${options.allowTombstones ? " or null" : ""}`;
42
+ }
43
+ const prototype = Object.getPrototypeOf(value);
44
+ if (prototype !== Object.prototype && prototype !== null) {
45
+ return `${field} must be a plain object with own properties`;
46
+ }
47
+ for (const [modelId, entry] of Object.entries(value as Record<string, unknown>)) {
48
+ const safeModelId = JSON.stringify(redactSecretString(modelId));
49
+ if (!modelId.trim()) return `${field} keys must be nonblank model ids`;
50
+ if (RESERVED_OBJECT_KEYS.has(modelId)) {
51
+ return `${field} key ${safeModelId} is reserved`;
52
+ }
53
+ if (options.requireNativeIds
54
+ && (modelId.includes("/") || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(modelId))) {
55
+ return `${field} key ${safeModelId} must be an exact supported native model id`;
56
+ }
57
+ if (options.allowTombstones && entry === null) continue;
58
+ if (!positiveSafeInteger(entry)) {
59
+ return `${field}[${safeModelId}] must be a positive safe integer${
60
+ options.allowTombstones ? " or null" : ""
61
+ }`;
62
+ }
63
+ }
64
+ return null;
65
+ }
@@ -4,6 +4,27 @@ import { resolveGithubCopilotTransport } from "./github-copilot-transport";
4
4
 
5
5
  export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
6
6
 
7
+ /** The two hosts that serve xAI's Responses API: the public API and the Grok CLI proxy. */
8
+ const XAI_RESPONSES_HOSTS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
9
+
10
+ /**
11
+ * True when this provider's Responses traffic terminates at xAI itself.
12
+ *
13
+ * Probed 2026-08-22 one field per request: the two hosts accept and refuse exactly the same
14
+ * web_search fields, so they are one dialect rather than two. Matching is exact-host over
15
+ * https, which keeps lookalikes (`api.x.ai.evil.test`) and nonstandard ports out.
16
+ */
17
+ export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
18
+ try {
19
+ const url = new URL(provider.baseUrl);
20
+ return url.protocol === "https:"
21
+ && XAI_RESPONSES_HOSTS.has(url.hostname.toLowerCase())
22
+ && (url.port === "" || url.port === "443");
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
7
28
  export const XAI_GROK_COMPATIBILITY = {
8
29
  version: "0.2.93",
9
30
  userAgent: "opencodex-grok/0.2.93",
@@ -26,6 +26,7 @@ import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode
26
26
  import { providerConfigSeed } from "../providers/derive";
27
27
  import type { OcxConfig, OcxProviderConfig } from "../types";
28
28
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
29
+ import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
29
30
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
30
31
  import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
31
32
 
@@ -565,6 +566,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
565
566
  if (contextOverlayError) return contextOverlayError;
566
567
  delete canonicalCandidate.contextWindow;
567
568
  delete canonicalCandidate.modelContextWindows;
569
+ // User-owned soft compaction policy; it does not alter the canonical transport seed.
570
+ delete canonicalCandidate.modelAutoCompactTokenLimits;
568
571
  const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
569
572
  if (!canonical) {
570
573
  return `provider ${name} must equal the canonical built-in provider seed`;
@@ -607,6 +610,13 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
607
610
  if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
608
611
  const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
609
612
  if (maxInputError) return `provider ${name} ${maxInputError}`;
613
+ const autoCompactError = modelAutoCompactTokenLimitsConfigError(
614
+ raw.modelAutoCompactTokenLimits,
615
+ { requireNativeIds: name === "openai" },
616
+ );
617
+ if (autoCompactError) {
618
+ return `provider ${JSON.stringify(redactSecretString(name))} ${autoCompactError}`;
619
+ }
610
620
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
611
621
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
612
622
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
@@ -707,6 +717,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
707
717
  "models",
708
718
  "contextWindow",
709
719
  "modelContextWindows",
720
+ "modelAutoCompactTokenLimits",
710
721
  "defaultMaxOutputTokens",
711
722
  "modelMaxOutputTokens",
712
723
  "openRouterRouting",
@@ -748,6 +759,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
748
759
  defaultProvider: config.defaultProvider,
749
760
  codexAutoStart: codexAutoStartEnabled(config),
750
761
  websockets: config.websockets,
762
+ // The GUI's browser-open toggle reads and writes this; absent means the
763
+ // historical auto-open behavior.
764
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
751
765
  providers,
752
766
  };
753
767
  }
@@ -60,6 +60,7 @@ import {
60
60
  webSearchCandidateRows,
61
61
  webSearchModelIsRejected,
62
62
  webSearchModelRejection,
63
+ type WebSearchBackend,
63
64
  } from "./web-search-sidecar-options";
64
65
  import { drainAndShutdown } from "../lifecycle";
65
66
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -1118,13 +1119,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1118
1119
  if (field === "webSearchSidecar"
1119
1120
  && (section.model !== undefined || section.backend !== undefined)) {
1120
1121
  const stored = config.claudeCode?.webSearchSidecar;
1121
- const effectiveBackend = section.backend === "anthropic"
1122
- ? "anthropic"
1123
- : section.backend === "openai"
1124
- ? "openai"
1125
- : section.backend === null
1126
- ? config.webSearchSidecar?.backend ?? "openai"
1127
- : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1122
+ // Validate against the SUBMITTED backend across the whole union, not
1123
+ // just openai/anthropic (#2457). allowedBackends above already refused
1124
+ // unknown literals; Array.includes does not narrow, hence the cast.
1125
+ // null keeps its own meaning here — drop the override and inherit the
1126
+ // global backend which is deliberately NOT the sidecar-settings rule.
1127
+ const submittedBackend = section.backend;
1128
+ const effectiveBackend = typeof submittedBackend === "string"
1129
+ && allowedBackends.includes(submittedBackend)
1130
+ ? submittedBackend as WebSearchBackend
1131
+ : submittedBackend === null
1132
+ ? config.webSearchSidecar?.backend ?? "openai"
1133
+ : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1128
1134
  const effectiveModel = section.model === ""
1129
1135
  ? config.webSearchSidecar?.model
1130
1136
  : typeof section.model === "string"
@@ -298,6 +298,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
298
298
  streamMode: config.streamMode ?? "auto",
299
299
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
300
300
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
301
+ // Absent means the historical auto-open, so the GUI can render the toggle
302
+ // without having to know that `undefined` and `true` mean the same thing.
303
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
301
304
  startupHealth: await readStartupHealth(config),
302
305
  codexRuntime: {
303
306
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -382,16 +385,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
382
385
  streamMode?: unknown;
383
386
  appOwnedMemoryBudgetMb?: unknown;
384
387
  codexAccountPickerEnabled?: unknown;
388
+ oauthOpenBrowser?: unknown;
385
389
  };
386
390
  if (body.codexAutoStart === undefined
387
391
  && body.streamMode === undefined
388
392
  && body.appOwnedMemoryBudgetMb === undefined
389
- && body.codexAccountPickerEnabled === undefined) {
390
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, or codexAccountPickerEnabled" }, 400);
393
+ && body.codexAccountPickerEnabled === undefined
394
+ && body.oauthOpenBrowser === undefined) {
395
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, or oauthOpenBrowser" }, 400);
391
396
  }
392
397
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
393
398
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
394
399
  }
400
+ if (body.oauthOpenBrowser !== undefined && typeof body.oauthOpenBrowser !== "boolean") {
401
+ return jsonResponse({ error: "oauthOpenBrowser boolean is required" }, 400);
402
+ }
395
403
  if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
396
404
  return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
397
405
  }
@@ -418,6 +426,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
418
426
  hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
419
427
  codexAccountPickerEnabled: config.codexAccountPickerEnabled,
420
428
  hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
429
+ oauthOpenBrowser: config.oauthOpenBrowser,
430
+ hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
421
431
  };
422
432
  const pickerWasEnabled = codexAccountPickerEnabled(config);
423
433
  let pickerIsEnabled = pickerWasEnabled;
@@ -441,6 +451,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
441
451
  } else if (body.codexAccountPickerEnabled === false) {
442
452
  config.codexAccountPickerEnabled = false;
443
453
  }
454
+ if (typeof body.oauthOpenBrowser === "boolean") {
455
+ config.oauthOpenBrowser = body.oauthOpenBrowser;
456
+ }
444
457
  pickerIsEnabled = codexAccountPickerEnabled(config);
445
458
  (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
446
459
  } catch (error) {
@@ -457,6 +470,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
457
470
  if (previousSettings.hasCodexAccountPickerEnabled) {
458
471
  config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
459
472
  } else delete config.codexAccountPickerEnabled;
473
+ if (previousSettings.hasOauthOpenBrowser) {
474
+ config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
475
+ } else delete config.oauthOpenBrowser;
460
476
  throw error;
461
477
  }
462
478
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
@@ -476,6 +492,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
476
492
  streamMode: config.streamMode ?? "auto",
477
493
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
478
494
  codexAccountPickerEnabled: pickerIsEnabled,
495
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
479
496
  catalogRefreshPending,
480
497
  startupHealth: await readStartupHealth(config),
481
498
  });
@@ -665,9 +682,18 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
665
682
 
666
683
  if (body.webSearch) {
667
684
  const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
668
- const effectiveBackend = body.webSearch.backend === "anthropic"
669
- ? "anthropic"
670
- : body.webSearch.backend === "openai" || body.webSearch.backend === null
685
+ // Validate against the backend the caller SUBMITTED, across the whole
686
+ // union — not just openai/anthropic (#2457). The union check above has
687
+ // already refused unknown literals, so a surviving string is a member;
688
+ // Array.includes does not narrow, hence the cast. Falling back to the
689
+ // stored backend for xai/gemini/exa both rejected legal pairs and
690
+ // accepted illegal ones: a submitted gemini was checked against a stored
691
+ // openai. null means "unset the backend", and unset resolves to openai.
692
+ const submittedBackend = body.webSearch.backend;
693
+ const effectiveBackend = typeof submittedBackend === "string"
694
+ && WEB_SEARCH_BACKENDS_UNION.includes(submittedBackend as never)
695
+ ? submittedBackend as typeof WEB_SEARCH_BACKENDS_UNION[number]
696
+ : submittedBackend === null
671
697
  ? "openai"
672
698
  : config.webSearchSidecar?.backend ?? "openai";
673
699
  const effectiveModel = typeof body.webSearch.model === "string"