@bitkyc08/opencodex 2.18.2 → 2.20.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 (131) hide show
  1. package/gui/dist/assets/index-DF_UFrGS.css +1 -0
  2. package/gui/dist/assets/index-DSK3S5HY.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/google-antigravity-wire.ts +4 -3
  7. package/src/adapters/google.ts +1 -1
  8. package/src/adapters/mimo-free.ts +17 -0
  9. package/src/adapters/openai-chat.ts +367 -32
  10. package/src/adapters/registry.ts +144 -0
  11. package/src/chat/inbound.ts +37 -8
  12. package/src/cli/claude.ts +2 -1
  13. package/src/cli/combo.ts +3 -0
  14. package/src/cli/dispatch.ts +8 -0
  15. package/src/cli/export-command.ts +2 -2
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +3 -2
  18. package/src/cli/lab.ts +135 -1
  19. package/src/cli/minimax.ts +491 -0
  20. package/src/cli/models-runtime.ts +22 -1
  21. package/src/cli/models.ts +67 -2
  22. package/src/cli/opencode.ts +2 -1
  23. package/src/cli/registry.ts +22 -2
  24. package/src/clients/config-export.ts +125 -7
  25. package/src/codex/app-server-processes.ts +57 -2
  26. package/src/codex/app-server-restart-service.ts +232 -0
  27. package/src/codex/catalog/aggregation.ts +10 -1
  28. package/src/codex/catalog/effort.ts +15 -3
  29. package/src/codex/catalog/parsing.ts +3 -1
  30. package/src/codex/catalog/provider-fetch.ts +45 -5
  31. package/src/codex/catalog/sync.ts +74 -4
  32. package/src/codex/convergence.ts +2 -0
  33. package/src/codex/shim.ts +100 -5
  34. package/src/combos/index.ts +1 -0
  35. package/src/combos/request.ts +30 -0
  36. package/src/combos/types.ts +6 -0
  37. package/src/config.ts +52 -0
  38. package/src/generated/compatibility-version.json +239 -87
  39. package/src/images/loop.ts +11 -1
  40. package/src/integrations/registry.ts +7 -0
  41. package/src/lab/conformance/jcs.ts +42 -2
  42. package/src/lab/conformance/negative-controls.ts +6 -2
  43. package/src/lab/conformance/runner.ts +16 -5
  44. package/src/lab/fabric/observe.ts +49 -14
  45. package/src/lab/index.ts +16 -0
  46. package/src/lab/ledger/purge.ts +152 -83
  47. package/src/lab/ledger/store.ts +168 -54
  48. package/src/lab/observe/from-conformance.ts +8 -6
  49. package/src/lab/observe/from-live.ts +8 -2
  50. package/src/lab/paths.ts +23 -0
  51. package/src/lab/public/bundle.ts +217 -0
  52. package/src/lab/public/community-authority.ts +175 -0
  53. package/src/lab/public/community-files.ts +29 -0
  54. package/src/lab/public/community.ts +479 -0
  55. package/src/lab/public/file-safety.ts +155 -0
  56. package/src/lab/public/ids.ts +26 -0
  57. package/src/lab/public/index.ts +16 -0
  58. package/src/lab/public/mutation-lock.ts +424 -0
  59. package/src/lab/public/operator.ts +353 -0
  60. package/src/lab/public/origin-purge.ts +79 -0
  61. package/src/lab/public/origin.ts +203 -0
  62. package/src/lab/public/privacy.ts +143 -0
  63. package/src/lab/public/private-file.ts +261 -0
  64. package/src/lab/public/project.ts +124 -0
  65. package/src/lab/public/purge-test-fault.ts +21 -0
  66. package/src/lab/public/purge.ts +223 -0
  67. package/src/lab/public/registry.ts +44 -0
  68. package/src/lab/public/revocation.ts +252 -0
  69. package/src/lab/public/signature.ts +219 -0
  70. package/src/lab/public/storage.ts +105 -0
  71. package/src/lab/public/strict-json.ts +206 -0
  72. package/src/lab/public/time.ts +26 -0
  73. package/src/lab/public/types.ts +172 -0
  74. package/src/lab/public/validate.ts +391 -0
  75. package/src/lib/codex-restart-contract.ts +120 -0
  76. package/src/lib/errors.ts +27 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +9 -3
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/spill-store.ts +20 -1
  91. package/src/responses/state.ts +159 -3
  92. package/src/router.ts +23 -6
  93. package/src/routing/capability.ts +4 -2
  94. package/src/routing/compatibility/behavior.ts +5 -1
  95. package/src/server/adapter-resolve.ts +2 -32
  96. package/src/server/auth-cors.ts +8 -0
  97. package/src/server/chat-completions.ts +74 -34
  98. package/src/server/chat-native-sse.ts +331 -0
  99. package/src/server/chat-native.ts +371 -0
  100. package/src/server/effort-policy.ts +18 -0
  101. package/src/server/management/combo-routes.ts +16 -2
  102. package/src/server/management/config-routes.ts +6 -4
  103. package/src/server/management/context.ts +17 -0
  104. package/src/server/management/lab-routes.ts +181 -19
  105. package/src/server/management/model-routes.ts +76 -2
  106. package/src/server/management/model-rows.ts +8 -0
  107. package/src/server/management/provider-capability-config.ts +48 -0
  108. package/src/server/management/provider-routes.ts +76 -4
  109. package/src/server/management/system-restart.ts +4 -2
  110. package/src/server/management/system-routes.ts +38 -0
  111. package/src/server/relay.ts +17 -3
  112. package/src/server/responses/compact.ts +4 -1
  113. package/src/server/responses/core.ts +278 -48
  114. package/src/server/responses/empty-completion-guard.ts +276 -0
  115. package/src/server/responses/fetch-helpers.ts +35 -4
  116. package/src/server/responses/input-admission.ts +169 -0
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/service-manager-probe.ts +2 -3
  123. package/src/types.ts +76 -6
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/vision/index.ts +21 -4
  129. package/src/web-search/index.ts +2 -1
  130. package/gui/dist/assets/index-CXI1262_.js +0 -76
  131. package/gui/dist/assets/index-DUCH59lJ.css +0 -1
