@bitkyc08/opencodex 2.21.0 → 2.22.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 (49) hide show
  1. package/AGENTS_INSTALL.md +32 -0
  2. package/README.md +1 -1
  3. package/gui/dist/assets/{index-BOFeam5a.js → index-ClEcVlFO.js} +43 -17
  4. package/gui/dist/assets/index-DQsMZzI5.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/claude/agents-inject.ts +2 -2
  8. package/src/cli/codex-log-guard-doctor.ts +103 -0
  9. package/src/cli/dispatch.ts +7 -1
  10. package/src/cli/help.ts +1 -1
  11. package/src/cli/models.ts +16 -6
  12. package/src/cli/observe.ts +38 -2
  13. package/src/cli/registry.ts +2 -1
  14. package/src/cli/v2.ts +34 -1
  15. package/src/codex/app-server-processes.ts +46 -26
  16. package/src/codex/catalog/effort.ts +49 -1
  17. package/src/codex/catalog/parsing.ts +64 -4
  18. package/src/codex/catalog/provider-fetch.ts +12 -0
  19. package/src/codex/catalog/sync.ts +14 -1
  20. package/src/codex/convergence.ts +2 -0
  21. package/src/codex/inject.ts +3 -3
  22. package/src/codex/log-guard/inspect.ts +506 -0
  23. package/src/codex/log-guard/lock.ts +150 -0
  24. package/src/codex/log-guard/maintenance.ts +403 -0
  25. package/src/codex/log-guard/path-safety.ts +39 -0
  26. package/src/codex/log-guard/policy.ts +44 -0
  27. package/src/codex/log-guard/processes.ts +205 -0
  28. package/src/codex/log-guard/protection.ts +489 -0
  29. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  30. package/src/codex/paths.ts +5 -0
  31. package/src/codex/plugins-doctor.ts +1 -1
  32. package/src/codex/project-config-warnings.ts +2 -2
  33. package/src/generated/compatibility-version.json +72 -32
  34. package/src/providers/antigravity-models.ts +11 -1
  35. package/src/providers/model-discovery.ts +94 -6
  36. package/src/providers/quota.ts +159 -0
  37. package/src/providers/registry.ts +13 -0
  38. package/src/providers/slug-codec.ts +29 -0
  39. package/src/router.ts +12 -4
  40. package/src/routing/capability.ts +32 -17
  41. package/src/server/index.ts +1 -0
  42. package/src/server/management/agent-settings-routes.ts +20 -2
  43. package/src/server/management/context.ts +15 -0
  44. package/src/server/management/model-routes.ts +12 -3
  45. package/src/server/management/storage-log-guard-routes.ts +186 -0
  46. package/src/server/management-api.ts +3 -1
  47. package/src/server/responses/core.ts +13 -0
  48. package/src/types.ts +6 -1
  49. package/gui/dist/assets/index-Xq49CY8F.css +0 -1
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "4100af2a5b5f0eb057a3701bfa09decb431bfdd6933d2314370ff06abf1f8437"
13
+ "sha256": "23d58fd93c12138baca76f044899d82aea45768aa38f5f78c86e957e080be06b"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -318,7 +318,7 @@
318
318
  },
319
319
  {
320
320
  "path": "src/claude/agents-inject.ts",
321
- "sha256": "fb338b3a3290589ae91f52e6fe60bbec85e71cc4eaea8ace8588519a89c26e4d"
321
+ "sha256": "46ebe634b087860bd3c107efe17d4c54289c19845c50c729eeaf53d18b4e8247"
322
322
  },
323
323
  {
324
324
  "path": "src/claude/alias.ts",
@@ -432,6 +432,10 @@
432
432
  "path": "src/cli/claude.ts",
433
433
  "sha256": "7b9b5ea1fbeab889f137a3f889e89350d7205ab77102f6b46357dd825be0e08d"
434
434
  },
435
+ {
436
+ "path": "src/cli/codex-log-guard-doctor.ts",
437
+ "sha256": "e6a62a5c9cbf65e900ec3523598a225f5ccd1238abb8da9cae232d60da6fdabf"
438
+ },
435
439
  {
436
440
  "path": "src/cli/codex-shim-autorestore.ts",
437
441
  "sha256": "9c6787a2a41f696eca1fbd95905a3f7b90718cf044dd21cb5e47f789fea48b44"
@@ -454,7 +458,7 @@
454
458
  },
455
459
  {
456
460
  "path": "src/cli/dispatch.ts",
457
- "sha256": "39c48bd5eb2b3d52db34f7698f482210771eb63e5f990829aaaa2fda7fffebfc"
461
+ "sha256": "3edcfa5957813d525da64ed091f605dc70adda272cf16414d62f42a99ed5ed08"
458
462
  },
