@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912

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 (166) hide show
  1. package/gui/dist/assets/index-D_t6sCWs.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -0,0 +1,321 @@
1
+ import type { OcxAccountPoolRotationStrategy } from "../types";
2
+ import type { GenerationContext } from "../lib/state-store-sweeper";
3
+
4
+ export const POOL_KEY_CODEX = "codex";
5
+ export const POOL_KEY_ANTHROPIC = "anthropic";
6
+
7
+ /**
8
+ * Pool key for a generic OAuth provider. Namespaced so a provider can never collide
9
+ * with the two dedicated kinds, and so `reconcilePoolRotationState` can recognise
10
+ * the entry as sweepable rather than skipping it as unknown.
11
+ */
12
+ export function genericPoolKey(providerName: string): string {
13
+ return `generic:${providerName}`;
14
+ }
15
+
16
+ interface SelectionState {
17
+ activeKey?: string;
18
+ successes: number;
19
+ currentWeights: Map<string, number>;
20
+ }
21
+
22
+ const selectionState = new Map<string, SelectionState>();
23
+ let lastReconciledGeneration = 0;
24
+
25
+ const DEFAULT_STICKY_LIMIT = 1;
26
+ const MIN_STICKY_LIMIT = 1;
27
+ const MAX_STICKY_LIMIT = 100;
28
+ const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
29
+ const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
30
+
31
+ /** Selection order for an account with no stored preference: one flat tier. */
32
+ export const DEFAULT_ACCOUNT_PRIORITY = 0;
33
+ export const MIN_ACCOUNT_PRIORITY = -100;
34
+ export const MAX_ACCOUNT_PRIORITY = 100;
35
+
36
+ /** Strict parse for management APIs — returns null instead of defaulting. */
37
+ export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
38
+ if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
39
+ return raw as OcxAccountPoolRotationStrategy;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /** Strict parse for management APIs — returns null instead of defaulting. */
45
+ export function parseAccountPoolStickyLimit(raw: unknown): number | null {
46
+ if (typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_STICKY_LIMIT && raw <= MAX_STICKY_LIMIT) {
47
+ return raw;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ export function normalizeAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy {
53
+ return parseAccountPoolStrategy(raw) ?? DEFAULT_STRATEGY;
54
+ }
55
+
56
+ export function normalizeAccountPoolStickyLimit(raw: unknown): number {
57
+ return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
58
+ }
59
+
60
+ /** Strict parse for management APIs — returns null instead of defaulting. */
61
+ export function parseAccountPriority(raw: unknown): number | null {
62
+ if (
63
+ typeof raw === "number"
64
+ && Number.isInteger(raw)
65
+ && raw >= MIN_ACCOUNT_PRIORITY
66
+ && raw <= MAX_ACCOUNT_PRIORITY
67
+ ) {
68
+ return raw;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ export function normalizeAccountPriority(raw: unknown): number {
74
+ return parseAccountPriority(raw) ?? DEFAULT_ACCOUNT_PRIORITY;
75
+ }
76
+
77
+ /**
78
+ * Narrow an already-eligible account list to the highest selection-order tier that
79
+ * still has usable quota. Priority is an *ordering* boundary layered on top of
80
+ * eligibility: it never admits an account the caller already filtered out, and it
81
+ * never keeps the pool on a tier whose every member is drained.
82
+ *
83
+ * Contract (each clause is load-bearing for "no behavior change when unconfigured"):
84
+ * - one distinct priority across `ids` (the unconfigured case) returns `ids` unchanged,
85
+ * so today's pick sequence is preserved byte for byte;
86
+ * - input order is preserved inside the returned tier, which keeps the `__main__`
87
+ * head-of-list bias and the first-index tie-break used by SWRR/lowest-usage;
88
+ * - every tier drained returns `ids` unchanged, reproducing today's
89
+ * stay-put-until-429 behavior rather than inventing a pick;
90
+ * - a `pinnedId` that is present *and* has headroom lowers the ceiling to its own
91
+ * tier, which is what makes a manual "use this now" survive round-robin and
92
+ * fill-first without any mutable selection state. A drained or absent pin is
93
+ * ignored, so the pin expires on its own once the account crosses the threshold.
94
+ */
95
+ export function selectPriorityTier(
96
+ ids: readonly string[],
97
+ priorityOf: (id: string) => number,
98
+ hasHeadroom: (id: string) => boolean,
99
+ pinnedId?: string,
100
+ ): readonly string[] {
101
+ // Readonly out as well as in: the no-change cases return the caller's own array, so a
102
+ // mutating caller would corrupt its input in exactly the cases that must not change.
103
+ const list = ids;
104
+ if (list.length <= 1) return list;
105
+
106
+ const priorities = list.map(priorityOf);
107
+ const firstPriority = priorities[0]!;
108
+ if (priorities.every(priority => priority === firstPriority)) return list;
109
+
110
+ let ceiling = Number.POSITIVE_INFINITY;
111
+ if (pinnedId !== undefined) {
112
+ const pinnedIndex = list.indexOf(pinnedId);
113
+ if (pinnedIndex >= 0 && hasHeadroom(pinnedId)) ceiling = priorities[pinnedIndex]!;
114
+ }
115
+
116
+ const tiers = [...new Set(priorities)].sort((a, b) => b - a);
117
+ for (const tier of tiers) {
118
+ if (tier > ceiling) continue;
119
+ const members = list.filter((_, index) => priorities[index] === tier);
120
+ if (members.some(hasHeadroom)) return members;
121
+ }
122
+ return list;
123
+ }
124
+
125
+ function getOrCreateState(poolKey: string): SelectionState {
126
+ let state = selectionState.get(poolKey);
127
+ if (!state) {
128
+ state = { successes: 0, currentWeights: new Map() };
129
+ selectionState.set(poolKey, state);
130
+ }
131
+ return state;
132
+ }
133
+
134
+ function cloneSelectionState(state: SelectionState): SelectionState {
135
+ return {
136
+ activeKey: state.activeKey,
137
+ successes: state.successes,
138
+ currentWeights: new Map(state.currentWeights),
139
+ };
140
+ }
141
+
142
+ function smoothWeightedIndex(ids: readonly string[], state: SelectionState): number {
143
+ let best = -1;
144
+ let bestScore = Number.NEGATIVE_INFINITY;
145
+ let total = 0;
146
+ const weight = 1;
147
+ for (let i = 0; i < ids.length; i++) {
148
+ const id = ids[i]!;
149
+ const score = (state.currentWeights.get(id) ?? 0) + weight;
150
+ state.currentWeights.set(id, score);
151
+ total += weight;
152
+ if (score > bestScore) {
153
+ best = i;
154
+ bestScore = score;
155
+ }
156
+ }
157
+ if (best >= 0) {
158
+ const key = ids[best]!;
159
+ state.currentWeights.set(key, (state.currentWeights.get(key) ?? 0) - total);
160
+ }
161
+ return best;
162
+ }
163
+
164
+ /**
165
+ * Shared pick core. Mutates `state` the same way live resolve does; callers pass
166
+ * either the live map entry or a scratch/clone for dry-run peek.
167
+ */
168
+ function pickRoundRobinFromState(
169
+ eligibleIds: readonly string[],
170
+ stickyLimit: number,
171
+ state: SelectionState,
172
+ commitSticky: boolean,
173
+ ): string | null {
174
+ if (eligibleIds.length === 0) return null;
175
+
176
+ const limit = normalizeAccountPoolStickyLimit(stickyLimit);
177
+
178
+ if (state.activeKey && eligibleIds.includes(state.activeKey)) {
179
+ return state.activeKey;
180
+ }
181
+
182
+ if (state.activeKey) {
183
+ delete state.activeKey;
184
+ state.successes = 0;
185
+ }
186
+
187
+ const index = smoothWeightedIndex(eligibleIds, state);
188
+ if (index < 0) return null;
189
+
190
+ const picked = eligibleIds[index]!;
191
+ if (commitSticky && limit > 1) {
192
+ state.activeKey = picked;
193
+ state.successes = 0;
194
+ }
195
+ return picked;
196
+ }
197
+
198
+ export function pickRoundRobinAccount(
199
+ poolKey: string,
200
+ eligibleIds: readonly string[],
201
+ stickyLimit: number,
202
+ ): string | null {
203
+ return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
204
+ }
205
+
206
+ /**
207
+ * Dry-run of {@link pickRoundRobinAccount}: returns the same account resolve would
208
+ * pick without advancing ring weights, activeKey, or successes.
209
+ */
210
+ export function peekRoundRobinAccount(
211
+ poolKey: string,
212
+ eligibleIds: readonly string[],
213
+ stickyLimit: number,
214
+ ): string | null {
215
+ const live = selectionState.get(poolKey);
216
+ const scratch = live
217
+ ? cloneSelectionState(live)
218
+ : { successes: 0, currentWeights: new Map<string, number>() };
219
+ return pickRoundRobinFromState(eligibleIds, stickyLimit, scratch, false);
220
+ }
221
+
222
+ export function notePoolRotationSuccess(
223
+ poolKey: string,
224
+ accountId: string,
225
+ stickyLimit: number,
226
+ ): void {
227
+ const limit = normalizeAccountPoolStickyLimit(stickyLimit);
228
+ const state = selectionState.get(poolKey);
229
+ if (!state) return;
230
+ if (state.activeKey !== accountId) {
231
+ state.activeKey = accountId;
232
+ state.successes = 0;
233
+ }
234
+ state.successes += 1;
235
+ if (state.successes >= limit) {
236
+ delete state.activeKey;
237
+ state.successes = 0;
238
+ }
239
+ }
240
+
241
+ export function notePoolRotationFailure(poolKey: string, accountId: string): void {
242
+ const state = selectionState.get(poolKey);
243
+ if (state?.activeKey === accountId) {
244
+ delete state.activeKey;
245
+ state.successes = 0;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Force the next sticky/RR pick onto `accountId` (manual dashboard selection).
251
+ * Clears sticky success counters and ring weights so the seeded account is held
252
+ * for the next new-session pick before ordinary rotation resumes.
253
+ */
254
+ export function seedPoolRotationAccount(poolKey: string, accountId: string): void {
255
+ const state = getOrCreateState(poolKey);
256
+ state.activeKey = accountId;
257
+ state.successes = 0;
258
+ state.currentWeights.clear();
259
+ }
260
+
261
+ export function clearPoolRotationState(poolKey?: string): void {
262
+ if (poolKey === undefined) {
263
+ selectionState.clear();
264
+ return;
265
+ }
266
+ selectionState.delete(poolKey);
267
+ }
268
+
269
+ export function reconcilePoolRotationState(context: GenerationContext): number {
270
+ if (context.generation <= lastReconciledGeneration) return 0;
271
+ const anthropicIds = new Set<string>();
272
+ // Live account ids per generic OAuth provider, built from the same
273
+ // `provider\0id` roster the Anthropic pass already walks. Without this the
274
+ // `generic:*` entries fall through as unknown and are never swept, so a removed
275
+ // account would keep its rotation weight forever.
276
+ const genericIds = new Map<string, Set<string>>();
277
+ for (const key of context.oauthAccountKeys) {
278
+ const separator = key.indexOf("\0");
279
+ if (separator <= 0) continue;
280
+ const provider = key.slice(0, separator);
281
+ const accountId = key.slice(separator + 1);
282
+ if (provider === "anthropic") {
283
+ anthropicIds.add(accountId);
284
+ continue;
285
+ }
286
+ let bucket = genericIds.get(provider);
287
+ if (!bucket) {
288
+ bucket = new Set<string>();
289
+ genericIds.set(provider, bucket);
290
+ }
291
+ bucket.add(accountId);
292
+ }
293
+ let removed = 0;
294
+ for (const [poolKey, state] of selectionState) {
295
+ const valid = poolKey === POOL_KEY_ANTHROPIC
296
+ ? anthropicIds
297
+ : poolKey === POOL_KEY_CODEX || poolKey.startsWith(`${POOL_KEY_CODEX}:`)
298
+ ? context.codexAccountIds
299
+ : poolKey.startsWith("generic:")
300
+ ? genericIds.get(poolKey.slice("generic:".length)) ?? new Set<string>()
301
+ : null;
302
+ if (!valid) continue;
303
+ if (valid.size === 0) {
304
+ selectionState.delete(poolKey);
305
+ removed += 1;
306
+ continue;
307
+ }
308
+ if (state.activeKey && !valid.has(state.activeKey)) {
309
+ delete state.activeKey;
310
+ state.successes = 0;
311
+ removed += 1;
312
+ }
313
+ for (const accountId of state.currentWeights.keys()) {
314
+ if (valid.has(accountId)) continue;
315
+ state.currentWeights.delete(accountId);
316
+ removed += 1;
317
+ }
318
+ }
319
+ lastReconciledGeneration = context.generation;
320
+ return removed;
321
+ }
@@ -1,5 +1,6 @@
1
1
  import { isGenericFailoverProvider } from "./generic-account-failover";
2
- import type { OcxProviderConfig } from "../types";
2
+ import { parseAccountPoolStickyLimit, parseAccountPoolStrategy } from "./pool-kernel";
3
+ import type { OcxConfig, OcxProviderConfig } from "../types";
3
4
 
4
5
  /**
5
6
  * Which pool-settings contract a provider speaks (#695, slice 1).
@@ -28,33 +29,85 @@ export function poolSettingsCapability(name: string, provider: OcxProviderConfig
28
29
  }
29
30
 
30
31
  export function parseGenericPoolStrategy(value: unknown): GenericPoolStrategy | null {
31
- return typeof value === "string" && (GENERIC_POOL_STRATEGIES as readonly string[]).includes(value)
32
- ? value as GenericPoolStrategy
33
- : null;
32
+ // Delegated, not re-implemented. Three pools accepting the same three names from three
33
+ // private copies of the same check is how they drift apart: the Codex and Anthropic kinds
34
+ // already shared this parser while the generic kind carried its own. The names and the
35
+ // 1..100 bound live in pool-kernel.ts, once.
36
+ return parseAccountPoolStrategy(value) as GenericPoolStrategy | null;
34
37
  }
35
38
 
36
39
  export function parseGenericAutoSwitchThreshold(value: unknown): number | null {
37
40
  return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100 ? value : null;
38
41
  }
39
42
 
43
+ export function parseGenericStickyLimit(value: unknown): number | null {
44
+ return parseAccountPoolStickyLimit(value);
45
+ }
46
+
47
+ /** Fields the unified pool-settings contract can carry, per kind. */
48
+ export const POOL_SETTINGS_FIELDS = [
49
+ "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow",
50
+ ] as const;
51
+ export type PoolSettingsField = typeof POOL_SETTINGS_FIELDS[number];
52
+
53
+ /**
54
+ * One shape for all three pool kinds.
55
+ *
56
+ * `supported` is the reason this is a consolidation rather than a fourth contract: a field a
57
+ * kind does not honour is DECLARED unsupported instead of being omitted, so a dashboard can
58
+ * tell "this pool has no quotaWindow" from "this response forgot to send one". Every kind
59
+ * answers with the same keys.
60
+ */
61
+ export interface PoolSettingsDto {
62
+ provider: string;
63
+ kind: PoolSettingsKind;
64
+ supported: PoolSettingsField[];
65
+ /** The STORED override. null means nothing is stored here, not "off". */
66
+ enabled: boolean | null;
67
+ /**
68
+ * What the runtime actually resolves for `enabled`, after the global default.
69
+ *
70
+ * The generic kind inherits `config.oauthAccountFailover.enabled` when it stores no override
71
+ * of its own, so `enabled: null` alone cannot distinguish a disabled pool from an inherited
72
+ * one. This resolves exactly that config question and nothing else -- deliberately NOT the
73
+ * roster quorum the dispatch predicate also applies, because a settings field that folded in
74
+ * "how many accounts are logged in" would be answering a different question than it asks.
75
+ */
76
+ enabledEffective: boolean;
77
+ strategy: string | null;
78
+ stickyLimit: number | null;
79
+ autoSwitchThreshold: number | null;
80
+ quotaWindow: string | null;
81
+ }
82
+
83
+
40
84
  export interface GenericPoolSettingsDto {
41
85
  provider: string;
42
86
  kind: "generic";
43
87
  enabled: boolean | null;
44
88
  strategy: GenericPoolStrategy | null;
45
89
  autoSwitchThreshold: number | null;
90
+ stickyLimit: number | null;
46
91
  /**
47
- * Slice-1 marker for `strategy` and `autoSwitchThreshold` only: persisted, not yet consumed
48
- * by the selector.
92
+ * Marker for `strategy`, `autoSwitchThreshold` and `stickyLimit` only: true while they are
93
+ * persisted but not consumed by the selector, false once `pool.kernel` is on and they
94
+ * actually choose an account.
49
95
  *
50
96
  * It deliberately does NOT describe `enabled`, which governs the pre-dispatch preference.
51
97
  * Widening it to the whole DTO would tell a dashboard that `enabled` changes nothing, which
52
98
  * has been false since reactive and proactive activation were split.
99
+ *
100
+ * Computed, never a literal: the flag is reversible, so a DTO that hard-codes either answer
101
+ * would be lying in one of the two states.
53
102
  */
54
- inert: true;
103
+ inert: boolean;
55
104
  }
56
105
 
57
- export function genericPoolSettingsDto(name: string, provider: OcxProviderConfig): GenericPoolSettingsDto {
106
+ export function genericPoolSettingsDto(
107
+ name: string,
108
+ provider: OcxProviderConfig,
109
+ kernelEnabled = false,
110
+ ): GenericPoolSettingsDto {
58
111
  const failover = provider.oauthAccountFailover ?? {};
59
112
  return {
60
113
  provider: name,
@@ -62,6 +115,71 @@ export function genericPoolSettingsDto(name: string, provider: OcxProviderConfig
62
115
  enabled: typeof failover.enabled === "boolean" ? failover.enabled : null,
63
116
  strategy: parseGenericPoolStrategy(failover.strategy),
64
117
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(failover.autoSwitchThreshold),
65
- inert: true,
118
+ stickyLimit: parseGenericStickyLimit(failover.stickyLimit),
119
+ inert: kernelEnabled !== true,
66
120
  };
67
121
  }
122
+
123
+ /** Which fields each kind actually honours. Declared, never silently omitted. */
124
+ const SUPPORTED_BY_KIND: Record<PoolSettingsKind, PoolSettingsField[]> = {
125
+ codex: ["strategy", "stickyLimit", "autoSwitchThreshold"],
126
+ anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow"],
127
+ generic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold"],
128
+ };
129
+
130
+ /**
131
+ * The one projection behind `/api/pool/settings`.
132
+ *
133
+ * Reads each kind's own storage -- this consolidates the CONTRACT, not the persistence -- and
134
+ * answers with identical keys plus a `supported` list, so an unsupported field is a declared
135
+ * `null` rather than an absence a caller has to guess about.
136
+ */
137
+ export function unifiedPoolSettingsDto(
138
+ config: OcxConfig,
139
+ provider: string,
140
+ kind: PoolSettingsKind,
141
+ ): PoolSettingsDto {
142
+ const base = { provider, kind, supported: SUPPORTED_BY_KIND[kind] };
143
+ if (kind === "codex") {
144
+ return {
145
+ ...base,
146
+ // The Codex pool has no enablement switch: it is on whenever accounts exist, so the
147
+ // honest answer is "not a field here" rather than a fabricated true.
148
+ enabled: null,
149
+ enabledEffective: true,
150
+ strategy: parseGenericPoolStrategy(config.accountPoolStrategy) ?? "quota",
151
+ stickyLimit: parseGenericStickyLimit(config.accountPoolStickyLimit) ?? 1,
152
+ autoSwitchThreshold: parseGenericAutoSwitchThreshold(config.autoSwitchThreshold) ?? 80,
153
+ quotaWindow: null,
154
+ };
155
+ }
156
+ if (kind === "anthropic") {
157
+ const pool = config.anthropicAccountPool ?? {};
158
+ const enabled = typeof pool.enabled === "boolean" ? pool.enabled : null;
159
+ return {
160
+ ...base,
161
+ enabled,
162
+ enabledEffective: enabled === true,
163
+ strategy: parseGenericPoolStrategy(pool.strategy) ?? "quota",
164
+ stickyLimit: parseGenericStickyLimit(pool.stickyLimit) ?? 1,
165
+ autoSwitchThreshold: parseGenericAutoSwitchThreshold(pool.autoSwitchThreshold) ?? 80,
166
+ quotaWindow: typeof pool.quotaWindow === "string" ? pool.quotaWindow : "five-hour",
167
+ };
168
+ }
169
+ const failover = config.providers?.[provider]?.oauthAccountFailover ?? {};
170
+ const stored = typeof failover.enabled === "boolean" ? failover.enabled : null;
171
+ return {
172
+ ...base,
173
+ enabled: stored,
174
+ // The defect this field exists to close: a generic provider with no stored override
175
+ // inherits the global, so `enabled: null` alone cannot tell a disabled pool from an
176
+ // inherited one. Config only -- the roster quorum the dispatch predicate also applies is a
177
+ // different question and stays out of a settings field.
178
+ enabledEffective: stored ?? (config.oauthAccountFailover?.enabled === true),
179
+ strategy: parseGenericPoolStrategy(failover.strategy),
180
+ stickyLimit: parseGenericStickyLimit(failover.stickyLimit),
181
+ autoSwitchThreshold: parseGenericAutoSwitchThreshold(failover.autoSwitchThreshold),
182
+ quotaWindow: null,
183
+ };
184
+ }
185
+
@@ -29,6 +29,7 @@ import {
29
29
  type GenerationContext,
30
30
  } from "../lib/state-store-sweeper";
31
31
  import { validateCopilotApiBaseUrl } from "./github-copilot";
32
+ import { validateDevinApiBaseUrl } from "./devin/api-base";
32
33
  import type { OAuthAccountSelection, OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
33
34
 
34
35
  export type AuthStore = Record<string, ProviderAccountSet>;
@@ -459,9 +460,12 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
459
460
  if (isCredentialSource(candidate.source)) normalized.source = candidate.source;
460
461
  if (typeof candidate.projectId === "string" && candidate.projectId.length > 0) normalized.projectId = candidate.projectId;
461
462
  if (typeof candidate.apiBaseUrl === "string" && candidate.apiBaseUrl.length > 0) {
462
- // Persist only allowlisted Copilot origins; drop anything else so auth.json cannot
463
- // become an SSRF springboard across reloads.
464
- const validated = validateCopilotApiBaseUrl(candidate.apiBaseUrl);
463
+ // Persist only allowlisted origins; drop anything else so auth.json cannot
464
+ // become an SSRF springboard across reloads. Copilot and Devin are the two
465
+ // providers whose host comes back from the network, and each owns its own
466
+ // allowlist.
467
+ const validated =
468
+ validateCopilotApiBaseUrl(candidate.apiBaseUrl) ?? validateDevinApiBaseUrl(candidate.apiBaseUrl);
465
469
  if (validated) normalized.apiBaseUrl = validated;
466
470
  }
467
471
  if (candidate.kiro && typeof candidate.kiro === "object") {
@@ -52,7 +52,7 @@ const DEFAULTS = {
52
52
  failureBackoffBaseSeconds: 300,
53
53
  failureBackoffMaxSeconds: 3600,
54
54
  codexWarmupMaxAgeSeconds: 691_200, // 8d — matches Codex managed-auth last_refresh cadence.
55
- codexWarmupModel: "gpt-5.4-mini",
55
+ codexWarmupModel: "gpt-5.6-luna",
56
56
  };
57
57
 
58
58
  interface BackoffEntry {
@@ -35,7 +35,6 @@ export const CODEBUDDY_GLOBAL_MODELS = [
35
35
  /** China (`internal`) session models from the official internal manifest (text/chat models only). */
36
36
  export const CODEBUDDY_CN_MODELS = [
37
37
  "default",
38
- "deepseek-v4-pro",
39
38
  "deepseek-v4-flash",
40
39
  "minimax-m3",
41
40
  "minimax-m2.7",
@@ -121,7 +120,6 @@ export const CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, st
121
120
 
122
121
  export const CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
123
122
  "default": 200_000,
124
- "deepseek-v4-pro": 1_000_000,
125
123
  "deepseek-v4-flash": 1_000_000,
126
124
  "minimax-m3": 512_000,
127
125
  "minimax-m2.7": 200_000,
@@ -142,7 +140,6 @@ export const CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
142
140
 
143
141
  export const CODEBUDDY_CN_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
144
142
  "default": 24_000,
145
- "deepseek-v4-pro": 50_000,
146
143
  "deepseek-v4-flash": 50_000,
147
144
  "minimax-m3": 128_000,
148
145
  "minimax-m2.7": 48_000,
@@ -1,10 +1,6 @@
1
1
  import { readBoundedResponseBody } from "../lib/bounded-body";
2
2
 
3
3
  const COMMAND_CODE_MODEL_EFFORTS = {
4
- "deepseek/deepseek-v4-pro": {
5
- efforts: ["high", "max"],
6
- profileUrl: "https://commandcode.ai/models/deepseek-v4-pro",
7
- },
8
4
  "deepseek/deepseek-v4-flash": {
9
5
  efforts: ["high", "max"],
10
6
  profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
@@ -130,6 +126,29 @@ const COMMAND_CODE_MODEL_EFFORTS = {
130
126
  efforts: ["low", "medium", "high", "xhigh", "max"],
131
127
  profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.1",
132
128
  },
129
+ /*
130
+ * Two live routes that never gained a row here, so the adapter dropped every
131
+ * requested effort (a client's `max` left the wire as no reasoning parameter
132
+ * at all) and the preset advertised no effort control for them.
133
+ *
134
+ * PROVENANCE, stated plainly: both ladders are inferred from the same-family
135
+ * rows above — deepseek v4: high..max; the Qwen 3.8 family: low..max — NOT
136
+ * read from the profile pages. commandcode.ai renders those client-side and
137
+ * ships an empty reasoning payload, so the self-refresh below is as dead for
138
+ * these rows as the #2647 block above already documents. Measured live
139
+ * 2026-09-11: /alpha/generate accepts `reasoning_effort: "max"` on both
140
+ * routes (HTTP 200). `ultra` is deliberately not offered: the adapter would
141
+ * strip it, and no profile evidence backs an ultra→max alias the way it does
142
+ * for v4-pro/v4-flash above.
143
+ */
144
+ "deepseek/deepseek-v4.1-flash": {
145
+ efforts: ["high", "max"],
146
+ profileUrl: "https://commandcode.ai/models/deepseek-v4-1-flash",
147
+ },
148
+ "Qwen/Qwen3.8-Flash": {
149
+ efforts: ["low", "medium", "high", "max"],
150
+ profileUrl: "https://commandcode.ai/models/qwen3-8-flash",
151
+ },
133
152
  } as const;
134
153
 
135
154
  /**
@@ -51,7 +51,11 @@ export const DEFAULT_MODEL_ALIASES: ReadonlyArray<{ match: RegExp; alias: string
51
51
  { match: /^claude-haiku/, alias: "haiku" },
52
52
  { match: /^gemini-3(?:\.\d+)?-pro/, alias: "g3p" },
53
53
  { match: /^gemini-3(?:\.\d+)?-flash/, alias: "g3f" },
54
+ // Ordered before the V4 rule on purpose: `builtinRule` takes the first match, and
55
+ // `/^deepseek-v4/` also matches `deepseek-v4.1-flash`.
56
+ { match: /^deepseek-v4\.1/, alias: "ds41" },
54
57
  { match: /^deepseek-v4/, alias: "ds4" },
58
+ { match: /^deepseek-flash/, alias: "dsf" },
55
59
  { match: /^grok-4/, alias: "grok" },
56
60
  ];
57
61
 
@@ -16,6 +16,7 @@ export interface DerivedKeyLoginProvider {
16
16
  label: string;
17
17
  baseUrl: string;
18
18
  responsesPath?: string;
19
+ chatCompletionsPath?: string;
19
20
  adapter: string;
20
21
  apiKeyValidation?: "unknown";
21
22
  apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
@@ -71,6 +72,7 @@ export interface DerivedProviderPreset {
71
72
  adapter: string;
72
73
  baseUrl: string;
73
74
  responsesPath?: string;
75
+ chatCompletionsPath?: string;
74
76
  defaultModel?: string;
75
77
  auth: "oauth" | "forward" | "key" | "local";
76
78
  codexAccountMode?: CodexAccountMode;
@@ -222,6 +224,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
222
224
  baseUrl: entry.baseUrl,
223
225
  ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
224
226
  ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
227
+ ...(entry.chatCompletionsPath ? { chatCompletionsPath: entry.chatCompletionsPath } : {}),
225
228
  ...(entry.alias ? { alias: entry.alias } : {}),
226
229
  // Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
227
230
  // distinguish local runtimes from API-key providers keep working after the seed round-trip.
@@ -257,6 +260,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
257
260
  ...(entry.chatServiceTier !== undefined ? { chatServiceTier: entry.chatServiceTier } : {}),
258
261
  ...(entry.openaiChatEofTolerance !== undefined ? { openaiChatEofTolerance: entry.openaiChatEofTolerance } : {}),
259
262
  ...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}),
263
+ ...(entry.chatCompletionsPath !== undefined ? { chatCompletionsPath: entry.chatCompletionsPath } : {}),
260
264
  ...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}),
261
265
  ...(entry.requiresAdjacentResponsesToolResults !== undefined
262
266
  ? { requiresAdjacentResponsesToolResults: entry.requiresAdjacentResponsesToolResults }
@@ -288,6 +292,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
288
292
  label: entry.label,
289
293
  baseUrl: entry.baseUrl,
290
294
  ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
295
+ ...(entry.chatCompletionsPath ? { chatCompletionsPath: entry.chatCompletionsPath } : {}),
291
296
  adapter: entry.adapter,
292
297
  ...(entry.apiKeyValidation !== undefined ? { apiKeyValidation: entry.apiKeyValidation } : {}),
293
298
  ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
@@ -478,6 +483,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
478
483
  if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
479
484
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
480
485
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
486
+ if (prov.chatCompletionsPath === undefined && seed.chatCompletionsPath !== undefined) prov.chatCompletionsPath = seed.chatCompletionsPath;
481
487
  // Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
482
488
  if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
483
489
  if (!prov.models && seed.models) prov.models = [...seed.models];
@@ -521,6 +527,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
521
527
  // Fill-only: a hand-edited path must survive, and a config saved before the registry
522
528
  // learned this route still gets backfilled.
523
529
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
530
+ if (prov.chatCompletionsPath === undefined && seed.chatCompletionsPath !== undefined) prov.chatCompletionsPath = seed.chatCompletionsPath;
524
531
  if (prov.statelessResponses === undefined && seed.statelessResponses !== undefined) prov.statelessResponses = seed.statelessResponses;
525
532
  if (prov.requiresAdjacentResponsesToolResults === undefined && seed.requiresAdjacentResponsesToolResults !== undefined) {
526
533
  prov.requiresAdjacentResponsesToolResults = seed.requiresAdjacentResponsesToolResults;
@@ -602,6 +609,7 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
602
609
  adapter: entry.adapter,
603
610
  baseUrl: entry.baseUrl,
604
611
  ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
612
+ ...(entry.chatCompletionsPath ? { chatCompletionsPath: entry.chatCompletionsPath } : {}),
605
613
  auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : entry.authKind === "local" ? "local" : "key",
606
614
  ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
607
615
  ...(entry.codexAccountMode ? { provider: providerConfigSeed(entry) } : {}),