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