459
463
  {
460
464
  "path": "src/cli/doctor.ts",
@@ -466,7 +470,7 @@
466
470
  },
467
471
  {
468
472
  "path": "src/cli/help.ts",
469
- "sha256": "55c0ee0ad7f3261bb23dbc7e08d208e94da60343c2f7d73e626110d5bbaf1b26"
473
+ "sha256": "829453ef2aac53bc15825bc6a47320a3a0a5a9989f616fbb62e76fdfa01891ee"
470
474
  },
471
475
  {
472
476
  "path": "src/cli/index.ts",
@@ -502,11 +506,11 @@
502
506
  },
503
507
  {
504
508
  "path": "src/cli/models.ts",
505
- "sha256": "f424705f5abd97977041aeb76713db86a5869529c86d35b9a51295831c58238e"
509
+ "sha256": "a7ddece988af02fc1ebbf4f97bbed882ab70713048c65b094b62bba42b99036f"
506
510
  },
507
511
  {
508
512
  "path": "src/cli/observe.ts",
509
- "sha256": "bacc427239130ab80e94d6450b946d50011035e7d50015f323cc3cb352a0d101"
513
+ "sha256": "c403189847bbeb114e87f133e6e3612c7e3f483e8c64e579152d54f4fc83f462"
510
514
  },
511
515
  {
512
516
  "path": "src/cli/opencode.ts",
@@ -526,7 +530,7 @@
526
530
  },
527
531
  {
528
532
  "path": "src/cli/registry.ts",
529
- "sha256": "a41d54ece43183ef0f2e20cdbbec2866b470d007f4a11d5884e0afa87d52d5a8"
533
+ "sha256": "160e10cb3a5797cb7918261d804fc09d7eb264a3fbddbc9e8735eabf4bad87e7"
530
534
  },
531
535
  {
532
536
  "path": "src/cli/root.ts",
@@ -566,7 +570,7 @@
566
570
  },
567
571
  {
568
572
  "path": "src/cli/v2.ts",
569
- "sha256": "28dc0d12a8edcf49821655e6dc242bbab72afdfec3a67e84143a08fb42363d64"
573
+ "sha256": "c82727ac194d3e21494b73f3ca4b8c32c86bacea1f39f489477f9cc0ddf8491f"
570
574
  },
571
575
  {
572
576
  "path": "src/clients/config-export.ts",
@@ -618,7 +622,7 @@
618
622
  },
619
623
  {
620
624
  "path": "src/codex/app-server-processes.ts",
621
- "sha256": "465d55f46f7fe7e2387712a708f52b20471b86cb1ba7e860d37475d4af3e7a4b"
625
+ "sha256": "28c7fb91d8fd5c2332738e512b9d87c53ea09843191c145b0801d912e124fe9e"
622
626
  },
623
627
  {
624
628
  "path": "src/codex/app-server-restart-service.ts",
@@ -670,7 +674,7 @@
670
674
  },
671
675
  {
672
676
  "path": "src/codex/catalog/effort.ts",
673
- "sha256": "174d3b2d850be0ec3e95d13b53ec50a34d5791ddeee8d9d6fc2d0f30ac69f0b3"
677
+ "sha256": "ac208a47306bd0081e7be7a73ff483296d28ca0e23427218ce999dc53a6071c9"
674
678
  },
675
679
  {
676
680
  "path": "src/codex/catalog/filesystem-evidence.ts",
@@ -690,15 +694,15 @@
690
694
  },
691
695
  {
692
696
  "path": "src/codex/catalog/parsing.ts",
693
- "sha256": "36b9fa4face3877de4495809cae18f3ecd007e7b12e59a1dbfff8e222e884f85"
697
+ "sha256": "0998690562c0f00554630f2070205f6864d8f573ffcaa809d0def6c67f21be6c"
694
698
  },
695
699
  {
696
700
  "path": "src/codex/catalog/provider-fetch.ts",
697
- "sha256": "1fa6525a53b7bed96b8a142eebb78f83b58a5029641e8fd983fb258db1445451"
701
+ "sha256": "472501e50bc4c82cfc0df13fd534b9ca9b391f3207b8bee74543d6cc999c3d30"
698
702
  },
699
703
  {
700
704
  "path": "src/codex/catalog/sync.ts",
701
- "sha256": "8d0b1819dd73650ab99d1c9b07fdc0bd12c9db197e8554888afe4abfa1993ac8"
705
+ "sha256": "d586a2d468cf4120da088f0bdc3088751c0e7015a9fc5545dd5584804d1e3525"
702
706
  },
703
707
  {
704
708
  "path": "src/codex/codex-write-lock.ts",
@@ -710,7 +714,7 @@
710
714
  },