@@ -200,13 +200,15 @@ export interface ProviderRegistryEntry {
200
200
  */
201
201
  requiresAdjacentResponsesToolResults?: boolean;
202
202
  /**
203
- * Registry default for the provider's Responses `service_tier` support; see
203
+ * Registry default for the provider's `service_tier` support; see
204
204
  * `OcxProviderConfig.supportsServiceTier`. Registry-only: backfilled (never
205
205
  * overriding) at enrich/route time and deliberately NOT seeded into saved
206
206
  * config, so an explicit user value stays distinguishable from the default
207
207
  * (and the canonical openai seed comparison keeps its exact key set).
208
208
  */
209
209
  supportsServiceTier?: boolean;
210
+ /** Registry default for exact model service-tier capability; explicit config keys win. */
211
+ modelSupportsServiceTier?: Record<string, boolean>;
210
212
  /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
211
213
  preserveResponsesReasoningContent?: boolean;
212
214
  /** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
@@ -903,6 +905,7 @@ const CLINE_PASS_MODELS = [
903
905
  "cline-pass/mimo-v2.5",
904
906
  "cline-pass/mimo-v2.5-pro",
905
907
  "cline-pass/minimax-m3",
908
+ "cline-pass/qwen3.8-max",
906
909
  "cline-pass/qwen3.7-max",
907
910
  "cline-pass/qwen3.7-plus",
908
911
  ];
@@ -928,9 +931,10 @@ const CLINE_PASS_IMAGE_MODELS = new Set([
928
931
  "cline-pass/minimax-m3",
929
932
  "cline-pass/qwen3.7-plus",
930
933
  ]);
931
- const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODELS.filter(id => !CLINE_PASS_IMAGE_MODELS.has(id));
934
+ const CLINE_PASS_MODALITY_KNOWN_MODELS = CLINE_PASS_MODELS.filter(id => id !== "cline-pass/qwen3.8-max");
935
+ const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODALITY_KNOWN_MODELS.filter(id => !CLINE_PASS_IMAGE_MODELS.has(id));
932
936
  const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
933
- CLINE_PASS_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
937
+ CLINE_PASS_MODALITY_KNOWN_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
934
938
  );
935
939
 
936
940
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
@@ -2420,6 +2424,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
2420
2424
  dashboardUrl: "https://xiaomimimo.com",
2421
2425
  defaultModel: "mimo-auto",
2422
2426
  models: ["mimo-auto"],
2427
+ reasoningEfforts: ["low", "medium", "high"],
2428
+ reasoningEffortMap: { xhigh: "high", max: "high", ultra: "high" },
2423
2429
  note: "No key needed — uses Xiaomi MiMo's free public tier (limited-time offer). A JWT is bootstrapped automatically with an anonymous random client id stored locally. The endpoint contract mirrors the official MiMoCode client and is not publicly documented — Xiaomi may change or restrict it at any time. Prompts may be processed/retained by Xiaomi; do not send confidential material.",
2424
2430
  },
2425
2431
  // Xiaomi MiMo paid token plan. Separate host and wire from both `xiaomi` (Anthropic) and
@@ -0,0 +1,310 @@
1
+ import type { OcxProviderConfig, RequestPacingRule } from "../types";
2
+ import type { GenerationContext } from "../lib/state-store-sweeper";
3
+
4
+ export const REQUEST_PACING_MAX_QUEUE_DEPTH = 256;
5
+ export const REQUEST_PACING_MAX_QUEUE_AGE_MS = 60_000;
6
+
7
+ let maxQueueDepth = REQUEST_PACING_MAX_QUEUE_DEPTH;
8
+ let maxQueueAgeMs = REQUEST_PACING_MAX_QUEUE_AGE_MS;
9
+
10
+ export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired";
11
+
12
+ export class RequestPacingQueueOverloadError extends Error {
13
+ constructor(
14
+ public readonly providerName: string,
15
+ public readonly reason: RequestPacingQueueOverloadReason,
16
+ public readonly retryAfterSeconds: number,
17
+ ) {
18
+ super(reason === "queue_full"
19
+ ? `request pacing queue for provider '${providerName}' is full`
20
+ : `request pacing queue for provider '${providerName}' exceeded the maximum queued age`);
21
+ this.name = "RequestPacingQueueOverloadError";
22
+ }
23
+ }
24
+
25
+ export class RequestPacingProviderRemovedError extends Error {
26
+ constructor(public readonly providerName: string) {
27
+ super(`request pacing provider '${providerName}' was removed`);
28
+ this.name = "RequestPacingProviderRemovedError";
29
+ }
30
+ }
31
+
32
+ interface Waiter {
33
+ modelId?: string;
34
+ providerIntervalMs: number;
35
+ modelIntervalMs: number;
36
+ queuedAt: number;
37
+ signal?: AbortSignal;
38
+ resolve: () => void;
39
+ reject: (reason: unknown) => void;
40
+ abort?: () => void;
41
+ }
42
+
43
+ interface ProviderPacer {
44
+ queue: Waiter[];
45
+ providerNextStartAt: number;
46
+ modelNextStartAt: Map<string, number>;
47
+ timer?: unknown;
48
+ lastStartedAt?: number;
49
+ lastModelId?: string;
50
+ }
51
+
52
+ export interface RequestPacingRuntime {
53
+ now: () => number;
54
+ setTimer: (callback: () => void, delayMs: number) => unknown;
55
+ clearTimer: (handle: unknown) => void;
56
+ enqueueMicrotask: (callback: () => void) => void;
57
+ }
58
+
59
+ export interface ProviderRequestPacingStatus {
60
+ provider: string;
61
+ enabled: boolean;
62
+ queued: number;
63
+ nextSlotInMs: number;
64
+ lastStartedAt?: number;
65
+ lastModelId?: string;
66
+ }
67
+
68
+ const pacers = new Map<string, ProviderPacer>();
69
+ const defaultRuntime: RequestPacingRuntime = {
70
+ now: Date.now,
71
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
72
+ clearTimer: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
73
+ enqueueMicrotask: queueMicrotask,
74
+ };
75
+ let runtime = defaultRuntime;
76
+ let lastReconciledGeneration = 0;
77
+
78
+ function abortReason(signal: AbortSignal): unknown {
79
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
80
+ }
81
+
82
+ function normalizedInterval(rule: RequestPacingRule | undefined): number {
83
+ if (!rule) return 0;
84
+ const rpmInterval = typeof rule.requestsPerMinute === "number" && rule.requestsPerMinute > 0
85
+ ? 60_000 / rule.requestsPerMinute
86
+ : 0;
87
+ const fixedInterval = typeof rule.minIntervalMs === "number" && rule.minIntervalMs > 0
88
+ ? rule.minIntervalMs
89
+ : 0;
90
+ return Math.max(rpmInterval, fixedInterval);
91
+ }
92
+
93
+ export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: string): number {
94
+ const policy = provider.requestPacing;
95
+ if (!policy?.enabled) return 0;
96
+ const override = modelId ? policy.models?.[modelId] : undefined;
97
+ return Math.max(normalizedInterval(policy), normalizedInterval(override));
98
+ }
99
+
100
+ function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string): {
101
+ providerIntervalMs: number;
102
+ modelIntervalMs: number;
103
+ } {
104
+ const policy = provider.requestPacing;
105
+ if (!policy?.enabled) return { providerIntervalMs: 0, modelIntervalMs: 0 };
106
+ return {
107
+ providerIntervalMs: normalizedInterval(policy),
108
+ modelIntervalMs: modelId ? normalizedInterval(policy.models?.[modelId]) : 0,
109
+ };
110
+ }
111
+
112
+ function waiterReadyAt(state: ProviderPacer, modelId: string | undefined): number {
113
+ return Math.max(
114
+ state.providerNextStartAt,
115
+ modelId ? (state.modelNextStartAt.get(modelId) ?? 0) : 0,
116
+ );
117
+ }
118
+
119
+ function pacingRetryAfterSeconds(state: ProviderPacer, modelId: string | undefined, now: number): number {
120
+ return Math.max(1, Math.ceil(Math.max(0, waiterReadyAt(state, modelId) - now) / 1000));
121
+ }
122
+
123
+ function rejectExpiredWaiters(providerName: string, state: ProviderPacer, now: number): void {
124
+ for (let index = state.queue.length - 1; index >= 0; index -= 1) {
125
+ const waiter = state.queue[index]!;
126
+ if (now - waiter.queuedAt < maxQueueAgeMs) continue;
127
+ state.queue.splice(index, 1);
128
+ if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
129
+ waiter.reject(new RequestPacingQueueOverloadError(
130
+ providerName,
131
+ "queue_expired",
132
+ pacingRetryAfterSeconds(state, waiter.modelId, now),
133
+ ));
134
+ }
135
+ }
136
+
137
+ function removeProviderPacer(providerName: string, state: ProviderPacer): void {
138
+ if (state.timer) runtime.clearTimer(state.timer);
139
+ state.timer = undefined;
140
+ pacers.delete(providerName);
141
+ const waiters = state.queue.splice(0);
142
+ const error = new RequestPacingProviderRemovedError(providerName);
143
+ for (const waiter of waiters) {
144
+ if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
145
+ waiter.reject(error);
146
+ }
147
+ }
148
+
149
+ function runQueue(providerName: string, state: ProviderPacer): void {
150
+ if (state.queue.length === 0) {
151
+ if (state.timer) runtime.clearTimer(state.timer);
152
+ state.timer = undefined;
153
+ return;
154
+ }
155
+ if (state.timer) return;
156
+ const now = runtime.now();
157
+ for (const [modelId, readyAt] of state.modelNextStartAt) {
158
+ if (readyAt <= now) state.modelNextStartAt.delete(modelId);
159
+ }
160
+ rejectExpiredWaiters(providerName, state, now);
161
+ if (state.queue.length === 0) return;
162
+
163
+ const providerReadyAt = Math.max(now, state.providerNextStartAt);
164
+ const waiterIndex = state.queue.findIndex(waiter => {
165
+ const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
166
+ return Math.max(providerReadyAt, modelReadyAt) <= now;
167
+ });
168
+ if (waiterIndex < 0) {
169
+ let earliestAt = Number.POSITIVE_INFINITY;
170
+ for (const waiter of state.queue) {
171
+ const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
172
+ const readyAt = Math.max(providerReadyAt, modelReadyAt);
173
+ const expiresAt = waiter.queuedAt + maxQueueAgeMs;
174
+ earliestAt = Math.min(earliestAt, readyAt, expiresAt);
175
+ }
176
+ const delayMs = Math.max(0, earliestAt - now);
177
+ state.timer = runtime.setTimer(() => {
178
+ state.timer = undefined;
179
+ runQueue(providerName, state);
180
+ }, delayMs);
181
+ return;
182
+ }
183
+ const waiter = state.queue[waiterIndex]!;
184
+ state.queue.splice(waiterIndex, 1);
185
+ if (waiter.abort) waiter.signal?.removeEventListener("abort", waiter.abort);
186
+ const startedAt = runtime.now();
187
+ state.lastStartedAt = startedAt;
188
+ state.lastModelId = waiter.modelId;
189
+ state.providerNextStartAt = startedAt + waiter.providerIntervalMs;
190
+ if (waiter.modelId && waiter.modelIntervalMs > 0) {
191
+ state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs);
192
+ }
193
+ waiter.resolve();
194
+ runtime.enqueueMicrotask(() => runQueue(providerName, state));
195
+ }
196
+
197
+ export async function waitForProviderRequestSlot(
198
+ providerName: string,
199
+ provider: OcxProviderConfig,
200
+ modelId?: string,
201
+ signal?: AbortSignal,
202
+ ): Promise<void> {
203
+ const intervals = requestPacingIntervals(provider, modelId);
204
+ if (Math.max(intervals.providerIntervalMs, intervals.modelIntervalMs) <= 0) return;
205
+ if (signal?.aborted) throw abortReason(signal);
206
+
207
+ const state = pacers.get(providerName) ?? {
208
+ queue: [], providerNextStartAt: 0, modelNextStartAt: new Map<string, number>(),
209
+ };
210
+ pacers.set(providerName, state);
211
+
212
+ // Give already-eligible or expired waiters a chance to leave before applying the
213
+ // admission bound to the newest request. This preserves FIFO-ish fairness while
214
+ // keeping the retained queue strictly bounded under burst load.
215
+ if (state.timer) {
216
+ runtime.clearTimer(state.timer);
217
+ state.timer = undefined;
218
+ }
219
+ runQueue(providerName, state);
220
+ if (state.queue.length >= maxQueueDepth) {
221
+ throw new RequestPacingQueueOverloadError(
222
+ providerName,
223
+ "queue_full",
224
+ pacingRetryAfterSeconds(state, modelId, runtime.now()),
225
+ );
226
+ }
227
+
228
+ await new Promise<void>((resolve, reject) => {
229
+ const waiter: Waiter = { modelId, ...intervals, queuedAt: runtime.now(), signal, resolve, reject };
230
+ waiter.abort = () => {
231
+ const index = state.queue.indexOf(waiter);
232
+ if (index >= 0) state.queue.splice(index, 1);
233
+ if (state.timer) {
234
+ runtime.clearTimer(state.timer);
235
+ state.timer = undefined;
236
+ }
237
+ reject(abortReason(signal!));
238
+ runQueue(providerName, state);
239
+ };
240
+ signal?.addEventListener("abort", waiter.abort, { once: true });
241
+ state.queue.push(waiter);
242
+ // Abort may race between the eager check above and listener registration.
243
+ if (signal?.aborted) {
244
+ waiter.abort();
245
+ return;
246
+ }
247
+ if (state.timer) {
248
+ runtime.clearTimer(state.timer);
249
+ state.timer = undefined;
250
+ }
251
+ runQueue(providerName, state);
252
+ });
253
+ }
254
+
255
+ export function providerRequestPacingStatus(
256
+ providerName: string,
257
+ provider: OcxProviderConfig,
258
+ now = runtime.now(),
259
+ ): ProviderRequestPacingStatus {
260
+ const state = pacers.get(providerName);
261
+ let nextSlotAt = state?.providerNextStartAt ?? 0;
262
+ if (state && state.queue.length > 0) {
263
+ let earliestQueuedSlotAt = Number.POSITIVE_INFINITY;
264
+ for (const waiter of state.queue) {
265
+ earliestQueuedSlotAt = Math.min(earliestQueuedSlotAt, waiterReadyAt(state, waiter.modelId));
266
+ }
267
+ if (Number.isFinite(earliestQueuedSlotAt)) nextSlotAt = earliestQueuedSlotAt;
268
+ }
269
+ return {
270
+ provider: providerName,
271
+ enabled: provider.requestPacing?.enabled === true,
272
+ queued: state?.queue.length ?? 0,
273
+ nextSlotInMs: Math.max(0, Math.ceil(nextSlotAt - now)),
274
+ ...(state?.lastStartedAt !== undefined ? { lastStartedAt: state.lastStartedAt } : {}),
275
+ ...(state?.lastModelId ? { lastModelId: state.lastModelId } : {}),
276
+ };
277
+ }
278
+
279
+ export function setProviderRequestPacingLimitsForTest(limits: {
280
+ maxQueueDepth?: number;
281
+ maxQueueAgeMs?: number;
282
+ }): void {
283
+ if (limits.maxQueueDepth !== undefined) maxQueueDepth = limits.maxQueueDepth;
284
+ if (limits.maxQueueAgeMs !== undefined) maxQueueAgeMs = limits.maxQueueAgeMs;
285
+ }
286
+
287
+ export function setProviderRequestPacingRuntimeForTest(nextRuntime: RequestPacingRuntime): void {
288
+ runtime = nextRuntime;
289
+ }
290
+
291
+ export function reconcileProviderRequestPacing(context: GenerationContext): number {
292
+ if (context.generation <= lastReconciledGeneration) return 0;
293
+ let removed = 0;
294
+ for (const [providerName, state] of pacers) {
295
+ if (context.providerNames.has(providerName)) continue;
296
+ removeProviderPacer(providerName, state);
297
+ removed += 1;
298
+ }
299
+ lastReconciledGeneration = context.generation;
300
+ return removed;
301
+ }
302
+
303
+ export function resetProviderRequestPacingForTest(): void {
304
+ for (const state of pacers.values()) if (state.timer) runtime.clearTimer(state.timer);
305
+ pacers.clear();
306
+ maxQueueDepth = REQUEST_PACING_MAX_QUEUE_DEPTH;
307
+ maxQueueAgeMs = REQUEST_PACING_MAX_QUEUE_AGE_MS;
308
+ runtime = defaultRuntime;
309
+ lastReconciledGeneration = 0;
310
+ }
@@ -0,0 +1,143 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
3
+ import { getProviderRegistryEntry, providerModelWireDefault, type InboundWire } from "./registry";
4
+
5
+ /** OpenAI-compatible adapters that can carry the standard `service_tier` field. */
6
+ export const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
7
+
8
+ export type CapturedServiceTierAdapterAuthority = Readonly<Record<string, string>>;
9
+
10
+ const capturedAdapterAuthority = new WeakMap<object, CapturedServiceTierAdapterAuthority>();
11
+
12
+ type ServiceTierCapabilityProvider = Pick<
13
+ OcxProviderConfig,
14
+ "adapter" | "supportsServiceTier" | "modelSupportsServiceTier" | "modelAdapters" | "baseUrl" | "authMode" | "chatServiceTier"
15
+ >;
16
+
17
+ /**
18
+ * Read a model map by exact model identity. Service-tier capability is deliberately
19
+ * stricter than the older model metadata maps: a family key or a colon-qualified
20
+ * fallback must not silently advertise Fast for a sibling model that was never verified.
21
+ * A case-insensitive exact match keeps hand-edited ids consistent with the other maps
22
+ * without widening the model scope.
23
+ */
24
+ function exactModelValue<T>(
25
+ record: Record<string, T> | undefined,
26
+ modelId: string,
27
+ ): T | undefined {
28
+ if (!record) return undefined;
29
+ if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
30
+ const folded = modelId.toLowerCase();
31
+ for (const [key, value] of Object.entries(record)) {
32
+ if (key.toLowerCase() === folded) return value;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ /**
38
+ * Resolve the declared provider/model capability. An explicit provider-level false is a
39
+ * fail-closed boundary and cannot be reopened by a model map. Otherwise an exact model
40
+ * declaration wins over the provider default, including an explicit false. The resolver is
41
+ * provider-local: the caller must first resolve the final provider, so identical bare model ids
42
+ * on two providers cannot share capability state.
43
+ */
44
+ export function supportsServiceTierForModel(
45
+ provider: Pick<OcxProviderConfig, "supportsServiceTier" | "modelSupportsServiceTier">,
46
+ modelId: string,
47
+ ): boolean | undefined {
48
+ if (provider.supportsServiceTier === false) return false;
49
+ return exactModelValue(provider.modelSupportsServiceTier, modelId)
50
+ ?? provider.supportsServiceTier;
51
+ }
52
+
53
+ /** Whether the Chat serializer may emit a tier for this exact model. */
54
+ export function canSerializeServiceTierForChatModel(
55
+ provider: Pick<OcxProviderConfig, "supportsServiceTier" | "modelSupportsServiceTier" | "chatServiceTier">,
56
+ modelId: string,
57
+ ): boolean {
58
+ const exact = exactModelValue(provider.modelSupportsServiceTier, modelId);
59
+ if (provider.supportsServiceTier === false || exact === false) return false;
60
+ return provider.chatServiceTier === true || exact === true;
61
+ }
62
+
63
+ /** Capture registry-owned model wire defaults before an asynchronous catalog flight begins. */
64
+ export function captureServiceTierAdapterAuthority(
65
+ providerName: string,
66
+ provider: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode">,
67
+ registryTransportMatch: boolean,
68
+ inbound: InboundWire = "responses",
69
+ ): CapturedServiceTierAdapterAuthority {
70
+ const authority: Record<string, string> = {};
71
+ const defaults = registryTransportMatch
72
+ ? getProviderRegistryEntry(providerName)?.modelWireDefaults
73
+ : undefined;
74
+ for (const modelId of Object.keys(defaults ?? {})) {
75
+ const adapter = providerModelWireDefault(
76
+ providerName,
77
+ provider,
78
+ modelId,
79
+ MODEL_ADAPTER_OVERRIDE_ALLOWED,
80
+ inbound,
81
+ );
82
+ if (adapter !== undefined) authority[modelId.trim().toLowerCase()] = adapter;
83
+ }
84
+ const frozen = Object.freeze(authority);
85
+ capturedAdapterAuthority.set(provider, frozen);
86
+ return frozen;
87
+ }
88
+
89
+ /** Resolve an explicit model wire override for catalog-time capability projection. */
90
+ export function serviceTierAdapterForModel(
91
+ providerName: string,
92
+ provider: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode" | "modelAdapters">,
93
+ modelId: string,
94
+ inbound: InboundWire = "responses",
95
+ ): string {
96
+ // Keep this lookup identical to resolveWireProtocolOverride(): configured model-adapter
97
+ // entries are exact-case keys, while registry defaults intentionally normalize ids there.
98
+ const configured = provider.modelAdapters?.[modelId];
99
+ if (configured !== undefined && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)) return configured;
100
+ const captured = capturedAdapterAuthority.get(provider);
101
+ if (captured !== undefined) {
102
+ return captured[modelId.trim().toLowerCase()] ?? provider.adapter;
103
+ }
104
+ return providerModelWireDefault(
105
+ providerName,
106
+ provider,
107
+ modelId,
108
+ MODEL_ADAPTER_OVERRIDE_ALLOWED,
109
+ inbound,
110
+ ) ?? provider.adapter;
111
+ }
112
+
113
+ /** Whether the final provider/model pair can actually publish/send OpenAI service tiers. */
114
+ export function canForwardServiceTierForModel(
115
+ provider: ServiceTierCapabilityProvider,
116
+ modelId: string,
117
+ providerName?: string,
118
+ inbound: InboundWire = "responses",
119
+ ): boolean {
120
+ return serviceTierSupportForModel(provider, modelId, providerName, inbound) === true;
121
+ }
122
+
123
+ /**
124
+ * Return the tri-state capability after resolving the model's final wire adapter.
125
+ * `false` means either an explicit provider/model denial or an adapter that cannot carry the
126
+ * field; `undefined` keeps the existing conservative contract for an unclassified OpenAI wire.
127
+ */
128
+ export function serviceTierSupportForModel(
129
+ provider: ServiceTierCapabilityProvider,
130
+ modelId: string,
131
+ providerName?: string,
132
+ inbound: InboundWire = "responses",
133
+ ): boolean | undefined {
134
+ const adapter = providerName === undefined
135
+ ? provider.adapter
136
+ : serviceTierAdapterForModel(providerName, provider, modelId, inbound);
137
+ if (!SERVICE_TIER_ADAPTERS.has(adapter)) return false;
138
+ // Treat the Chat serializer decision as authoritative so catalog metadata, routing
139
+ // evidence, fast-mode injection, and caller-tier stripping cannot claim support that the
140
+ // final request builder will omit. A provider-wide false and an exact false stay closed.
141
+ if (adapter === "openai-chat" && !canSerializeServiceTierForChatModel(provider, modelId)) return false;
142
+ return supportsServiceTierForModel(provider, modelId);
143
+ }
@@ -0,0 +1,86 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import {
3
+ getProviderRegistryEntry,
4
+ providerMatchesRegistryTransport,
5
+ type ProviderRegistryEntry,
6
+ } from "./registry";
7
+
8
+ const STATIC_MODEL_CATALOG_PROVIDER_IDS = new Set(["cline-pass", "mimo-free"]);
9
+
10
+ function normalizedEndpoint(value: string): string {
11
+ const trimmed = value.trim();
12
+ try {
13
+ const parsed = new URL(trimmed);
14
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
15
+ return parsed.toString().replace(/\/$/, "");
16
+ } catch {
17
+ return trimmed.replace(/\/+$/, "");
18
+ }
19
+ }
20
+
21
+ function exactRegistryTransportMatch(
22
+ entry: ProviderRegistryEntry,
23
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
24
+ options: { allowLegacyMimoLocal?: boolean } = {},
25
+ ): boolean {
26
+ if (entry.allowBaseUrlOverride || /\{[^}]*\}/.test(entry.baseUrl)) return false;
27
+ if (typeof provider.baseUrl !== "string" || provider.adapter !== entry.adapter) return false;
28
+ const legacyMimoLocal = options.allowLegacyMimoLocal === true
29
+ && entry.id === "mimo-free"
30
+ && provider.authMode === "local";
31
+ if (provider.authMode !== undefined && provider.authMode !== "key" && !legacyMimoLocal) return false;
32
+ return normalizedEndpoint(provider.baseUrl) === normalizedEndpoint(entry.baseUrl);
33
+ }
34
+
35
+ /** Registry policy for providers whose maintained model list is authoritative. */
36
+ export function registryEntrySupportsLiveModelDiscovery(entry: ProviderRegistryEntry): boolean {
37
+ return !STATIC_MODEL_CATALOG_PROVIDER_IDS.has(entry.id);
38
+ }
39
+
40
+ /**
41
+ * Static-catalog authority is tied to canonical provider identity and exact transport.
42
+ * Renamed/custom rows stay operator-owned: Cline and ClinePass intentionally share a transport,
43
+ * so destination matching alone cannot safely identify a renamed ClinePass configuration.
44
+ */
45
+ export function staticModelCatalogEntryForProvider(
46
+ name: string,
47
+ provider: OcxProviderConfig,
48
+ ): ProviderRegistryEntry | undefined {
49
+ const entry = getProviderRegistryEntry(name);
50
+ if (!entry || !STATIC_MODEL_CATALOG_PROVIDER_IDS.has(entry.id)) return undefined;
51
+ return exactRegistryTransportMatch(entry, provider, { allowLegacyMimoLocal: true })
52
+ ? entry
53
+ : undefined;
54
+ }
55
+
56
+ export function providerSupportsLiveModelDiscovery(name: string, provider: OcxProviderConfig): boolean {
57
+ return staticModelCatalogEntryForProvider(name, provider) === undefined;
58
+ }
59
+
60
+ /**
61
+ * MiMo Free predates collision preservation in the registry. Keep same-named custom rows out of
62
+ * registry ownership without broadening the generic transport matcher to other key providers.
63
+ */
64
+ export function providerMatchesRegistryTransportWithStaticGuards(
65
+ name: string,
66
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
67
+ ): boolean {
68
+ if (name !== "mimo-free") return providerMatchesRegistryTransport(name, provider);
69
+ const entry = getProviderRegistryEntry(name);
70
+ return entry !== undefined
71
+ && exactRegistryTransportMatch(entry, provider, { allowLegacyMimoLocal: true });
72
+ }
73
+
74
+ /** Repair only registry-owned legacy state; operator-owned model lists stay untouched. */
75
+ export function repairStaticModelCatalogProvider(name: string, provider: OcxProviderConfig): void {
76
+ const entry = staticModelCatalogEntryForProvider(name, provider);
77
+ if (!entry) return;
78
+ provider.liveModels = false;
79
+ if (
80
+ name === "mimo-free"
81
+ && entry.id === "mimo-free"
82
+ && (provider.authMode === undefined || provider.authMode === "local")
83
+ ) {
84
+ provider.authMode = "key";
85
+ }
86
+ }
@@ -19,6 +19,30 @@ export function isCodexReasoningEffort(effort: string): boolean {
19
19
  return CODEX_REASONING_SET.has(effort);
20
20
  }
21
21
 
22
+ /**
23
+ * True for ladder members plus the `none`/`minimal` sentinels. Both are valid declared
24
+ * efforts (OpenAI accepts `minimal`; Codex validates `none` against
25
+ * `supported_reasoning_levels` for no-reasoning subagent spawns, #883/#962) but are NOT
26
+ * part of the low..ultra ladder: they never appear in default ladders, ranks, or clamps
27
+ * (`minimal` is mapped to `low` on the wire by requestToCodexEffort).
28
+ */
29
+ export function isDeclaredReasoningEffort(effort: string): boolean {
30
+ return effort === "none" || effort === "minimal" || CODEX_REASONING_SET.has(effort);
31
+ }
32
+
33
+ /**
34
+ * Reorder any declared subset (low..ultra, plus the optional `none`/`minimal` sentinels
35
+ * first, in that order) into canonical order and drop duplicates. Catalog
36
+ * `supported_reasoning_levels` follow the input order and the fallback default picks the
37
+ * first entry, so a caller-chosen order would otherwise leak into the catalog.
38
+ */
39
+ export function canonicalizeReasoningEfforts(values: readonly string[]): string[] {
40
+ const seen = new Set(values);
41
+ const ordered = CODEX_REASONING_ORDER.filter(effort => seen.has(effort));
42
+ const sentinels = ["none", "minimal"].filter(effort => seen.has(effort));
43
+ return [...sentinels, ...ordered];
44
+ }
45
+
22
46
  /**
23
47
  * Reasoning ladder accepted for the OpenAI vision sidecar. `ultra` is deliberately excluded:
24
48
  * the vision describer is a single helper call, and `ultra` would be collapsed to `max` by the
@@ -66,7 +90,9 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
66
90
  const seen = new Set<string>();
67
91
  const out: string[] = [];
68
92
  for (const effort of efforts) {
69
- if (!CODEX_REASONING_SET.has(effort) || seen.has(effort)) continue;
93
+ // `none`/`minimal` are valid declared sentinels, kept and sorted first (rank -1); they
94
+ // never appear in the default ladder.
95
+ if ((effort !== "none" && effort !== "minimal" && !CODEX_REASONING_SET.has(effort)) || seen.has(effort)) continue;
70
96
  seen.add(effort);
71
97
  out.push(effort);
72
98
  }
@@ -36,6 +36,17 @@ export interface ResponseSpillPayload {
36
36
  createdAt: number;
37
37
  clientThreadId?: string;
38
38
  items: unknown[];
39
+ /**
40
+ * Index in `items` where the provider output begins, used by replay-overlap detection
41
+ * in state.ts. Optional so a payload written before this field still loads (it simply
42
+ * never authorizes a skip).
43
+ *
44
+ * Compatibility is FORWARD-ONLY: `validPayload` is a strict key allowlist, so a build
45
+ * predating this field rejects a payload carrying it as corrupt rather than ignoring
46
+ * it. Rolling back across this change invalidates spilled entries, which degrades to a
47
+ * replay miss — an already-handled path — not to corrupted live state.
48
+ */
49
+ providerOutputStart?: number;
39
50
  providers?: OcxProviderContinuationState;
40
51
  }
