@bitkyc08/opencodex 2.19.0 → 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 (119) 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/mimo-free.ts +17 -0
  6. package/src/adapters/openai-chat.ts +367 -32
  7. package/src/adapters/registry.ts +144 -0
  8. package/src/chat/inbound.ts +13 -7
  9. package/src/cli/claude.ts +2 -1
  10. package/src/cli/combo.ts +3 -0
  11. package/src/cli/dispatch.ts +8 -0
  12. package/src/cli/export-command.ts +2 -2
  13. package/src/cli/help.ts +2 -0
  14. package/src/cli/index.ts +3 -2
  15. package/src/cli/lab.ts +135 -1
  16. package/src/cli/minimax.ts +491 -0
  17. package/src/cli/models-runtime.ts +22 -1
  18. package/src/cli/models.ts +67 -2
  19. package/src/cli/opencode.ts +2 -1
  20. package/src/cli/registry.ts +22 -2
  21. package/src/clients/config-export.ts +125 -7
  22. package/src/codex/app-server-processes.ts +57 -2
  23. package/src/codex/app-server-restart-service.ts +232 -0
  24. package/src/codex/catalog/aggregation.ts +10 -1
  25. package/src/codex/catalog/effort.ts +15 -3
  26. package/src/codex/catalog/parsing.ts +3 -1
  27. package/src/codex/catalog/provider-fetch.ts +45 -5
  28. package/src/codex/catalog/sync.ts +74 -4
  29. package/src/codex/convergence.ts +2 -0
  30. package/src/combos/index.ts +1 -0
  31. package/src/combos/request.ts +30 -0
  32. package/src/combos/types.ts +6 -0
  33. package/src/config.ts +52 -0
  34. package/src/generated/compatibility-version.json +224 -76
  35. package/src/images/loop.ts +11 -1
  36. package/src/integrations/registry.ts +7 -0
  37. package/src/lab/conformance/jcs.ts +42 -2
  38. package/src/lab/conformance/negative-controls.ts +6 -2
  39. package/src/lab/conformance/runner.ts +16 -5
  40. package/src/lab/fabric/observe.ts +49 -14
  41. package/src/lab/index.ts +16 -0
  42. package/src/lab/ledger/purge.ts +152 -83
  43. package/src/lab/ledger/store.ts +168 -54
  44. package/src/lab/observe/from-conformance.ts +8 -6
  45. package/src/lab/observe/from-live.ts +8 -2
  46. package/src/lab/paths.ts +23 -0
  47. package/src/lab/public/bundle.ts +217 -0
  48. package/src/lab/public/community-authority.ts +175 -0
  49. package/src/lab/public/community-files.ts +29 -0
  50. package/src/lab/public/community.ts +479 -0
  51. package/src/lab/public/file-safety.ts +155 -0
  52. package/src/lab/public/ids.ts +26 -0
  53. package/src/lab/public/index.ts +16 -0
  54. package/src/lab/public/mutation-lock.ts +424 -0
  55. package/src/lab/public/operator.ts +353 -0
  56. package/src/lab/public/origin-purge.ts +79 -0
  57. package/src/lab/public/origin.ts +203 -0
  58. package/src/lab/public/privacy.ts +143 -0
  59. package/src/lab/public/private-file.ts +261 -0
  60. package/src/lab/public/project.ts +124 -0
  61. package/src/lab/public/purge-test-fault.ts +21 -0
  62. package/src/lab/public/purge.ts +223 -0
  63. package/src/lab/public/registry.ts +44 -0
  64. package/src/lab/public/revocation.ts +252 -0
  65. package/src/lab/public/signature.ts +219 -0
  66. package/src/lab/public/storage.ts +105 -0
  67. package/src/lab/public/strict-json.ts +206 -0
  68. package/src/lab/public/time.ts +26 -0
  69. package/src/lab/public/types.ts +172 -0
  70. package/src/lab/public/validate.ts +391 -0
  71. package/src/lib/codex-restart-contract.ts +120 -0
  72. package/src/lib/lab-activation.ts +109 -47
  73. package/src/lib/lab-live-pinned-sender.ts +16 -5
  74. package/src/lib/pinned-http.ts +70 -16
  75. package/src/lib/self-launch-argv.ts +15 -0
  76. package/src/lib/state-store-registrations.ts +2 -0
  77. package/src/lib/upstream-reachability.ts +4 -0
  78. package/src/lib/windows-elevation.ts +10 -1
  79. package/src/providers/derive.ts +24 -4
  80. package/src/providers/registry.ts +7 -3
  81. package/src/providers/request-pacing.ts +310 -0
  82. package/src/providers/service-tier.ts +143 -0
  83. package/src/providers/static-model-discovery.ts +86 -0
  84. package/src/reasoning-effort.ts +27 -1
  85. package/src/router.ts +23 -6
  86. package/src/routing/capability.ts +4 -2
  87. package/src/routing/compatibility/behavior.ts +5 -1
  88. package/src/server/adapter-resolve.ts +2 -32
  89. package/src/server/auth-cors.ts +8 -0
  90. package/src/server/chat-completions.ts +74 -36
  91. package/src/server/chat-native-sse.ts +331 -0
  92. package/src/server/chat-native.ts +371 -0
  93. package/src/server/management/combo-routes.ts +16 -2
  94. package/src/server/management/config-routes.ts +6 -4
  95. package/src/server/management/context.ts +17 -0
  96. package/src/server/management/lab-routes.ts +181 -19
  97. package/src/server/management/model-routes.ts +76 -2
  98. package/src/server/management/model-rows.ts +8 -0
  99. package/src/server/management/provider-capability-config.ts +48 -0
  100. package/src/server/management/provider-routes.ts +76 -4
  101. package/src/server/management/system-restart.ts +4 -2
  102. package/src/server/management/system-routes.ts +38 -0
  103. package/src/server/relay.ts +17 -3
  104. package/src/server/responses/compact.ts +4 -1
  105. package/src/server/responses/core.ts +257 -46
  106. package/src/server/responses/empty-completion-guard.ts +276 -0
  107. package/src/server/responses/fetch-helpers.ts +35 -4
  108. package/src/server/responses/pacing-overload.ts +13 -0
  109. package/src/server/responses/policy-fallback.ts +16 -2
  110. package/src/server/responses/terminal-guard.ts +1 -1
  111. package/src/server/responses/upstream-error.ts +5 -0
  112. package/src/server/responses.ts +17 -2
  113. package/src/types.ts +66 -4
  114. package/src/update/index.ts +6 -5
  115. package/src/update/job.ts +5 -6
  116. package/src/update/notify.ts +5 -3
  117. package/src/usage/log.ts +11 -1
  118. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  119. package/gui/dist/assets/index-D_JUZLEC.js +0 -76