711
715
  {
712
716
  "path": "src/codex/convergence.ts",
713
- "sha256": "9d9e6bb02d7ec2f87de2729708b311f4bf1eb0047e74fd7cd1dc00f41cbf069c"
717
+ "sha256": "ffe1a891c51b33ace33d99bb11ca36015544189f8c4bb1966c2bb08469a163fb"
714
718
  },
715
719
  {
716
720
  "path": "src/codex/custom-model-catalog-migration.ts",
@@ -770,7 +774,7 @@
770
774
  },
771
775
  {
772
776
  "path": "src/codex/inject.ts",
773
- "sha256": "9d83aedcc214dba9c850fc904f38395d47df429a0587a677125a75b07d5f19ef"
777
+ "sha256": "fd020e956a6b7b50d256c9bdd7d585172cc7662993e4eb21fe2c55b0e2387e37"
774
778
  },
775
779
  {
776
780
  "path": "src/codex/injected-marker.ts",
@@ -792,6 +796,38 @@
792
796
  "path": "src/codex/journal.ts",
793
797
  "sha256": "f90fcb039ff2ca8ddbdfe8e9101b431e4c8d864cc18923bbbc98c671b1b7adb0"
794
798
  },
799
+ {
800
+ "path": "src/codex/log-guard/inspect.ts",
801
+ "sha256": "b2b4153ff668d3f3f89322e21bee5af375fa7f92d8f4039e8db88f1f040c8357"
802
+ },
803
+ {
804
+ "path": "src/codex/log-guard/lock.ts",
805
+ "sha256": "b4ce32198272163541652b9864f892eeff8f681e7807de67c321c6c8901b4384"
806
+ },
807
+ {
808
+ "path": "src/codex/log-guard/maintenance.ts",
809
+ "sha256": "f11d9c7d774719715a8b3b33e7573ab09eca33fa4f57e853479a4fb5d8617786"
810
+ },
811
+ {
812
+ "path": "src/codex/log-guard/path-safety.ts",
813
+ "sha256": "8df1c28dafbe16417dcc474fe06c63ec0d6b8367d47971569b4a80a17a614e16"
814
+ },
815
+ {
816
+ "path": "src/codex/log-guard/policy.ts",
817
+ "sha256": "ba282dba9271822d1778256f28dd4e8087f83dd282ce63dc06539e46db0da58b"
818
+ },
819
+ {
820
+ "path": "src/codex/log-guard/processes.ts",
821
+ "sha256": "a1388d9adfedc5918d0fdce83ae1f0577ede9a23f07789ba46ced5351a05edb1"
822
+ },
823
+ {
824
+ "path": "src/codex/log-guard/protection.ts",
825
+ "sha256": "92c1e86ec698bba7953f1e0d389383e76d1c7f05206d6beaa5f1eccc451e9b9b"
826
+ },
827
+ {
828
+ "path": "src/codex/log-guard/sqlite-errors.ts",
829
+ "sha256": "aa4335d71cf642e9b98b066af9204e6d66fff49a7f95b14c50b4c82abd1cce6f"
830
+ },
795
831
  {
796
832
  "path": "src/codex/main-account-cache.ts",
797
833
  "sha256": "1fe336e582bce085b1e37d8a1925b60e91ec667706f80d939974d41062673f6e"
@@ -866,7 +902,7 @@
866
902
  },
867
903
  {
868
904
  "path": "src/codex/paths.ts",
869
- "sha256": "8137c05a2ef4d15094d0dd53b8ebe2d2324dfd005f4b74ecd390a8454c67f2c2"
905
+ "sha256": "f14512fec482d38044172cee72532372bb32757f964b4c2c90f1349c4d07c32c"
870
906
  },
871
907
  {
872
908
  "path": "src/codex/plan.ts",
@@ -874,7 +910,7 @@
874
910
  },
875
911
  {
876
912
  "path": "src/codex/plugins-doctor.ts",
877
- "sha256": "8e43ea761676d4e93f53461f689995143c4c123e7c83d7948906af0a29cd0540"
913
+ "sha256": "51344b58e99a143d2c7d397481e5b2a26c86ab0a1c61db5b153068793e4d3cab"
878
914
  },
879
915
  {
880
916
  "path": "src/codex/pool-rotation.ts",
@@ -882,7 +918,7 @@
882
918
  },
883
919
  {
884
920
  "path": "src/codex/project-config-warnings.ts",
885
- "sha256": "801d2308cef14ee36ae7740a1a94d54876331b6878d5b2c23a1a1f4bdfd6d93a"
921
+ "sha256": "b7543a8d5bd7ff439398480360d9f8b5840ade77e1db19155994d298136ef868"
886
922
  },