41
52
 
@@ -261,12 +272,19 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
261
272
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
262
273
  const payload = value as Record<string, unknown>;
263
274
  const keys = Object.keys(payload);
264
- if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providers"].includes(key))) return false;
275
+ if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providerOutputStart", "providers"].includes(key))) return false;
265
276
  if (payload.version !== 1 || payload.responseId !== responseId) return false;
266
277
  if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false;
267
278
  if (payload.clientThreadId !== undefined
268
279
  && (typeof payload.clientThreadId !== "string" || payload.clientThreadId.trim().length === 0)) return false;
269
280
  if (!Array.isArray(payload.items)) return false;
281
+ // A malformed boundary must degrade to "never skip", never to a bad index: reject the
282
+ // payload outright so materialization treats it as corrupt rather than trusting it.
283
+ if (payload.providerOutputStart !== undefined) {
284
+ const anchor = payload.providerOutputStart;
285
+ if (typeof anchor !== "number" || !Number.isSafeInteger(anchor)
286
+ || anchor < 0 || anchor > payload.items.length) return false;
287
+ }
270
288
  if (payload.providers !== undefined) {
271
289
  if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false;
272
290
  for (const providerState of Object.values(payload.providers)) {
@@ -289,6 +307,7 @@ export function writeResponseSpillDurably(
289
307
  createdAt: state.createdAt,
290
308
  ...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}),
291
309
  items: state.items,
310
+ ...(state.providerOutputStart !== undefined ? { providerOutputStart: state.providerOutputStart } : {}),
292
311
  ...(state.providers ? { providers: state.providers } : {}),
293
312
  };
294
313
  const serialized = JSON.stringify(payload);