@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.
- package/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +13 -7
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +224 -76
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +7 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/types.ts +66 -4
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- 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
|
+
}
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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 || !
|
|
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
|
-
:
|
|
264
|
-
?
|
|
265
|
-
|
|
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 } : {}),
|