887
923
  {
888
924
  "path": "src/codex/prompt-journal.ts",
@@ -1962,7 +1998,7 @@
1962
1998
  },
1963
1999
  {
1964
2000
  "path": "src/providers/antigravity-models.ts",
1965
- "sha256": "005e17707703842d91d9b18aa6f28375bda6b49efff07cb7642c597ee0bcdc8b"
2001
+ "sha256": "1382f985bf4fdf525fb4425d5040e7244460f441f492b22ddc493226e5b778d1"
1966
2002
  },
1967
2003
  {
1968
2004
  "path": "src/providers/api-keys.ts",
@@ -2018,7 +2054,7 @@
2018
2054
  },
2019
2055
  {
2020
2056
  "path": "src/providers/model-discovery.ts",
2021
- "sha256": "298d6f169883bfd5de3b6059fb3ae71c07328d0cec85cf4e188b80e78a50edc6"
2057
+ "sha256": "bcf2a1b01dc8cdad91bd2b2e3ee935107e203905649985ee57691d33e3fed9c9"
2022
2058
  },
2023
2059
  {
2024
2060
  "path": "src/providers/model-rename-migration.ts",
@@ -2058,11 +2094,11 @@
2058
2094
  },
2059
2095
  {
2060
2096
  "path": "src/providers/quota.ts",
2061
- "sha256": "fa2d207d569be34fd50fd72b90389e3e4147c5101cce4318629f3f21172a5e9a"
2097
+ "sha256": "828c4304b6b67d2a71d85141f8da0d2a59453f782c3de46a1905e68903750375"
2062
2098
  },
2063
2099
  {
2064
2100
  "path": "src/providers/registry.ts",
2065
- "sha256": "8e16a0051359debc48b59ade82901c64a0bfecf59edc69bd6536f6dd3fa66f1b"
2101
+ "sha256": "363d273b5dbf67ed5b3491578f18c5d408d10b546a21a4a7cad3697606a47047"
2066
2102
  },
2067
2103
  {
2068
2104
  "path": "src/providers/request-pacing.ts",
@@ -2074,7 +2110,7 @@
2074
2110
  },
2075
2111
  {
2076
2112
  "path": "src/providers/slug-codec.ts",
2077
- "sha256": "01a82a23ee8046e280d9fdc85769233ccc7bc67d1866fe475f4f2efe33136aca"
2113
+ "sha256": "733bfa8cb48e8e9baa678f5aefbf579f7eaa5eb05b16433779bd26e39b41ffe8"
2078
2114
  },
2079
2115
  {
2080
2116
  "path": "src/providers/static-model-discovery.ts",
@@ -2134,7 +2170,7 @@
2134
2170
  },
2135
2171
  {
2136
2172
  "path": "src/router.ts",
2137
- "sha256": "7366badf3a6ace0ae2f044bcab0872e6179bb7bc2f5c7d5a5e35d5655a26d39f"
2173
+ "sha256": "574c51a5c95350217d72b6fa82b06806251e6706265866e909fec729882f82fb"
2138
2174
  },
2139
2175
  {
2140
2176
  "path": "src/routing/analytics.ts",
@@ -2142,7 +2178,7 @@
2142
2178
  },
2143
2179
  {
2144
2180
  "path": "src/routing/capability.ts",
2145
- "sha256": "02349faa5d03510aa8b6a3e2b2eebc6f4afe15ffe424cf5a49a28b643158f4c1"
2181
+ "sha256": "00606e95e636abd8612bb6825c6cb8c2a62ca1eb396ebebba64501f34cd0e060"
2146
2182
  },
2147
2183
  {
2148
2184
  "path": "src/routing/compatibility/assemble.ts",
@@ -2286,7 +2322,7 @@
2286
2322
  },
2287
2323
  {
2288
2324
  "path": "src/server/index.ts",
2289
- "sha256": "e909c5777cac73894273ab6ce9a0e635dc55a74de726cc6a6f71c4409a695ba2"
2325
+ "sha256": "2745fca63c657742f90c22f2a251a3bf1702aca0c5c5b24cfab9d01dad39cd93"
2290
2326
  },
2291
2327
  {
2292
2328
  "path": "src/server/lifecycle.ts",
@@ -2306,7 +2342,7 @@
2306
2342
  },
2307
2343
  {
2308
2344
  "path": "src/server/management-api.ts",
2309
- "sha256": "0a513030e2b730393d86c87ad237f1e9f35e64586d8736c91c69fed6aaabb60d"
2345
+ "sha256": "4736d3b1030eff60d06b22e3339bb25f66062d9d3bf0c2becc701f5b10bcc032"
2310
2346
  },
