@bitkyc08/opencodex 2.55.0 → 2.56.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-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type { ResponsesRequestContext } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { ResponsesTransport } from "./request-transport";
|
|
4
|
+
import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
|
|
5
|
+
import type { ResponsesEffects } from "./response-effects";
|
|
6
|
+
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
7
|
+
import { formatErrorResponse } from "../../bridge";
|
|
8
|
+
import { planWebSearch, buildWebSearchTool, runWithWebSearch } from "../../web-search";
|
|
9
|
+
import {
|
|
10
|
+
planImageBridge,
|
|
11
|
+
planVideoBridge,
|
|
12
|
+
IMAGE_GEN_TOOL_NAME,
|
|
13
|
+
buildImageTool,
|
|
14
|
+
VIDEO_GEN_TOOL_NAME,
|
|
15
|
+
buildVideoTool,
|
|
16
|
+
runWithImageBridge,
|
|
17
|
+
clampImageMaxRounds,
|
|
18
|
+
} from "../../images";
|
|
19
|
+
import type { ProviderAdapter } from "../../adapters/base";
|
|
20
|
+
import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
|
|
21
|
+
import {
|
|
22
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
23
|
+
isGenericOAuthFailoverEnabled,
|
|
24
|
+
rotateGenericOAuthAccountOn429,
|
|
25
|
+
failoverAccountSnapshot,
|
|
26
|
+
} from "../../oauth/generic-account-failover";
|
|
27
|
+
import {
|
|
28
|
+
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
29
|
+
rotateAnthropicAccountOn429,
|
|
30
|
+
getAnthropicPoolAccessSnapshot,
|
|
31
|
+
formatAnthropicProviderForLog,
|
|
32
|
+
} from "../../oauth/anthropic-routing";
|
|
33
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
34
|
+
import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from "./core-replay";
|
|
35
|
+
import { namespacedToolName } from "../../types";
|
|
36
|
+
import { providerFetch } from "./fetch-helpers";
|
|
37
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
38
|
+
import { noteAttemptSend, recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
39
|
+
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
40
|
+
import { rememberResponseState } from "../../responses/state";
|
|
41
|
+
import { trackStreamLifetime } from "../lifecycle";
|
|
42
|
+
|
|
43
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
44
|
+
export async function executeResponsesSidecars(
|
|
45
|
+
requestContext: Pick<ResponsesRequestContext, "config" | "options" | "logCtx">,
|
|
46
|
+
requestState: Pick<
|
|
47
|
+
PreparedResponsesRequest,
|
|
48
|
+
| "parsed"
|
|
49
|
+
| "route"
|
|
50
|
+
| "inboundWire"
|
|
51
|
+
| "selectedForwardHeaders"
|
|
52
|
+
| "translatorBudget"
|
|
53
|
+
| "rememberKiroDeliveredFinalAnswer"
|
|
54
|
+
| "responseStateOptions"
|
|
55
|
+
>,
|
|
56
|
+
transportState: Pick<
|
|
57
|
+
ResponsesTransport,
|
|
58
|
+
| "adapter"
|
|
59
|
+
| "genericFailoverAccountId"
|
|
60
|
+
| "genericFailovers"
|
|
61
|
+
| "applyFailoverSnapshot"
|
|
62
|
+
| "anthropicPoolAccountId"
|
|
63
|
+
| "anthropicPoolFailovers"
|
|
64
|
+
| "anthropicSessionKey"
|
|
65
|
+
| "commitResolvedOAuthSelection"
|
|
66
|
+
| "resolveSelectionAdapter"
|
|
67
|
+
| "oauthDispatch"
|
|
68
|
+
>,
|
|
69
|
+
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction" | "openAiSidecar">,
|
|
70
|
+
responseEffects: Pick<
|
|
71
|
+
ResponsesEffects,
|
|
72
|
+
| "commitReasoningReplayServingRoute"
|
|
73
|
+
| "continuationStateForResponse"
|
|
74
|
+
| "notifyResponseComplete"
|
|
75
|
+
| "cancelResponseCompletion"
|
|
76
|
+
>,
|
|
77
|
+
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
|
|
78
|
+
) {
|
|
79
|
+
const { config, options, logCtx } = requestContext;
|
|
80
|
+
const {
|
|
81
|
+
applyFailoverSnapshot,
|
|
82
|
+
anthropicSessionKey,
|
|
83
|
+
commitResolvedOAuthSelection,
|
|
84
|
+
resolveSelectionAdapter,
|
|
85
|
+
oauthDispatch,
|
|
86
|
+
} = transportState;
|
|
87
|
+
const {
|
|
88
|
+
parsed,
|
|
89
|
+
route,
|
|
90
|
+
inboundWire,
|
|
91
|
+
translatorBudget,
|
|
92
|
+
rememberKiroDeliveredFinalAnswer,
|
|
93
|
+
responseStateOptions,
|
|
94
|
+
} = requestState;
|
|
95
|
+
const { routedCompaction, openAiSidecar } = sidecarState;
|
|
96
|
+
const { reserveCredentialHop } = sendBudgetState;
|
|
97
|
+
const {
|
|
98
|
+
commitReasoningReplayServingRoute,
|
|
99
|
+
continuationStateForResponse,
|
|
100
|
+
notifyResponseComplete,
|
|
101
|
+
cancelResponseCompletion,
|
|
102
|
+
} = responseEffects;
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId
|
|
106
|
+
// (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all
|
|
107
|
+
// (schema.ts:106) accepts a tool item whose strict schema failed only for a missing call_id. A
|
|
108
|
+
// translating adapter then consumes `toolCallId: string` holding undefined: kiro-wire.ts:32
|
|
109
|
+
// TypeErrors, ollama-native.ts:334 throws, and anthropic.ts:775 sends
|
|
110
|
+
// "[tool_result without adjacent tool_use: undefined]" upstream (issue #3259).
|
|
111
|
+
//
|
|
112
|
+
// This CANNOT move into the schema. parseRequest (:2812) runs before the passthrough branch
|
|
113
|
+
// (:3719), so a parse-time rejection would also kill forward/key passthrough and routed
|
|
114
|
+
// compaction — paths that never read context.messages, build from _rawBody, and already
|
|
115
|
+
// degrade an unpaired output to "[tool output for unknown call]" on their own.
|
|
116
|
+
//
|
|
117
|
+
// Keyed on the adapter, not on position: routedCompaction skips the passthrough branch above
|
|
118
|
+
// yet still builds from _rawBody (see the :3703 comment).
|
|
119
|
+
if (!("passthrough" in transportState.adapter && transportState.adapter.passthrough)) {
|
|
120
|
+
const unpaired = parsed.context.messages.find(
|
|
121
|
+
message => message.role === "toolResult"
|
|
122
|
+
&& (typeof (message as { toolCallId?: unknown }).toolCallId !== "string"
|
|
123
|
+
|| (message as { toolCallId: string }).toolCallId.length === 0),
|
|
124
|
+
);
|
|
125
|
+
if (unpaired) {
|
|
126
|
+
// Never interpolate the tool output: this message reaches the client and the logs.
|
|
127
|
+
return formatErrorResponse(
|
|
128
|
+
400,
|
|
129
|
+
"invalid_request_error",
|
|
130
|
+
"tool result requires a non-empty string call_id",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Image / web-search sidecars: plan once, then dispatch with runTurn-aware priority.
|
|
136
|
+
// Routed-compaction turns must NOT hit the image bridge: compaction clears tools/_webSearch but
|
|
137
|
+
// leaves _imageGeneration, so planImageBridge would activate and return a normal Responses
|
|
138
|
+
// completion instead of the synthetic compaction item Codex expects (#424).
|
|
139
|
+
//
|
|
140
|
+
// Web-search's loop only supports buildRequest/fetch/parseStream — NOT adapter.runTurn. Sending
|
|
141
|
+
// Cursor/runTurn requests into runWithWebSearch produces empty HTTP failures. So:
|
|
142
|
+
// - non-runTurn: web-search wins over image when both eligible (documented priority)
|
|
143
|
+
// - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
|
|
144
|
+
// can proceed for web-search-only turns
|
|
145
|
+
const wsPlan = !routedCompaction
|
|
146
|
+
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, {
|
|
147
|
+
admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, providerName: route.providerName,
|
|
148
|
+
})
|
|
149
|
+
: undefined;
|
|
150
|
+
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
151
|
+
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
152
|
+
const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
|
|
153
|
+
const rotateSidecarProviderOn429 = async (
|
|
154
|
+
retryAfter: string | null,
|
|
155
|
+
responseHeaders?: Headers,
|
|
156
|
+
): Promise<ProviderAdapter | null> => {
|
|
157
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
158
|
+
retryAfter,
|
|
159
|
+
now: Date.now(),
|
|
160
|
+
attemptedKey: route.provider.apiKey,
|
|
161
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
162
|
+
});
|
|
163
|
+
if (rotated) {
|
|
164
|
+
route.provider = rotated;
|
|
165
|
+
} else if (
|
|
166
|
+
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
167
|
+
// unreachable: Anthropic never has a genericFailoverAccountId (isGenericFailoverProvider
|
|
168
|
+
// excludes it), so its sidecar 429s died on this guard before the Anthropic arm below
|
|
169
|
+
// could ever be considered.
|
|
170
|
+
transportState.genericFailoverAccountId
|
|
171
|
+
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
172
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
173
|
+
) {
|
|
174
|
+
// Intersection with the request's shared budget. The sidecar replay is dispatched by the
|
|
175
|
+
// web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
|
|
176
|
+
// charge; a refusal returns null and the caller keeps the real 429 it already has.
|
|
177
|
+
const hop = reserveCredentialHop(
|
|
178
|
+
"auth-recovery",
|
|
179
|
+
`${route.providerName}|${route.modelId}|sidecar-oauth-429`,
|
|
180
|
+
);
|
|
181
|
+
if (!hop.allowed) return null;
|
|
182
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
183
|
+
config,
|
|
184
|
+
route.providerName,
|
|
185
|
+
transportState.genericFailoverAccountId,
|
|
186
|
+
retryAfter,
|
|
187
|
+
);
|
|
188
|
+
if (!nextAccountId) {
|
|
189
|
+
hop.permit?.release();
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
194
|
+
transportState.genericFailovers += 1;
|
|
195
|
+
if (!await applyFailoverSnapshot(snapshot)) {
|
|
196
|
+
hop.permit?.release();
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
hop.permit?.release();
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
hop.permit?.use();
|
|
204
|
+
} else if (
|
|
205
|
+
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
206
|
+
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
207
|
+
// the very same 429 on the main response path rotated.
|
|
208
|
+
transportState.anthropicPoolAccountId
|
|
209
|
+
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
210
|
+
) {
|
|
211
|
+
// Same intersection for the Anthropic roster: its own per-request bound still applies,
|
|
212
|
+
// and the shared budget decides whether this request may spend another send at all.
|
|
213
|
+
const hop = reserveCredentialHop(
|
|
214
|
+
"auth-recovery",
|
|
215
|
+
`${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
|
|
216
|
+
);
|
|
217
|
+
if (!hop.allowed) return null;
|
|
218
|
+
const nextAccountId = rotateAnthropicAccountOn429(
|
|
219
|
+
config,
|
|
220
|
+
transportState.anthropicPoolAccountId,
|
|
221
|
+
retryAfter,
|
|
222
|
+
anthropicSessionKey,
|
|
223
|
+
Date.now(),
|
|
224
|
+
responseHeaders,
|
|
225
|
+
);
|
|
226
|
+
if (!nextAccountId) {
|
|
227
|
+
hop.permit?.release();
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
// Deliberately NOT applyFailoverSnapshot: that helper exists to pair per-account routing
|
|
232
|
+
// metadata (Copilot origin, Antigravity project, Kiro context) with its bearer. Anthropic
|
|
233
|
+
// carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
|
|
234
|
+
// local-cli credential rule. Both existing Anthropic rotation sites apply the token the
|
|
235
|
+
// same way.
|
|
236
|
+
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
237
|
+
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
238
|
+
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
239
|
+
transportState.anthropicPoolFailovers += 1;
|
|
240
|
+
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
241
|
+
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
242
|
+
} catch {
|
|
243
|
+
hop.permit?.release();
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
hop.permit?.use();
|
|
247
|
+
} else {
|
|
248
|
+
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
249
|
+
// credential. The 429 is terminal for this sidecar turn.
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const rotatedAdapter = resolveSelectionAdapter(
|
|
253
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
254
|
+
config.cacheRetention,
|
|
255
|
+
);
|
|
256
|
+
bindRouteReasoningReplayScope({
|
|
257
|
+
parsed,
|
|
258
|
+
providerName: route.providerName,
|
|
259
|
+
provider: route.provider,
|
|
260
|
+
adapterName: rotatedAdapter.name,
|
|
261
|
+
});
|
|
262
|
+
return rotatedAdapter;
|
|
263
|
+
};
|
|
264
|
+
if ((imgPlan || vidPlan) && canRunWebSearch) {
|
|
265
|
+
// Web search takes priority when both are active — the media bridge cannot run
|
|
266
|
+
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
267
|
+
// configured video/image bridge was skipped for this turn, rather than silently
|
|
268
|
+
// dropping a paid capability.
|
|
269
|
+
if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
|
|
270
|
+
if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
|
|
271
|
+
}
|
|
272
|
+
if ((imgPlan || vidPlan) && (!wsPlan || transportState.adapter.runTurn)) {
|
|
273
|
+
// The image bridge detects a hosted image_generation tool and requires streaming.
|
|
274
|
+
// The video bridge activates from config and injects a tool — it also needs streaming
|
|
275
|
+
// (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
|
|
276
|
+
// the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
|
|
277
|
+
if (!parsed.stream) {
|
|
278
|
+
if (imgPlan) {
|
|
279
|
+
return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
|
|
280
|
+
}
|
|
281
|
+
// Video-only: skip bridge for non-streaming requests
|
|
282
|
+
} else {
|
|
283
|
+
// Replace any pre-existing image_gen/video_gen aliases instead of appending duplicate wire names.
|
|
284
|
+
const priorTools = parsed.context.tools ?? [];
|
|
285
|
+
const bridgeTools = [...priorTools.filter(t => {
|
|
286
|
+
if (t.imageGeneration) return false;
|
|
287
|
+
if (t.videoGeneration) return false;
|
|
288
|
+
if (imgPlan && imgPlan.toolNames.has(t.name)) return false;
|
|
289
|
+
if (imgPlan && t.namespace && imgPlan.toolNames.has(namespacedToolName(t.namespace, t.name))) return false;
|
|
290
|
+
// Only strip unnamespaced video_gen aliases — a namespaced MCP video_gen is left alone.
|
|
291
|
+
if (vidPlan && !t.namespace && vidPlan.toolNames.has(t.name)) return false;
|
|
292
|
+
return true;
|
|
293
|
+
})];
|
|
294
|
+
const existingNames = new Set(bridgeTools.map(t => t.name));
|
|
295
|
+
if (imgPlan && !existingNames.has(IMAGE_GEN_TOOL_NAME)) bridgeTools.push(buildImageTool());
|
|
296
|
+
if (vidPlan && !existingNames.has(VIDEO_GEN_TOOL_NAME)) bridgeTools.push(buildVideoTool());
|
|
297
|
+
parsed.context.tools = bridgeTools;
|
|
298
|
+
// Hosted image_generation tool_choice / allowed_tools must target the synthetic function name.
|
|
299
|
+
// Gate on imgPlan — in a video-only turn buildImageTool() was never injected, so rewriting
|
|
300
|
+
// image_generation/image_gen aliases would add an undeclared tool that strict upstreams reject.
|
|
301
|
+
const tc = parsed.options.toolChoice;
|
|
302
|
+
if (imgPlan && tc && typeof tc === "object" && "allowedTools" in tc && Array.isArray(tc.allowedTools)) {
|
|
303
|
+
const mapped = tc.allowedTools.map(name =>
|
|
304
|
+
name === "image_generation" || name === "image_gen" || (imgPlan.toolNames.has(name) ?? false)
|
|
305
|
+
? IMAGE_GEN_TOOL_NAME
|
|
306
|
+
: name,
|
|
307
|
+
);
|
|
308
|
+
parsed.options.toolChoice = { ...tc, allowedTools: [...new Set(mapped)] };
|
|
309
|
+
} else if (imgPlan && tc && typeof tc === "object" && "name" in tc && typeof tc.name === "string"
|
|
310
|
+
&& (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
|
|
311
|
+
parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
|
|
312
|
+
}
|
|
313
|
+
const imageProviderFetch = providerFetch(
|
|
314
|
+
route.provider,
|
|
315
|
+
options.codexWsRuntimeIdentity,
|
|
316
|
+
{ providerName: route.providerName, modelId: route.modelId },
|
|
317
|
+
);
|
|
318
|
+
const imgResponse = await runWithImageBridge({
|
|
319
|
+
parsed, adapter: transportState.adapter,
|
|
320
|
+
incomingMeta: { headers: requestState.selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
|
|
321
|
+
...(imgPlan ? { plan: imgPlan } : {}),
|
|
322
|
+
...(vidPlan ? { videoPlan: vidPlan } : {}),
|
|
323
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
324
|
+
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
325
|
+
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
|
|
326
|
+
abortSignal: options.abortSignal,
|
|
327
|
+
maxRounds: imgPlan && vidPlan
|
|
328
|
+
? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
|
|
329
|
+
: imgPlan
|
|
330
|
+
? clampImageMaxRounds(config.images?.maxRounds)
|
|
331
|
+
: clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
|
|
332
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
333
|
+
stallTimeoutSec: config.stallTimeoutSec,
|
|
334
|
+
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
335
|
+
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
336
|
+
fetchForRequest: (request, iterParsed) => {
|
|
337
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
338
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
339
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
340
|
+
});
|
|
341
|
+
return fetch.unpacedFetch ?? fetch;
|
|
342
|
+
},
|
|
343
|
+
onRequestBuilt: request => {
|
|
344
|
+
recordAdapterReasoning(logCtx, request);
|
|
345
|
+
recordAdapterTier(logCtx, request);
|
|
346
|
+
},
|
|
347
|
+
...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
|
|
348
|
+
onUsage: usage => {
|
|
349
|
+
// Cursor may assign _cursorConversationId inside the image loop's first runTurn;
|
|
350
|
+
// backfill so Logs can filter/total that opening request (parity with the normal
|
|
351
|
+
// runTurn branch).
|
|
352
|
+
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
353
|
+
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
354
|
+
}
|
|
355
|
+
logCtx.usageFromBridge = true;
|
|
356
|
+
if (usage) {
|
|
357
|
+
logCtx.usage = usage;
|
|
358
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
on429: rotateSidecarProviderOn429,
|
|
362
|
+
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
363
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
364
|
+
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
365
|
+
onCompletedResponse: (response, providerState) => {
|
|
366
|
+
commitReasoningReplayServingRoute();
|
|
367
|
+
rememberKiroDeliveredFinalAnswer(transportState.adapter.name, response);
|
|
368
|
+
rememberResponseState(
|
|
369
|
+
parsed._rawBody,
|
|
370
|
+
response,
|
|
371
|
+
continuationStateForResponse(providerState),
|
|
372
|
+
responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
|
|
373
|
+
);
|
|
374
|
+
notifyResponseComplete(response);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
if (imgResponse.body) {
|
|
378
|
+
const imgTurnAc = new AbortController();
|
|
379
|
+
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
380
|
+
return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), {
|
|
381
|
+
status: imgResponse.status,
|
|
382
|
+
headers: imgResponse.headers,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return imgResponse;
|
|
386
|
+
} // end else (streaming bridge)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
|
|
390
|
+
// run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
|
|
391
|
+
// through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
|
|
392
|
+
// Placed BEFORE the runTurn early-return for non-runTurn adapters so dual-tool turns dispatch
|
|
393
|
+
// through web-search instead of being swallowed. runTurn adapters never enter this branch.
|
|
394
|
+
if (canRunWebSearch && wsPlan) {
|
|
395
|
+
parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
396
|
+
// Resolve the mutable route at send time: a 429 rotation replaces route.provider, so retaining
|
|
397
|
+
// one pre-rotation providerFetch would keep the old credential and transport pin.
|
|
398
|
+
const routedProviderFetch = ((input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
|
|
399
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
400
|
+
providerName: route.providerName,
|
|
401
|
+
modelId: route.modelId,
|
|
402
|
+
})(input, init)) as typeof globalThis.fetch;
|
|
403
|
+
const wsResponse = await runWithWebSearch({
|
|
404
|
+
parsed, adapter: transportState.adapter,
|
|
405
|
+
fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
406
|
+
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
407
|
+
providerName: route.providerName, modelId: route.modelId,
|
|
408
|
+
}),
|
|
409
|
+
incomingMeta: {
|
|
410
|
+
headers: requestState.selectedForwardHeaders,
|
|
411
|
+
abortSignal: options.abortSignal,
|
|
412
|
+
translatorBudget,
|
|
413
|
+
providerFetch: routedProviderFetch,
|
|
414
|
+
},
|
|
415
|
+
backend: wsPlan.backend,
|
|
416
|
+
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
417
|
+
anthropicSidecar: wsPlan.anthropicSidecar,
|
|
418
|
+
xaiSidecar: wsPlan.xaiSidecar,
|
|
419
|
+
geminiSidecar: wsPlan.geminiSidecar,
|
|
420
|
+
xaiSearchOptions: wsPlan.xaiSearchOptions,
|
|
421
|
+
// The exa key never rides the plan: read it from config at unpack time (L9).
|
|
422
|
+
...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
|
|
423
|
+
hostedTool: wsPlan.hostedTool,
|
|
424
|
+
selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? requestState.selectedForwardHeaders,
|
|
425
|
+
settings: wsPlan.settings,
|
|
426
|
+
maxSearches: wsPlan.maxSearches,
|
|
427
|
+
forceEmptyResponseId: true,
|
|
428
|
+
abortSignal: options.abortSignal,
|
|
429
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
430
|
+
onRequestBuilt: request => {
|
|
431
|
+
recordAdapterReasoning(logCtx, request);
|
|
432
|
+
recordAdapterTier(logCtx, request);
|
|
433
|
+
},
|
|
434
|
+
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
435
|
+
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery),
|
|
436
|
+
onUsage: usage => {
|
|
437
|
+
logCtx.usageFromBridge = true;
|
|
438
|
+
if (usage) {
|
|
439
|
+
logCtx.usage = usage;
|
|
440
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
444
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
445
|
+
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
446
|
+
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
447
|
+
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
448
|
+
on429: rotateSidecarProviderOn429,
|
|
449
|
+
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
450
|
+
onCompletedResponse: response => {
|
|
451
|
+
commitReasoningReplayServingRoute();
|
|
452
|
+
notifyResponseComplete(response);
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
456
|
+
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
457
|
+
if (wsResponse.body) {
|
|
458
|
+
const wsTurnAc = new AbortController();
|
|
459
|
+
wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
460
|
+
return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), {
|
|
461
|
+
status: wsResponse.status,
|
|
462
|
+
headers: wsResponse.headers,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return wsResponse;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
@@ -2,7 +2,7 @@ import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
|
2
2
|
import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
|
|
3
3
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
4
4
|
|
|
5
|
-
interface NamespacedTool {
|
|
5
|
+
export interface NamespacedTool {
|
|
6
6
|
namespace: string;
|
|
7
7
|
name: string;
|
|
8
8
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that knows how this proxy refuses a turn on its own workflow budget.
|
|
3
|
+
*
|
|
4
|
+
* It is a module rather than two inline blocks because the two call sites -- the HTTP admission
|
|
5
|
+
* check in `src/server/index.ts` and the pre-dispatch ceiling check in
|
|
6
|
+
* `src/server/responses/core.ts` -- had drifted into saying different things about the same
|
|
7
|
+
* refusal, and because the non-obvious part below has to be stated once and not twice.
|
|
8
|
+
*/
|
|
9
|
+
import { formatErrorResponse } from "../bridge";
|
|
10
|
+
import {
|
|
11
|
+
addFinalRequestLog,
|
|
12
|
+
markLocalRequestLogRefusal,
|
|
13
|
+
type RequestLogContext,
|
|
14
|
+
} from "./request-log";
|
|
15
|
+
import {
|
|
16
|
+
WORKFLOW_LOCAL_REFUSAL_HEADER,
|
|
17
|
+
workflowDenialSummary,
|
|
18
|
+
recordWorkflowRefusalEvent,
|
|
19
|
+
type WorkflowDenial,
|
|
20
|
+
} from "../lib/workflow-budget";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a caller needs to hand over for the refusal to become a row on `/api/logs`.
|
|
24
|
+
*
|
|
25
|
+
* The HTTP admission check refuses before the body is parsed, so its `logCtx` still carries the
|
|
26
|
+
* `unknown` model and provider the caller seeded it with. That is the honest record -- this
|
|
27
|
+
* request genuinely never resolved either -- and it is the same placeholder the native
|
|
28
|
+
* passthrough path already writes. Skipping the row entirely was the worse option: an operator
|
|
29
|
+
* reading the logs saw no trace at all of a request the proxy had refused.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkflowRefusalLog {
|
|
32
|
+
readonly requestId: string;
|
|
33
|
+
readonly start: number;
|
|
34
|
+
readonly logCtx: RequestLogContext;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the 429 for a refusal this proxy made itself.
|
|
39
|
+
*
|
|
40
|
+
* The status and type arguments below do not reach the client: `classifyError` rewrites every
|
|
41
|
+
* 429 to `rate_limit_error` / `rate_limit_exceeded`, so the body is shaped exactly like a
|
|
42
|
+
* provider rate limit. That is a deliberate wire contract -- changing it would change how every
|
|
43
|
+
* client retries -- which leaves two places to carry the truth. The message names the ceiling
|
|
44
|
+
* that fired and says no provider was contacted, and the header carries the machine-readable
|
|
45
|
+
* name. Nothing upstream sets that header, so its presence is conclusive.
|
|
46
|
+
*
|
|
47
|
+
* The row is where an operator actually looks, so it gets the same treatment #4639 established:
|
|
48
|
+
* `terminalSource: "synthetic"`, a local reason, and an error code naming the ceiling. Pass
|
|
49
|
+
* `logCtx` when the caller is inside a turn that will write its own row, or `refusalLog` when
|
|
50
|
+
* the refusal happens before any row exists and this is the only chance to write one.
|
|
51
|
+
*/
|
|
52
|
+
export function workflowRefusalResponse(
|
|
53
|
+
reason: WorkflowDenial,
|
|
54
|
+
logCtx?: RequestLogContext,
|
|
55
|
+
refusalLog?: WorkflowRefusalLog,
|
|
56
|
+
rootId?: string,
|
|
57
|
+
): Response {
|
|
58
|
+
const summary = workflowDenialSummary(reason);
|
|
59
|
+
// Only a caller that decided the refusal ITSELF passes a root id. admitWorkflowTurn already
|
|
60
|
+
// records its own denials, so passing one there would double-count them.
|
|
61
|
+
if (rootId) recordWorkflowRefusalEvent(rootId, reason);
|
|
62
|
+
const recordOn = logCtx ?? refusalLog?.logCtx;
|
|
63
|
+
if (recordOn) {
|
|
64
|
+
markLocalRequestLogRefusal(recordOn, summary.code);
|
|
65
|
+
// A locally assigned code wins in addFinalRequestLog, so this is what names the ceiling in
|
|
66
|
+
// the logs column rather than the generic rate-limit classification a 429 would get.
|
|
67
|
+
recordOn.errorCode = summary.code;
|
|
68
|
+
}
|
|
69
|
+
if (refusalLog) {
|
|
70
|
+
addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, 429, {
|
|
71
|
+
closeReason: "terminal",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const refusal = formatErrorResponse(
|
|
75
|
+
429,
|
|
76
|
+
reason === "workflow-sends-exhausted" ? "workflow_budget_exhausted" : "queue_capacity_exceeded",
|
|
77
|
+
summary.message,
|
|
78
|
+
);
|
|
79
|
+
refusal.headers.set(WORKFLOW_LOCAL_REFUSAL_HEADER, summary.code);
|
|
80
|
+
// Without this a browser dashboard cannot read the header at all: the data plane never sets
|
|
81
|
+
// Access-Control-Expose-Headers, so a cross-origin reader sees only the CORS-safelisted ones.
|
|
82
|
+
refusal.headers.set("Access-Control-Expose-Headers", WORKFLOW_LOCAL_REFUSAL_HEADER);
|
|
83
|
+
return refusal;
|
|
84
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -882,6 +882,36 @@ export interface OcxConfig {
|
|
|
882
882
|
* binding under either setting -- neither is a cache-affinity preference.
|
|
883
883
|
*/
|
|
884
884
|
cacheAffinity?: boolean;
|
|
885
|
+
/**
|
|
886
|
+
* Operator-declared quota domains: groups of credential ids that demonstrably share
|
|
887
|
+
* one upstream usage limit (#4546, wp6). Members of one group count once toward
|
|
888
|
+
* available capacity, and a quota refusal inside a group is never answered by
|
|
889
|
+
* rotating to another member -- the limit is the same, so the move would pay a cold
|
|
890
|
+
* prefix for zero new capacity.
|
|
891
|
+
*
|
|
892
|
+
* Declared groups speak only to quota. Sharing a usage limit says nothing about
|
|
893
|
+
* prompt-cache compatibility, which keeps its own provider-documented domain.
|
|
894
|
+
* Absent or empty means no declared grouping, so an unconfigured install behaves
|
|
895
|
+
* exactly as before.
|
|
896
|
+
*
|
|
897
|
+
* A declaration has to mean exactly one thing, so the config rejects the spellings
|
|
898
|
+
* that could mean two. Credential ids are provider-scoped elsewhere (the auth store
|
|
899
|
+
* keys an account by provider and id), so each member is written
|
|
900
|
+
* `"<provider>:<credential-id>"` -- a bare `"acct-1"` names one credential per
|
|
901
|
+
* provider and would merge unrelated domains. The provider segment is matched
|
|
902
|
+
* case-insensitively through the usual aliases, so `chatgpt:` and `codex:` both mean
|
|
903
|
+
* OpenAI. Group ids must be unique, `credentials` must be non-empty, and a credential
|
|
904
|
+
* may belong to at most one group; a declaration that breaks any of those is rejected
|
|
905
|
+
* on write and dropped with a warning on load, never resolved by list order.
|
|
906
|
+
*/
|
|
907
|
+
credentialGroups?: Array<{
|
|
908
|
+
/** Operator-chosen group identifier; only equality matters. */
|
|
909
|
+
id: string;
|
|
910
|
+
/** Provider-qualified credential ids (`"<provider>:<credential-id>"`), non-empty. */
|
|
911
|
+
credentials: string[];
|
|
912
|
+
/** Free-text provenance note for the operator's own records. */
|
|
913
|
+
note?: string;
|
|
914
|
+
}>;
|
|
885
915
|
};
|
|
886
916
|
/** Active pool account id for next session. undefined = main (passthrough as-is). */
|
|
887
917
|
activeCodexAccountId?: string;
|