@bitkyc08/opencodex 2.19.0 → 2.21.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 (130) hide show
  1. package/gui/dist/assets/index-BOFeam5a.js +76 -0
  2. package/gui/dist/assets/index-Xq49CY8F.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +2 -28
  6. package/src/adapters/google.ts +31 -3
  7. package/src/adapters/mimo-free.ts +17 -0
  8. package/src/adapters/openai-chat.ts +392 -40
  9. package/src/adapters/registry.ts +144 -0
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/chat/inbound.ts +13 -7
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +3 -2
  15. package/src/cli/combo.ts +3 -0
  16. package/src/cli/dispatch.ts +8 -0
  17. package/src/cli/export-command.ts +2 -2
  18. package/src/cli/help.ts +2 -0
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/lab.ts +135 -1
  21. package/src/cli/minimax.ts +491 -0
  22. package/src/cli/models-runtime.ts +22 -1
  23. package/src/cli/models.ts +67 -2
  24. package/src/cli/opencode.ts +2 -1
  25. package/src/cli/registry.ts +22 -2
  26. package/src/clients/config-export.ts +125 -7
  27. package/src/codex/app-server-processes.ts +57 -2
  28. package/src/codex/app-server-restart-service.ts +232 -0
  29. package/src/codex/catalog/aggregation.ts +10 -1
  30. package/src/codex/catalog/effort.ts +15 -3
  31. package/src/codex/catalog/parsing.ts +3 -1
  32. package/src/codex/catalog/provider-fetch.ts +45 -5
  33. package/src/codex/catalog/sync.ts +74 -4
  34. package/src/codex/convergence.ts +2 -0
  35. package/src/combos/index.ts +1 -0
  36. package/src/combos/request.ts +30 -0
  37. package/src/combos/types.ts +6 -0
  38. package/src/config.ts +52 -0
  39. package/src/generated/compatibility-version.json +241 -85
  40. package/src/images/loop.ts +26 -6
  41. package/src/integrations/registry.ts +7 -0
  42. package/src/lab/conformance/jcs.ts +42 -2
  43. package/src/lab/conformance/negative-controls.ts +6 -2
  44. package/src/lab/conformance/runner.ts +16 -5
  45. package/src/lab/fabric/observe.ts +49 -14
  46. package/src/lab/index.ts +16 -0
  47. package/src/lab/ledger/purge.ts +152 -83
  48. package/src/lab/ledger/store.ts +168 -54
  49. package/src/lab/observe/from-conformance.ts +8 -6
  50. package/src/lab/observe/from-live.ts +8 -2
  51. package/src/lab/paths.ts +23 -0
  52. package/src/lab/public/bundle.ts +217 -0
  53. package/src/lab/public/community-authority.ts +175 -0
  54. package/src/lab/public/community-files.ts +29 -0
  55. package/src/lab/public/community.ts +479 -0
  56. package/src/lab/public/file-safety.ts +155 -0
  57. package/src/lab/public/ids.ts +26 -0
  58. package/src/lab/public/index.ts +16 -0
  59. package/src/lab/public/mutation-lock.ts +424 -0
  60. package/src/lab/public/operator.ts +353 -0
  61. package/src/lab/public/origin-purge.ts +79 -0
  62. package/src/lab/public/origin.ts +203 -0
  63. package/src/lab/public/privacy.ts +143 -0
  64. package/src/lab/public/private-file.ts +261 -0
  65. package/src/lab/public/project.ts +124 -0
  66. package/src/lab/public/purge-test-fault.ts +21 -0
  67. package/src/lab/public/purge.ts +223 -0
  68. package/src/lab/public/registry.ts +44 -0
  69. package/src/lab/public/revocation.ts +252 -0
  70. package/src/lab/public/signature.ts +219 -0
  71. package/src/lab/public/storage.ts +105 -0
  72. package/src/lab/public/strict-json.ts +206 -0
  73. package/src/lab/public/time.ts +26 -0
  74. package/src/lab/public/types.ts +172 -0
  75. package/src/lab/public/validate.ts +391 -0
  76. package/src/lib/codex-restart-contract.ts +120 -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 +14 -4
  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/custom-tool-compat.ts +4 -1
  91. package/src/responses/parser.ts +7 -1
  92. package/src/responses/provider-opaque-metadata.ts +73 -0
  93. package/src/responses/schema.ts +6 -0
  94. package/src/router.ts +23 -6
  95. package/src/routing/capability.ts +4 -2
  96. package/src/routing/compatibility/behavior.ts +5 -1
  97. package/src/server/adapter-resolve.ts +2 -32
  98. package/src/server/auth-cors.ts +50 -6
  99. package/src/server/chat-completions.ts +74 -36
  100. package/src/server/chat-native-sse.ts +331 -0
  101. package/src/server/chat-native.ts +371 -0
  102. package/src/server/management/combo-routes.ts +16 -2
  103. package/src/server/management/config-routes.ts +6 -4
  104. package/src/server/management/context.ts +17 -0
  105. package/src/server/management/lab-routes.ts +181 -19
  106. package/src/server/management/model-routes.ts +76 -2
  107. package/src/server/management/model-rows.ts +8 -0
  108. package/src/server/management/provider-capability-config.ts +48 -0
  109. package/src/server/management/provider-routes.ts +76 -4
  110. package/src/server/management/system-restart.ts +4 -2
  111. package/src/server/management/system-routes.ts +38 -0
  112. package/src/server/relay.ts +17 -3
  113. package/src/server/responses/compact.ts +4 -1
  114. package/src/server/responses/core.ts +257 -46
  115. package/src/server/responses/empty-completion-guard.ts +276 -0
  116. package/src/server/responses/fetch-helpers.ts +35 -4
  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/server/system-env.ts +1 -1
  123. package/src/types.ts +84 -5
  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/web-search/loop.ts +21 -5
  129. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  130. 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
  }