2311
2347
  {
2312
2348
  "path": "src/server/management-auth.ts",
@@ -2314,7 +2350,7 @@
2314
2350
  },
2315
2351
  {
2316
2352
  "path": "src/server/management/agent-settings-routes.ts",
2317
- "sha256": "2e470534a00bfe47ca3c76688373c72650c60032d7d1ff56d3b57f3cadff3907"
2353
+ "sha256": "d22e53a8f698e29c78905944a8c696522a0fe468bda72da4fb5f6491aa0b0cc6"
2318
2354
  },
2319
2355
  {
2320
2356
  "path": "src/server/management/api-access.ts",
@@ -2338,7 +2374,7 @@
2338
2374
  },
2339
2375
  {
2340
2376
  "path": "src/server/management/context.ts",
2341
- "sha256": "3c5d032849dbbe1036ceb748c5eb2f8b4bcf63816d5d8878e953ee3202c08f91"
2377
+ "sha256": "85564a590d77fb49f7aab7a0cf4729ca6d704bcef4b3a68b5a437d87809aa212"
2342
2378
  },
2343
2379
  {
2344
2380
  "path": "src/server/management/integration-routes.ts",
@@ -2358,7 +2394,7 @@
2358
2394
  },
2359
2395
  {
2360
2396
  "path": "src/server/management/model-routes.ts",
2361
- "sha256": "2a4b0b72eec0e4487831e9236c4f911214052bc1b3d195ee46db109b3891e003"
2397
+ "sha256": "de001901177f757c8a611f808e63ae55f64c6d81a9a5db6f29ac6bbb28f5c6f9"
2362
2398
  },
2363
2399
  {
2364
2400
  "path": "src/server/management/model-rows.ts",
@@ -2400,6 +2436,10 @@
2400
2436
  "path": "src/server/management/sidebar-routes.ts",
2401
2437
  "sha256": "0926c61288d6344456516c4208ea788192739d5d4c47c5566baf861313b75301"
2402
2438
  },
2439
+ {
2440
+ "path": "src/server/management/storage-log-guard-routes.ts",
2441
+ "sha256": "76491da8b595410593405d8ae7fbfa0b870653277f801b75c01086fa69ca3445"
2442
+ },
2403
2443
  {
2404
2444
  "path": "src/server/management/sync-response.ts",
2405
2445
  "sha256": "580cd5b67c1b1c7ac2a25262da00dc4dd31c7959e506622fc13a2f240d800786"
@@ -2514,7 +2554,7 @@
2514
2554
  },
2515
2555
  {
2516
2556
  "path": "src/server/responses/core.ts",
2517
- "sha256": "d60f902c7c496690e701e2bd0217adb1df8b7f2fcb4c43cc9ec9afc74d8a80b8"
2557
+ "sha256": "8a4539f6e9fd8a7016bc7562316525f7ce0f236481ec51c63d14a6ca253d1786"
2518
2558
  },
2519
2559
  {
2520
2560
  "path": "src/server/responses/empty-completion-guard.ts",
@@ -2674,7 +2714,7 @@
2674
2714
  },
2675
2715
  {
2676
2716
  "path": "src/types.ts",
2677
- "sha256": "59e811fabf37cd8b40621c6fcf7992c595ad645ddda3a6e6583ed6634fec6b84"
2717
+ "sha256": "a1aa12232d1424a93598402de73b7455bf5d33892c67ef0772b8c60a44f64560"
2678
2718
  },