@@ -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
  }
package/src/router.ts CHANGED
@@ -11,8 +11,12 @@ import type { NormalizedComboConfig } from "./combos/types";
11
11
  import { hasOwnProvider, resolveEnvValue } from "./config";
12
12
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
13
13
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
14
- import { PROVIDER_REGISTRY, providerCodexAccountMode, providerMatchesRegistryTransport } from "./providers/registry";
14
+ import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
15
15
  import { applyDirectReasoningEffortContracts } from "./providers/derive";
16
+ import {
17
+ providerMatchesRegistryTransportWithStaticGuards,
18
+ providerSupportsLiveModelDiscovery,
19
+ } from "./providers/static-model-discovery";
16
20
  import {
17
21
  isCanonicalOpenAiForwardProvider,
18
22
  LEGACY_CHATGPT_PROVIDER_ID,
@@ -86,7 +90,7 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
86
90
  export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
87
91
  const ids = new Set<string>();
88
92
  for (const id of prov.models ?? []) ids.add(id);
89
- const registry = providerMatchesRegistryTransport(provName, prov)
93
+ const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov)
90
94
  ? PROVIDER_REGISTRY.find(entry => entry.id === provName)
91
95
  : undefined;
92
96
  for (const id of registry?.models ?? []) ids.add(id);
@@ -99,6 +103,7 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
99
103
  registry?.modelDefaultReasoningEfforts,
100
104
  registry?.modelReasoningEffortMap,
101
105
  registry?.modelMaxOutputTokens,
106
+ registry?.modelSupportsServiceTier,
102
107
  ]) {
103
108
  for (const id of Object.keys(map ?? {})) ids.add(id);
104
109
  }
@@ -249,20 +254,26 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
249
254
 
250
255
  export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
251
256
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
252
- if (!registryEntry || !providerMatchesRegistryTransport(providerName, provider)) {
257
+ if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
253
258
  assertProviderDestinationAllowed(providerName, provider);
254
259
  return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
255
260
  }
256
261
  const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
262
+ const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
263
+ const repairLegacyMimoFreeAuth = providerName === "mimo-free"
264
+ && staticModelCatalog
265
+ && (provider.authMode === undefined || provider.authMode === "local");
257
266
  const explicitKeyOverride = registryEntry.authKind === "oauth"
258
267
  && registryEntry.allowKeyAuthOverride === true
259
268
  && provider.authMode === "key"
260
269
  && resolvedApiKey !== undefined;
261
270
  const canonicalAuthMode = explicitKeyOverride
262
271
  ? "key"
263
- : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
264
- ? registryEntry.authKind
265
- : provider.authMode === "forward" ? undefined : provider.authMode;
272
+ : repairLegacyMimoFreeAuth
273
+ ? "key"
274
+ : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
275
+ ? registryEntry.authKind
276
+ : provider.authMode === "forward" ? undefined : provider.authMode;
266
277
  const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
267
278
  const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
268
279
  const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
@@ -283,6 +294,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
283
294
  ? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
284
295
  : mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
285
296
  const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
297
+ const modelSupportsServiceTier = mergeRecordFill(
298
+ registryEntry.modelSupportsServiceTier,
299
+ provider.modelSupportsServiceTier,
300
+ );
286
301
  const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
287
302
  const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
288
303
  const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
@@ -343,6 +358,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
343
358
  : {}),
344
359
  authMode: canonicalAuthMode,
345
360
  apiKey: resolvedApiKey,
361
+ ...(staticModelCatalog ? { liveModels: false } : {}),
346
362
  // Backfill the Google wire mode + Vertex project/location from the registry when the user
347
363
  // config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
348
364
  // through the correct branch (CCA/Vertex) instead of falling back to AI Studio.
@@ -372,6 +388,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
372
388
  ...(modelInputModalities ? { modelInputModalities } : {}),
373
389
  ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
374
390
  ...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
391
+ ...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
375
392
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
376
393
  ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
377
394
  ...(reasoningEffortMap ? { reasoningEffortMap } : {}),