@bitkyc08/opencodex 2.54.0-preview.20260914 → 2.55.0-preview.20260914
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-B4VYfZcY.js → index-DH2PUHqr.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +57 -0
- package/src/adapters/anthropic-image-normalize.ts +28 -1
- package/src/adapters/anthropic.ts +68 -6
- package/src/adapters/base.ts +8 -0
- package/src/adapters/coding-agent/protocol.ts +41 -16
- package/src/adapters/cursor/cursor-errors.ts +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/native-exec-fs.ts +10 -10
- package/src/adapters/cursor/native-exec-network.ts +2 -2
- package/src/adapters/cursor/native-exec-shell.ts +13 -12
- package/src/adapters/cursor/native-exec.ts +51 -10
- package/src/adapters/cursor/policy-error.ts +75 -0
- package/src/adapters/cursor/protobuf-request.ts +105 -1
- package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
- package/src/adapters/devin/live-models.ts +33 -2
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +46 -0
- package/src/adapters/input-media-guard.ts +45 -0
- package/src/adapters/kiro/adapter.ts +8 -0
- package/src/adapters/kiro/payload.ts +28 -6
- package/src/adapters/kiro-events.ts +25 -6
- package/src/adapters/kiro-images.ts +30 -0
- package/src/adapters/kiro-retry.ts +8 -0
- package/src/adapters/openai-chat.ts +33 -4
- package/src/adapters/openai-responses.ts +26 -0
- package/src/adapters/registry.ts +4 -0
- package/src/bridge.ts +163 -116
- package/src/chat/image-parts.ts +151 -0
- package/src/chat/inbound.ts +70 -33
- package/src/cli/connect.ts +30 -9
- package/src/cli/dispatch.ts +7 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/runtime-api.ts +25 -0
- package/src/cli/status.ts +21 -19
- package/src/cli/system-restart-client.ts +25 -0
- package/src/clients/config-export.ts +14 -4
- package/src/codex/app-server-processes.ts +25 -0
- package/src/codex/auth-context.ts +8 -0
- package/src/codex/autostart-health.ts +36 -2
- package/src/codex/catalog/provider-fetch.ts +41 -0
- package/src/codex/catalog-auto-refresh.ts +182 -0
- package/src/codex/catalog-refresh-status.ts +93 -0
- package/src/codex/history-provider.ts +55 -0
- package/src/codex/model-entitlements.ts +78 -0
- package/src/codex/native-profile-processes.ts +114 -15
- package/src/codex/prompt-text-probe.ts +274 -41
- package/src/codex/routing-adoption.ts +189 -0
- package/src/codex/routing.ts +520 -48
- package/src/codex/runtime.ts +249 -7
- package/src/combos/failover.ts +45 -0
- package/src/config.ts +124 -4
- package/src/generated/compatibility-version.json +110 -74
- package/src/generated/model-metadata.ts +1 -0
- package/src/lib/request-execution-budget.ts +202 -0
- package/src/lib/upstream-retry.ts +95 -8
- package/src/lib/workflow-budget.ts +172 -0
- package/src/oauth/devin.ts +57 -12
- package/src/providers/quota.ts +37 -6
- package/src/providers/registry.ts +53 -6
- package/src/responses/input-media.ts +65 -0
- package/src/responses/parser-content.ts +42 -0
- package/src/responses/schema.ts +12 -2
- package/src/server/audio-live.ts +1 -2
- package/src/server/audio-transcriptions.ts +1 -2
- package/src/server/auth-cors.ts +1 -1
- package/src/server/background-lifecycle.ts +18 -0
- package/src/server/chat-completions.ts +23 -8
- package/src/server/chat-native.ts +17 -17
- package/src/server/index.ts +24 -0
- package/src/server/management/request-history-routes.ts +5 -0
- package/src/server/request-log.ts +8 -2
- package/src/server/responses/compact.ts +51 -3
- package/src/server/responses/core.ts +238 -28
- package/src/server/search.ts +7 -9
- package/src/types/config.ts +51 -6
- package/src/usage/log.ts +37 -0
- package/src/vision/eligibility.ts +37 -4
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +45 -10
- package/src/web-search/alpha-search.ts +324 -0
- package/src/web-search/index.ts +13 -22
- package/src/web-search/passthrough-bridge.ts +195 -22
- package/src/web-search/sidecar-providers.ts +22 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
2
|
+
import { chatBodyCarriesImage, chatBodyCarriesToolResultImage } from "../chat/image-parts";
|
|
2
3
|
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
3
4
|
import {
|
|
4
5
|
chatCompletionsErrorBody,
|
|
@@ -19,7 +20,7 @@ import type { AdmissionLease } from "../lib/admission";
|
|
|
19
20
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
20
21
|
import { redactSecretString } from "../lib/redact";
|
|
21
22
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
22
|
-
import { isModelTextOnly } from "../vision";
|
|
23
|
+
import { isModelTextOnly, requiresVisionPreprocessing } from "../vision";
|
|
23
24
|
import {
|
|
24
25
|
applyUpstreamRecoveryInit,
|
|
25
26
|
fetchWithResetRetry,
|
|
@@ -138,7 +139,7 @@ function isRec(value: unknown): value is Rec {
|
|
|
138
139
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec): boolean {
|
|
142
|
+
export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec, config?: OcxConfig): boolean {
|
|
142
143
|
const provider = route.provider;
|
|
143
144
|
if (provider.adapter !== "openai-chat") return false;
|
|
144
145
|
if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return false;
|
|
@@ -147,12 +148,24 @@ export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec): boo
|
|
|
147
148
|
if (rawBody.store === true || rawBody.background === true) return false;
|
|
148
149
|
if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return false;
|
|
149
150
|
if (rawBody.compaction_trigger !== undefined) return false;
|
|
151
|
+
// A standard Chat tool message accepts a string or text parts, not image_url, so
|
|
152
|
+
// normalizing a Pi/Anthropic tool image into image_url is not enough on its own —
|
|
153
|
+
// the part is still inside a tool message. The translated adapter already places
|
|
154
|
+
// tool-result images in a following user carrier after the complete paired batch
|
|
155
|
+
// (flushToolResultImages), so divert these requests there. Ordinary user images and
|
|
156
|
+
// text-only tool results keep the native fast path.
|
|
157
|
+
if (chatBodyCarriesToolResultImage(rawBody)) return false;
|
|
150
158
|
// Vision sidecar coverage (roadmap 180): a text-only routed model with an
|
|
151
159
|
// image-bearing body must go through the Responses pipeline, whose plan
|
|
152
160
|
// site describes or strips the image. The native fast path has no vision
|
|
153
161
|
// handling, so letting it keep such a request forwards raw pixels to a
|
|
154
162
|
// model the operator declared blind.
|
|
155
|
-
if (
|
|
163
|
+
if (chatBodyCarriesImage(rawBody)) {
|
|
164
|
+
const needsVision = config
|
|
165
|
+
? requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)
|
|
166
|
+
: isModelTextOnly(provider, route.modelId);
|
|
167
|
+
if (needsVision) return false;
|
|
168
|
+
}
|
|
156
169
|
if (Array.isArray(rawBody.tools)) {
|
|
157
170
|
for (const tool of rawBody.tools) {
|
|
158
171
|
if (!isRec(tool)) continue;
|
|
@@ -164,19 +177,6 @@ export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec): boo
|
|
|
164
177
|
return true;
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
/** Any messages[].content[] part of type image_url. */
|
|
168
|
-
function chatBodyCarriesImage(rawBody: Rec): boolean {
|
|
169
|
-
const messages = rawBody.messages;
|
|
170
|
-
if (!Array.isArray(messages)) return false;
|
|
171
|
-
for (const message of messages) {
|
|
172
|
-
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
173
|
-
for (const part of message.content) {
|
|
174
|
-
if (isRec(part) && part.type === "image_url") return true;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
180
|
function chatCompletionJson(value: unknown): Rec | null {
|
|
181
181
|
if (!isRec(value) || !Array.isArray(value.choices) || value.choices.length === 0) return null;
|
|
182
182
|
return value;
|
|
@@ -326,7 +326,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
326
326
|
dispatchOverride: async (_input, init, execute) => {
|
|
327
327
|
if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
|
|
328
328
|
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
329
|
-
if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, options.chatBody)) {
|
|
329
|
+
if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, options.chatBody, config)) {
|
|
330
330
|
throw new Error("Provider key selection is no longer available for native Chat");
|
|
331
331
|
}
|
|
332
332
|
activeProvider = current;
|
package/src/server/index.ts
CHANGED
|
@@ -134,6 +134,7 @@ import {
|
|
|
134
134
|
type RequestLogEntry,
|
|
135
135
|
} from "./request-log";
|
|
136
136
|
import { sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
137
|
+
import { admitWorkflowTurn, type WorkflowLane } from "../lib/workflow-budget";
|
|
137
138
|
export {
|
|
138
139
|
addFinalRequestLog,
|
|
139
140
|
filterRequestLogs,
|
|
@@ -1292,13 +1293,36 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1292
1293
|
): Promise<Response> {
|
|
1293
1294
|
const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
1294
1295
|
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
1296
|
+
// A fan-out shares the conversation it serves. Without a reserve, a worker burst takes every
|
|
1297
|
+
// slot under its own root and the interactive turn that started it waits behind its own
|
|
1298
|
+
// children. A request that names a parent is treated as that fan-out; a top-level request is
|
|
1299
|
+
// the conversation and may use the reserved slots.
|
|
1300
|
+
const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
1301
|
+
const workflowThreadId = req.headers.get("thread-id")?.trim() || undefined;
|
|
1302
|
+
const workflowLane: WorkflowLane = workflowRootId !== undefined
|
|
1303
|
+
&& workflowThreadId !== undefined
|
|
1304
|
+
&& workflowThreadId !== workflowRootId
|
|
1305
|
+
? "worker"
|
|
1306
|
+
: "interactive";
|
|
1307
|
+
const workflow = admitWorkflowTurn(workflowRootId, workflowLane, undefined, workflowThreadId);
|
|
1308
|
+
if (workflow && !workflow.admitted) {
|
|
1309
|
+
lease.release();
|
|
1310
|
+
return formatErrorResponse(
|
|
1311
|
+
429,
|
|
1312
|
+
workflow.reason === "workflow-sends-exhausted" ? "workflow_budget_exhausted" : "queue_capacity_exceeded",
|
|
1313
|
+
"This task has reached its concurrent-work limit, so no further upstream request was made. Work already in flight settles as it finishes.",
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
const releaseWorkflow = (): void => { if (workflow?.admitted) workflow.lease.release(); };
|
|
1295
1317
|
let response: Response;
|
|
1296
1318
|
try {
|
|
1297
1319
|
response = await work(lease);
|
|
1298
1320
|
} catch (error) {
|
|
1321
|
+
releaseWorkflow();
|
|
1299
1322
|
lease.release();
|
|
1300
1323
|
throw error;
|
|
1301
1324
|
}
|
|
1325
|
+
releaseWorkflow();
|
|
1302
1326
|
if (!lease.isTransferred()) {
|
|
1303
1327
|
lease.release();
|
|
1304
1328
|
}
|
|
@@ -151,6 +151,11 @@ export async function handleRequestHistoryRoutes(ctx: ManagementContext): Promis
|
|
|
151
151
|
return jsonResponse({
|
|
152
152
|
requestId,
|
|
153
153
|
routeDecision: trace,
|
|
154
|
+
// The account decision belongs in the why-this-route answer: a rebound with its cause is
|
|
155
|
+
// the difference between "the pool moved this conversation" and "this is a new session".
|
|
156
|
+
affinity: entry.affinity
|
|
157
|
+
? { move: entry.affinity, reason: entry.affinityReason ?? null }
|
|
158
|
+
: null,
|
|
154
159
|
attemptSequence: entry.attempts ?? [],
|
|
155
160
|
outcome: {
|
|
156
161
|
status: entry.status,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
upstreamErrorMessageFromPayload,
|
|
13
13
|
} from "../lib/errors";
|
|
14
14
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
15
|
+
import type { CodexAffinityMove, CodexAffinityReason } from "../codex/routing";
|
|
15
16
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
16
17
|
import type { AttemptTierOutcome, OcxProviderConfig, OcxUsage } from "../types";
|
|
17
18
|
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
@@ -139,7 +140,9 @@ export interface RequestLogContext {
|
|
|
139
140
|
errorCode?: string;
|
|
140
141
|
/** Structured reason from `response.incomplete`; internal-only input to log classification. */
|
|
141
142
|
terminalIncompleteReason?: string;
|
|
142
|
-
affinity?:
|
|
143
|
+
affinity?: CodexAffinityMove;
|
|
144
|
+
/** Why the binding was kept, moved, or released (#4546). */
|
|
145
|
+
affinityReason?: CodexAffinityReason;
|
|
143
146
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
144
147
|
terminalSource?: "upstream" | "synthetic";
|
|
145
148
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
@@ -204,7 +207,9 @@ export interface RequestLogEntry {
|
|
|
204
207
|
totalTokens?: number;
|
|
205
208
|
attempts?: PersistedUsageAttempt[];
|
|
206
209
|
/** Codex pool affinity decision for this request (diagnostics for #186). */
|
|
207
|
-
affinity?:
|
|
210
|
+
affinity?: CodexAffinityMove;
|
|
211
|
+
/** Why that decision was made (#4546): a move is the expensive event, so it names its cause. */
|
|
212
|
+
affinityReason?: CodexAffinityReason;
|
|
208
213
|
/** Where the upstream terminal/failure was observed. */
|
|
209
214
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
210
215
|
/** Whether the terminal came from a real upstream SSE event or a proxy synthetic tail. */
|
|
@@ -1080,6 +1085,7 @@ export function addFinalRequestLog(
|
|
|
1080
1085
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
1081
1086
|
...(attempts !== undefined ? { attempts } : {}),
|
|
1082
1087
|
...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
|
|
1088
|
+
...(logCtx.affinityReason ? { affinityReason: logCtx.affinityReason } : {}),
|
|
1083
1089
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
1084
1090
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1085
1091
|
...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
|
|
@@ -76,8 +76,14 @@ import {
|
|
|
76
76
|
fetchWithResetRetry,
|
|
77
77
|
fetchWithTransientRetry,
|
|
78
78
|
applyUpstreamRecoveryInit,
|
|
79
|
+
SendBudgetExhaustedError,
|
|
80
|
+
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
79
81
|
type UpstreamSendRecovery,
|
|
80
82
|
} from "../../lib/upstream-retry";
|
|
83
|
+
import {
|
|
84
|
+
createRequestExecutionBudget,
|
|
85
|
+
type RequestExecutionBudget,
|
|
86
|
+
} from "../../lib/request-execution-budget";
|
|
81
87
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
82
88
|
import {
|
|
83
89
|
acquireUpstreamHostAdmission,
|
|
@@ -224,6 +230,14 @@ export interface HandleResponsesCompactOptions {
|
|
|
224
230
|
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
225
231
|
/** Release the listener's idle guard only after the complete request body is accepted. */
|
|
226
232
|
onRequestBodyRead?: () => void;
|
|
233
|
+
/**
|
|
234
|
+
* The logical request's send budget (#4546). Compact used to hold its own: the normal send
|
|
235
|
+
* took a fresh transient allowance of three, the 401 replay and the 429 alternate each added
|
|
236
|
+
* one -- and the guard that was supposed to make those two mutually exclusive keys on
|
|
237
|
+
* `kind === "pool"`, so a main-pool credential could spend all five. The recursive handoff
|
|
238
|
+
* child then started over, so one compact could reach ten.
|
|
239
|
+
*/
|
|
240
|
+
sendBudget?: RequestExecutionBudget;
|
|
227
241
|
}
|
|
228
242
|
|
|
229
243
|
export function compactResponseTooLargeError(): Response {
|
|
@@ -760,6 +774,10 @@ export async function handleResponsesCompact(
|
|
|
760
774
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
761
775
|
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
762
776
|
const compactUrl = `${base}/responses/compact`;
|
|
777
|
+
// One holder for this logical compact, inherited by the handoff child so a second model
|
|
778
|
+
// does not start over with a fresh four.
|
|
779
|
+
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
780
|
+
const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;
|
|
763
781
|
const actualCompactHostKey = upstreamHostHealthKey(
|
|
764
782
|
route.providerName,
|
|
765
783
|
safeOriginLabel(compactUrl),
|
|
@@ -834,6 +852,27 @@ export async function handleResponsesCompact(
|
|
|
834
852
|
// wrapping reset retry — because those retries happen before any alternate is even
|
|
835
853
|
// considered. The alternate is one bounded send: a second ladder would multiply the
|
|
836
854
|
// work an already-rejecting pool is doing.
|
|
855
|
+
//
|
|
856
|
+
// Both modes now draw one shared budget. The comment below used to say the 401 replay
|
|
857
|
+
// spends the account budget so the 429 alternate is skipped, but that guard is keyed on
|
|
858
|
+
// `kind === "pool"` and a main-pool credential left it false -- so 401 then 429 really did
|
|
859
|
+
// reach five. The single sends spend the base allowance first and then the one shared
|
|
860
|
+
// final-recovery reserve, which is the same rule the Responses path follows.
|
|
861
|
+
const sendSingleCompactAttempt = (
|
|
862
|
+
doFetch: () => Promise<Response>,
|
|
863
|
+
): Promise<Response> => {
|
|
864
|
+
if (sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS) > 0) {
|
|
865
|
+
sendBudget.used += 1;
|
|
866
|
+
return doFetch();
|
|
867
|
+
}
|
|
868
|
+
const decision = sendBudget.reserveDispatch({
|
|
869
|
+
sendClass: "auth-recovery",
|
|
870
|
+
targetKey: compactTargetKey,
|
|
871
|
+
});
|
|
872
|
+
if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
|
|
873
|
+
if (!decision.permit.use()) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
|
|
874
|
+
return doFetch();
|
|
875
|
+
};
|
|
837
876
|
const sendCompactAttempt = (
|
|
838
877
|
sendProvider: OcxProviderConfig,
|
|
839
878
|
sendHeaders: Headers,
|
|
@@ -866,8 +905,15 @@ export async function handleResponsesCompact(
|
|
|
866
905
|
return res;
|
|
867
906
|
});
|
|
868
907
|
return recovery === "single"
|
|
869
|
-
? doFetch
|
|
870
|
-
: fetchWithTransientRetry(doFetch, {
|
|
908
|
+
? sendSingleCompactAttempt(doFetch)
|
|
909
|
+
: fetchWithTransientRetry(doFetch, {
|
|
910
|
+
abortSignal: req.signal,
|
|
911
|
+
label: safeHostLabel(compactUrl),
|
|
912
|
+
// Draws the shared remainder instead of a fresh three. Compact is a native endpoint
|
|
913
|
+
// of the same logical turn, so its sends belong to the same cap.
|
|
914
|
+
attempts: sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS),
|
|
915
|
+
onSendsConsumed: (used: number) => { sendBudget.used += Math.max(0, used); },
|
|
916
|
+
});
|
|
871
917
|
};
|
|
872
918
|
|
|
873
919
|
// The account each outcome belongs to. Reassigned only when the alternate send below
|
|
@@ -1133,7 +1179,9 @@ export async function handleResponsesCompact(
|
|
|
1133
1179
|
logCtx,
|
|
1134
1180
|
turnAdmissionLease,
|
|
1135
1181
|
admission,
|
|
1136
|
-
|
|
1182
|
+
// The handoff child is the same logical compact on a second model, so it inherits
|
|
1183
|
+
// the holder. Forwarding `options` alone was not enough: the child minted its own.
|
|
1184
|
+
{ ...options, sendBudget },
|
|
1137
1185
|
);
|
|
1138
1186
|
if (fallback.ok || fallback.status === 499) return fallback;
|
|
1139
1187
|
await fallback.body?.cancel().catch(() => undefined);
|