2679
2719
  {
2680
2720
  "path": "src/update/badge.ts",
@@ -331,7 +331,17 @@ export function parseAntigravityAvailableModels(
331
331
  out.push({
332
332
  id,
333
333
  ...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
334
- inputModalities: info.supportsImages === true ? ["text", "image"] : ["text"],
334
+ // Tri-state, deliberately not a ternary: `true` asserts image support,
335
+ // `false` asserts against it, and ABSENT is unknown. Collapsing absent into
336
+ // `["text"]` let routing read it as a confident `image: false` (#1796). The
337
+ // strict catalog still receives its `["text"]` compatibility default
338
+ // downstream via ensureStrictCatalogFields; only the routing-evidence
339
+ // channel stays honest about what was never asserted.
340
+ ...(info.supportsImages === true
341
+ ? { inputModalities: ["text", "image"] as string[] }
342
+ : info.supportsImages === false
343
+ ? { inputModalities: ["text"] as string[] }
344
+ : {}),
335
345
  });
336
346
  }
337
347
  return out;
@@ -99,7 +99,12 @@ export function providerModelDiscoverySpecError(spec: ProviderModelDiscoverySpec
99
99
  return "discovery path must be a query-free relative/origin path";
100
100
  }
101
101
  if (path.includes("\\")) return "discovery path must use forward slashes";
102
- if (path.split("/").some(segment => segment.replace(/%2e/gi, ".") === "..")) {
102
+ const segments = path.split("/");
103
+ if (segments.some((segment, index) => {
104
+ const decoded = segment.replace(/%2e/gi, ".");
105
+ if (decoded !== "..") return false;
106
+ return index !== 0 || segments.filter(s => s.replace(/%2e/gi, ".") === "..").length !== 1;
107
+ })) {
103
108
  return "discovery path must not contain parent-directory segments";
104
109
  }
105
110
  }
@@ -326,12 +331,83 @@ export function extractModelEnvelopeRows(
326
331
  }
327
332
 
328
333
  /** Validate, bound, deduplicate, and declaratively filter OpenAI `{data:[...]}` or top-level arrays (Together `#617`). */
334
+ /**
335
+ * Metadata a sibling `models[]` array may contribute to an ALREADY-ADMITTED
336
+ * `data[]` row (#1797).
337
+ *
338
+ * llama.cpp serves a dual-envelope body: an Ollama-style `models[]` array
339
+ * carrying `capabilities` alongside the OpenAI-style `data[]` array carrying
340
+ * `meta`, so the two halves of one model's metadata never meet and a server
341
+ * that truthfully advertises "multimodal" produced an image-blind row.
342
+ *
343
+ * Two boundaries make this safe, and both were added after review found the
344
+ * first attempt unsound:
345
+ *
346
+ * 1. It runs AFTER admission filtering. Enriching first let a sibling supply
347
+ * the exact field a provider filter requires — reproduced against the real
348
+ * Chutes policy, where a row lacking `supported_features: ["tools"]` was
349
+ * admitted once a same-id sibling provided it. Enrichment may change what is
350
+ * KNOWN about a model, never WHICH models are published.
351
+ * 2. Only the capability keys #1797 needs are copied. A blanket "fill every
352
+ * absent key" made the untrusted `models[]` array a way into any field the
353
+ * pipeline consumes.
354
+ *
355
+ * The list deliberately EXCLUDES `supported_features` and `features`, even
356
+ * though both are capability-shaped: they are the two keys real provider
357
+ * filters test (`registry.ts:1591` requires `supported_features` to contain
358
+ * "tools"; `registry.ts:1883` tests `features.tool_use`). Ordering already
359
+ * prevents a sibling from flipping an admission verdict, but a key that is
360
+ * both enrichable and filter-relevant is one refactor away from becoming a
361
+ * bypass again. #1797 does not need them.
362
+ */
363
+ const SIBLING_ENRICHABLE_KEYS = new Set(["capabilities", "modalities", "input_modalities"]);
364
+
365
+ type SiblingIndex = Map<string, Record<string, unknown> | null>;
366
+
367
+ function buildSiblingIndex(value: unknown, limit: number): SiblingIndex | null {
368
+ const record = plainObject(value);
369
+ const sibling = record?.models;
370
+ if (!Array.isArray(sibling) || sibling.length === 0 || sibling.length > limit) return null;
371
+
372
+ const byId: SiblingIndex = new Map();
373
+ for (const raw of sibling) {
374
+ const entry = plainObject(raw);
375
+ if (!entry) continue;
376
+ for (const key of ["id", "model", "name"]) {
377
+ const id = entry[key];
378
+ if (typeof id !== "string" || id.length === 0) continue;
379
+ // `null` marks an ambiguous id: two sibling entries claim it, so neither
380
+ // can be attributed with confidence and both are ignored.
381
+ byId.set(id, byId.has(id) && byId.get(id) !== entry ? null : entry);
382
+ }
383
+ }
384
+ return byId.size > 0 ? byId : null;
385
+ }
386
+
387
+ function enrichAdmittedModel(item: ProviderModelsApiItem, siblings: SiblingIndex): ProviderModelsApiItem {
388
+ const extra = siblings.get(item.id);
389
+ if (!extra) return item;
390
+ let merged: Record<string, unknown> | null = null;
391
+ for (const key of SIBLING_ENRICHABLE_KEYS) {
392
+ if (!(key in extra) || key in item) continue;
393
+ merged ??= { ...(item as Record<string, unknown>) };
394
+ merged[key] = extra[key];
395
+ }
396
+ return (merged ?? item) as ProviderModelsApiItem;
397
+ }
398
+
399
+ function plainObject(value: unknown): Record<string, unknown> | null {
400
+ return typeof value === "object" && value !== null && !Array.isArray(value)
401
+ ? value as Record<string, unknown>
402
+ : null;
403
+ }
329
404
  export function extractProviderModelItems(
330
405
  value: unknown,
331
406
  discovery: ResolvedProviderModelDiscovery,
332
407
  ): ProviderModelItemsResult {
333
408
  const limit = positiveIntegerAtMost(discovery.maxModels, MODEL_DISCOVERY_MAX_MODELS);
334
409
  let data: unknown[];
410
+ let siblings: SiblingIndex | null = null;
335
411
  if (Array.isArray(value)) {
336
412
  // Together-style top-level /models arrays. Catalog discovery must not treat a stray
337
413
  // `models` key on openai-chat responses as valid — only `data` envelopes or top-level arrays.
@@ -341,6 +417,7 @@ export function extractProviderModelItems(
341
417
  const envelope = extractModelEnvelopeRows(value, discovery.maxModels, ["data"]);
342
418
  if (!envelope.ok) return envelope;
343
419
  data = envelope.rows;
420
+ siblings = buildSiblingIndex(value, limit);
344
421
  }
345
422
 
346
423
  const items: ProviderModelsApiItem[] = [];
@@ -351,11 +428,22 @@ export function extractProviderModelItems(
351
428
  }
352
429
  const id = (raw as { id?: unknown }).id;
353
430
  if (!isValidModelDiscoveryModelId(id)) return { ok: false, reason: "invalid_shape" };
354
- const normalizedId = id;
355
- const item = raw as ProviderModelsApiItem;
356
- if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(normalizedId)) continue;
357
- seen.add(normalizedId);
358
- items.push(item);
431
+ const prefix = discovery.spec?.stripIdPrefix;
432
+ let finalId = id;
433
+ if (prefix && finalId.startsWith(prefix)) {
434
+ finalId = finalId.slice(prefix.length);
435
+ if (!isValidModelDiscoveryModelId(finalId)) continue;
436
+ }
437
+ const item = finalId === id ? raw as ProviderModelsApiItem : { ...(raw as ProviderModelsApiItem), id: finalId };
438
+ // Admission is decided on the ORIGINAL `data[]` row, before any sibling
439
+ // enrichment. Merging first let a `models[]` entry supply the very field a
440
+ // provider filter requires — reproduced against the real Chutes policy,
441
+ // where a row lacking `supported_features: ["tools"]` was admitted once a
442
+ // same-id sibling provided it. Enrichment must never change WHICH models
443
+ // are published, only what is known about an already-admitted one.
444
+ if (!providerModelMatchesDiscoveryFilter(item, discovery.spec?.filter) || seen.has(finalId)) continue;
445
+ seen.add(finalId);
446
+ items.push(siblings ? enrichAdmittedModel(item, siblings) : item);
359
447
  }
360
448
  return { ok: true, items, rawCount: data.length };
361
449
  }