@@ -70,6 +70,9 @@ function rewriteForUpstream(
70
70
  || isPlainObject(value.format)
71
71
  || isPlainObject(value.parameters);
72
72
  if (!isDefinition) return { ...rest, type: "function" };
73
+ const inputDescription = value.name === "exec"
74
+ ? "JavaScript source for unified exec. Use await tools.exec_command(...) for shell commands and text(...) to return textual output; do not provide a bare shell command."
75
+ : "Raw input for this client-executed custom tool.";
73
76
  return {
74
77
  ...rest,
75
78
  type: "function",
@@ -78,7 +81,7 @@ function rewriteForUpstream(
78
81
  properties: {
79
82
  input: {
80
83
  type: "string",
81
- description: "Raw input for this client-executed custom tool.",
84
+ description: inputDescription,
82
85
  },
83
86
  },
84
87
  required: ["input"],
@@ -12,6 +12,7 @@ import type {
12
12
  } from "../types";
13
13
  import { namespacedToolName } from "../types";
14
14
  import { responsesRequestSchema } from "./schema";
15
+ import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
15
16
  import { compactionItemToText } from "./compaction";
16
17
  import { previousResponseReplayPrefixLength } from "./state";
17
18
  import { decodeReasoningEnvelope } from "./reasoning-envelope";
@@ -498,7 +499,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
498
499
  }
499
500
 
500
501
  if (effectiveType === "function_call") {
501
- const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string };
502
+ const call = item as { id?: string; call_id: string; name: string; arguments?: string; namespace?: string; extra_content?: unknown };
502
503
  // Tolerate empty/non-JSON arguments (e.g. a no-arg tool call serialized as "") instead of
503
504
  // throwing — a single poisoned history item would otherwise 400 every subsequent turn.
504
505
  let args: Record<string, unknown> = {};
@@ -519,6 +520,11 @@ export function parseRequest(body: unknown): OcxParsedRequest {
519
520
  type: "toolCall", id: call.call_id, name: call.name, arguments: args,
520
521
  ...(call.namespace ? { namespace: call.namespace } : {}),
521
522
  };
523
+ // Provider-opaque metadata (e.g. a Gemini thought signature) travels with the call so a
524
+ // history-replayed or previous_response_id turn rebuilds the same signed part instead of
525
+ // depending on the same-process replay cache (issue #1735).
526
+ const providerMetadata = providerMetadataFromResponsesFunctionCall(call);
527
+ if (providerMetadata) toolCall.providerMetadata = providerMetadata;
522
528
  assistantHolderWithReasoning().content.push(toolCall);
523
529
  continue;
524
530
  }