@@ -38,6 +38,11 @@ const REQUEST_TIMEOUT_MS = 8_000;
38
38
  export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
39
39
  const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
40
40
  const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
41
+ const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
42
+ const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
43
+ const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
44
+ const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
45
+ const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
41
46
  const A6API_BASE_URL = "https://api.a6api.com";
42
47
  const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
43
48
  const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
@@ -1510,6 +1515,12 @@ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
1510
1515
  return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
1511
1516
  }
1512
1517
 
1518
+ function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
1519
+ const normalized = normalizedBaseUrl(baseUrl);
1520
+ // OAuth preset points at the API root; the Provider-API preset at /provider/v1.
1521
+ return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
1522
+ }
1523
+
1513
1524
  /** Prefer the nested `data` shell when the outer object is only an envelope. */
1514
1525
  function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
1515
1526
  const body = asRecord(value);
@@ -1631,6 +1642,145 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
1631
1642
  return quota ? report(provider, "kimi:usages", quota) : null;
1632
1643
  }
1633
1644
 
1645
+ /**
1646
+ * Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
1647
+ * normalized to a percent with an optional reset timestamp.
1648
+ */
1649
+ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
1650
+ const row = asRecord(value);
1651
+ if (!row) return null;
1652
+ const cap = toFiniteNumber(row.cap);
1653
+ const used = toFiniteNumber(row.used);
1654
+ if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
1655
+ const percent = normalizePercent((used / cap) * 100);
1656
+ if (percent === undefined) return null;
1657
+ const resetAt = quotaResetAt(row);
1658
+ return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
1659
+ }
1660
+
1661
+ /** Soft-fail GET returning a parsed record, or null when unavailable. */
1662
+ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
1663
+ try {
1664
+ const response = await fetch(url, {
1665
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1666
+ redirect: "error",
1667
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1668
+ });
1669
+ if (!response.ok) return null;
1670
+ return asRecord(await readQuotaJson(response));
1671
+ } catch {
1672
+ return null;
1673
+ }
1674
+ }
1675
+
1676
+ /**
1677
+ * Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
1678
+ * Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
1679
+ * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
1680
+ */
1681
+ async function fetchCommandCodeSpend(
1682
+ bearer: string,
1683
+ credits: Record<string, unknown> | null,
1684
+ orgQuery: string,
1685
+ ): Promise<ProviderQuotaCreditsUsd | undefined> {
1686
+ if (!credits) return undefined;
1687
+ const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1688
+ const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
1689
+ const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
1690
+ // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
1691
+ // remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
1692
+ if (!periodStart) return undefined;
1693
+ const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
1694
+ const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
1695
+ const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1696
+ const summary = asRecord(summaryBody?.data) ?? summaryBody;
1697
+ const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
1698
+ if (used === undefined || used < 0) return undefined;
1699
+ const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
1700
+ .map(value => toFiniteNumber(value))
1701
+ .filter((value): value is number => value !== undefined);
1702
+ // Field presence is what separates a real balance from absent data: an exhausted
1703
+ // all-zero account still reports remaining=0, while no remaining-credit field at
1704
+ // all means there is nothing to meter.
1705
+ if (pools.length === 0) return undefined;
1706
+ const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
1707
+ const limit = used + remaining;
1708
+ const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
1709
+ // Purchased credits roll over past the subscription period end, so an expiry is
1710
+ // only truthful when the aggregate contains no non-expiring purchased pool.
1711
+ const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
1712
+ return percent === undefined
1713
+ ? undefined
1714
+ : {
1715
+ used,
1716
+ limit,
1717
+ remaining,
1718
+ percent,
1719
+ ...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
1720
+ };
1721
+ }
1722
+
1723
+ /** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
1724
+ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
1725
+ if (config.authMode === "oauth") {
1726
+ try {
1727
+ return await getValidAccessToken("command-code");
1728
+ } catch {
1729
+ return null;
1730
+ }
1731
+ }
1732
+ // ACTIVE key only: a quota bar for a different account than the one routing
1733
+ // requests is a wrong meter, not a helpful one.
1734
+ return resolveEnvValue(config.apiKey)?.trim() || null;
1735
+ }
1736
+
1737
+ /**
1738
+ * Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
1739
+ * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
1740
+ * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
1741
+ */
1742
+ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1743
+ // Never release credentials to a user-edited or lookalike provider host.
1744
+ if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
1745
+ const bearer = await resolveCommandCodeQuotaBearer(config);
1746
+ if (!bearer) return null;
1747
+ const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
1748
+ const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
1749
+ const org = asRecord(whoami?.org);
1750
+ const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
1751
+ const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
1752
+ const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1753
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1754
+ redirect: "error",
1755
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1756
+ });
1757
+ if (!response.ok) {
1758
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1759
+ ? TERMINAL_QUOTA_FAILURE
1760
+ : null;
1761
+ }
1762
+ const raw = asRecord(await readQuotaJson(response));
1763
+ const body = asRecord(raw?.data) ?? raw;
1764
+ const credits = asRecord(body?.credits);
1765
+ const limits = asRecord(body?.windowLimits);
1766
+ if (!credits && !limits) return null;
1767
+ const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
1768
+ const weekly = parseCommandCodeWindow(limits?.weekly);
1769
+ const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
1770
+ return report(provider, "command-code:credits", {
1771
+ ...(fiveHour ? {
1772
+ fiveHourPercent: fiveHour.percent,
1773
+ ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
1774
+ } : {}),
1775
+ ...(weekly ? {
1776
+ weeklyPercent: weekly.percent,
1777
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
1778
+ } : {}),
1779
+ ...(creditsUsd ? { creditsUsd } : {}),
1780
+ updatedAt: Date.now(),
1781
+ });
1782
+ }
1783
+
1634
1784
  /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
1635
1785
  async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
1636
1786
  let accessToken: string;
@@ -1919,6 +2069,15 @@ async function maybeFetchProviderQuota(
1919
2069
  if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
1920
2070
  return fetchKimiQuota(name, provider);
1921
2071
  }
2072
+ // OAuth account login or Provider-API key only; forward/local modes carry no
2073
+ // credential of ours on the canonical host.
2074
+ if (provider.authMode === "oauth" && name === "command-code") {
2075
+ return fetchCommandCodeQuota(name, provider);
2076
+ }
2077
+ if ((provider.authMode ?? "key") === "key" && name === "commandcode"
2078
+ && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2079
+ return fetchCommandCodeQuota(name, provider);
2080
+ }
1922
2081
  if ((provider.authMode ?? "key") === "key" && name === "opencode-go") {
1923
2082
  return fetchOpenCodeGoQuota(name, provider);
1924
2083
  }