@bitkyc08/opencodex 2.7.33 → 2.7.34
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +21 -10
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-BkmJJgg6.js +52 -0
- package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +13 -6
- package/src/adapters/cursor/discovery.ts +39 -4
- package/src/adapters/cursor/exec-policy.ts +11 -13
- package/src/adapters/cursor/live-transport.ts +22 -4
- package/src/adapters/cursor/protobuf-events.ts +140 -8
- package/src/adapters/cursor/protobuf-request.ts +15 -0
- package/src/adapters/cursor/request-builder.ts +10 -5
- package/src/adapters/cursor/transport.ts +3 -2
- package/src/adapters/cursor/types.ts +14 -0
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro-errors.ts +111 -2
- package/src/adapters/kiro-events.ts +154 -35
- package/src/adapters/kiro-retry.ts +116 -32
- package/src/adapters/kiro-tools.ts +30 -20
- package/src/adapters/kiro-wire.ts +47 -6
- package/src/adapters/kiro.ts +891 -228
- package/src/adapters/openai-chat.ts +12 -5
- package/src/adapters/openai-responses.ts +7 -2
- package/src/bridge.ts +109 -26
- package/src/claude/outbound.ts +27 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog.ts +375 -33
- package/src/combos/index.ts +3 -0
- package/src/combos/request.ts +4 -4
- package/src/combos/resolve.ts +2 -2
- package/src/combos/types.ts +104 -2
- package/src/config.ts +70 -1
- package/src/lib/eventstream-decoder.ts +9 -0
- package/src/oauth/index.ts +3 -1
- package/src/oauth/kiro-credentials.ts +48 -20
- package/src/oauth/login-cli.ts +2 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +2 -2
- package/src/providers/openai-sidecar.ts +28 -1
- package/src/providers/registry.ts +39 -2
- package/src/responses/parser.ts +22 -10
- package/src/responses/schema.ts +1 -0
- package/src/responses/state.ts +50 -10
- package/src/router.ts +15 -3
- package/src/server/auth-cors.ts +7 -0
- package/src/server/claude-messages.ts +6 -0
- package/src/server/index.ts +6 -3
- package/src/server/management-api.ts +187 -43
- package/src/server/ports.ts +4 -2
- package/src/server/request-log.ts +3 -2
- package/src/server/responses-item-id-repair.ts +281 -0
- package/src/server/responses.ts +274 -73
- package/src/types.ts +109 -16
- package/src/update/job.ts +81 -1
- package/src/vision/describe.ts +2 -1
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/loop.ts +9 -1
- package/src/web-search/progress-stream.ts +12 -10
- package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
- package/gui/dist/assets/index-d63HMU0x.js +0 -52
package/src/server/responses.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { Server } from "bun";
|
|
|
2
2
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
|
|
3
3
|
import {
|
|
4
4
|
getConfigPath,
|
|
5
|
+
multiAgentGuidanceEnabled,
|
|
5
6
|
resolveEnvValue,
|
|
6
7
|
} from "../config";
|
|
7
8
|
import { parseRequest } from "../responses/parser";
|
|
8
9
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../responses/compaction";
|
|
9
10
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../adapters/openai-responses";
|
|
10
|
-
import { expandPreviousResponseInput,
|
|
11
|
+
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../responses/state";
|
|
11
12
|
import { routeModel } from "../router";
|
|
12
13
|
import {
|
|
13
14
|
advanceComboAfterFailure,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
import { isInjectionDebugEnabled } from "../lib/debug-settings";
|
|
27
28
|
import { injectionDebugLog } from "../lib/injection-debug-log";
|
|
28
29
|
import { modelInList, namespacedToolName } from "../types";
|
|
29
|
-
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
|
|
30
|
+
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../types";
|
|
30
31
|
import {
|
|
31
32
|
forceRefreshOAuthAccessSnapshot,
|
|
32
33
|
getOAuthCredentialApiBaseUrl,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../lib/upstream-retry";
|
|
59
60
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
60
61
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
62
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
63
|
+
import { slugsEquivalent } from "../providers/slug-codec";
|
|
61
64
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../providers/openai-virtual-models";
|
|
62
65
|
import { isUsageDebugEnabled } from "../usage/debug";
|
|
63
66
|
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
|
|
@@ -91,6 +94,7 @@ import {
|
|
|
91
94
|
relayWithAbort,
|
|
92
95
|
sanitizePassthroughHeaders,
|
|
93
96
|
} from "./relay";
|
|
97
|
+
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "./responses-item-id-repair";
|
|
94
98
|
|
|
95
99
|
export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
|
|
96
100
|
toolNsMap: Map<string, { namespace: string; name: string }>;
|
|
@@ -182,13 +186,9 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
|
182
186
|
* model/effort overrides on a full-history fork (multi_agents_v2/spawn.rs
|
|
183
187
|
* reject_full_fork_spawn_overrides), so the prompt mandates fork_turns "none" or a
|
|
184
188
|
* partial fork plus a self-contained task message.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* rejects any request whose declared schema deviates, "Invalid Value: 'tools'").
|
|
189
|
-
* That is prompt-workable: SpawnAgentArgs always parses model/reasoning_effort
|
|
190
|
-
* regardless of the flag (spawn.rs), so the prompt tells the model to pass the
|
|
191
|
-
* arguments even though the schema does not list them.
|
|
189
|
+
* Current Codex surfaces can expose model/reasoning_effort overrides directly or
|
|
190
|
+
* omit them. The proxy wording therefore stays schema-agnostic and advertises only
|
|
191
|
+
* the effective candidates described for this collaboration surface.
|
|
192
192
|
*
|
|
193
193
|
* The v2 body is budgeted to <= 700 chars (V2_GUIDANCE_CHAR_BUDGET): rules first,
|
|
194
194
|
* then the preferred model, then the compact roster of configured `subagentModels`
|
|
@@ -197,7 +197,43 @@ export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
|
197
197
|
* v2 body with {{model}}/{{effort}}/{{roster}} placeholder substitution (own length,
|
|
198
198
|
* user-owned); firing gates are unchanged.
|
|
199
199
|
*/
|
|
200
|
-
|
|
200
|
+
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../codex/catalog";
|
|
201
|
+
|
|
202
|
+
export interface MultiAgentGuidanceOptions {
|
|
203
|
+
multiAgentGuidanceEnabled?: boolean;
|
|
204
|
+
injectionModel?: string;
|
|
205
|
+
injectionEffort?: string;
|
|
206
|
+
subagentModels?: string[];
|
|
207
|
+
injectionPrompt?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface MultiAgentGuidanceDeps {
|
|
211
|
+
resolveEffectiveSubagentRoster?: (
|
|
212
|
+
configuredModels: readonly string[],
|
|
213
|
+
surface: SpawnAgentSurface,
|
|
214
|
+
) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function resolveEffectiveSubagentRoster(
|
|
218
|
+
configuredModels: readonly string[],
|
|
219
|
+
surface: SpawnAgentSurface,
|
|
220
|
+
): Promise<EffectiveSubagentRoster> {
|
|
221
|
+
const { effectiveSubagentRoster } = await import("../codex/catalog");
|
|
222
|
+
return effectiveSubagentRoster(configuredModels, surface);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function multiAgentGuidanceText(
|
|
226
|
+
parsed: OcxParsedRequest,
|
|
227
|
+
options: MultiAgentGuidanceOptions = {},
|
|
228
|
+
deps: MultiAgentGuidanceDeps = {},
|
|
229
|
+
): Promise<string | null> {
|
|
230
|
+
if (options.multiAgentGuidanceEnabled === false) return null;
|
|
231
|
+
const {
|
|
232
|
+
injectionModel,
|
|
233
|
+
injectionEffort,
|
|
234
|
+
subagentModels,
|
|
235
|
+
injectionPrompt,
|
|
236
|
+
} = options;
|
|
201
237
|
const surface = collabSurface(parsed);
|
|
202
238
|
if (surface === null) return null;
|
|
203
239
|
|
|
@@ -205,17 +241,37 @@ export async function multiAgentGuidanceText(parsed: OcxParsedRequest, injection
|
|
|
205
241
|
// codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
|
|
206
242
|
// guidance, and only when there is something concrete to designate: a configured
|
|
207
243
|
// injectionModel and/or a roster entry that resolves in the injected catalog.
|
|
208
|
-
const
|
|
244
|
+
const configuredForGuidance = [
|
|
245
|
+
...(subagentModels ?? []),
|
|
246
|
+
...(injectionModel ? [injectionModel] : []),
|
|
247
|
+
];
|
|
248
|
+
const resolveRoster = deps.resolveEffectiveSubagentRoster ?? resolveEffectiveSubagentRoster;
|
|
249
|
+
const effective = await resolveRoster(configuredForGuidance, "v2");
|
|
250
|
+
const rosterModels = effective.advertised.filter(candidate =>
|
|
251
|
+
(subagentModels ?? []).some(model => slugsEquivalent(model, candidate.model))
|
|
252
|
+
);
|
|
253
|
+
const roster = subagentRosterText(rosterModels);
|
|
254
|
+
const preferred = injectionModel
|
|
255
|
+
? effective.candidates.find(candidate => slugsEquivalent(injectionModel, candidate.model))
|
|
256
|
+
: undefined;
|
|
257
|
+
|
|
258
|
+
if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
|
|
259
|
+
injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
|
|
260
|
+
.map(item => `${item.configured}:${item.reason}`)
|
|
261
|
+
.join(", ")}`);
|
|
262
|
+
}
|
|
209
263
|
if (!injectionModel && roster === "") return null;
|
|
210
264
|
if (injectionPrompt) {
|
|
211
265
|
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster)}</multi_agent_mode>`;
|
|
212
266
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
+ "
|
|
216
|
-
+ "
|
|
217
|
-
|
|
218
|
-
|
|
267
|
+
if (!preferred && roster === "") return null;
|
|
268
|
+
let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
|
|
269
|
+
+ "use only models listed for this collaboration surface. "
|
|
270
|
+
+ "When setting either override, set fork_turns to \"none\" "
|
|
271
|
+
+ "(or a positive turn count such as \"3\"; full-history forks reject overrides) "
|
|
272
|
+
+ "and make the task message self-contained.";
|
|
273
|
+
if (preferred) {
|
|
274
|
+
text += ` Preferred sub-agent: model "${preferred.model}"`
|
|
219
275
|
+ (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
|
|
220
276
|
+ " — use it unless the user names another.";
|
|
221
277
|
}
|
|
@@ -246,25 +302,21 @@ function applyInjectionPlaceholders(prompt: string, model?: string, effort?: str
|
|
|
246
302
|
}
|
|
247
303
|
|
|
248
304
|
/**
|
|
249
|
-
* Compact one-line roster of
|
|
250
|
-
*
|
|
251
|
-
*
|
|
305
|
+
* Compact one-line roster of effective sub-agent candidates, or "" when empty.
|
|
306
|
+
* Efforts come from the injected catalog so only rungs codex-rs will actually
|
|
307
|
+
* accept are advertised.
|
|
252
308
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const ladders = new Set(resolved.map(id => efforts.get(id)!.join("/")));
|
|
261
|
-
if (ladders.size === 1) {
|
|
262
|
-
// Shared ladder (the common case: the injected catalog advertises one rung set)
|
|
263
|
-
// -> state it once instead of per model, keeping the roster inside the budget.
|
|
264
|
-
const ids = resolved.map(id => `"${id}"`).join(", ");
|
|
265
|
-
return ` Available models (reasoning_effort ${[...ladders][0]}): ${ids}.`;
|
|
309
|
+
function subagentRosterText(models: Array<{ model: string; efforts: string[] }>): string {
|
|
310
|
+
if (models.length === 0) return "";
|
|
311
|
+
const ladders = new Set(models.map(model => model.efforts.join("/")));
|
|
312
|
+
if (!ladders.has("") && ladders.size === 1) {
|
|
313
|
+
return ` Available models (reasoning_effort ${[...ladders][0]}): ${models
|
|
314
|
+
.map(model => `"${model.model}"`)
|
|
315
|
+
.join(", ")}.`;
|
|
266
316
|
}
|
|
267
|
-
const entries =
|
|
317
|
+
const entries = models.map(model => model.efforts.length > 0
|
|
318
|
+
? `"${model.model}" (${model.efforts.join("/")})`
|
|
319
|
+
: `"${model.model}"`);
|
|
268
320
|
return ` Available models (valid reasoning_effort): ${entries.join(", ")}.`;
|
|
269
321
|
}
|
|
270
322
|
|
|
@@ -311,6 +363,57 @@ function looksLikeBackendCiphertext(payload: string): boolean {
|
|
|
311
363
|
*/
|
|
312
364
|
const FERNET_TOKEN_RUN = /gAAAA[A-Za-z0-9_-]{60,}={0,2}/g;
|
|
313
365
|
|
|
366
|
+
const AGENT_MESSAGE_ROUTING_ENVELOPE = /(?:^|\n)Message Type\s*:\s*NEW_TASK[^\n]*\nTask name\s*:[^\n]*\nSender\s*:[^\n]*\nPayload\s*:\s*(?:\n|$)/gi;
|
|
367
|
+
const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-(?:LEAF-GUARD|SKILL-AFFORDANCE)\][\s\S]*?(?=\n{2,}|$)/g;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* True when a V2 agent message contains a backend-minted Fernet task but no
|
|
371
|
+
* provider-readable task text. The routing envelope and hook-added control
|
|
372
|
+
* preambles are metadata, not actionable work. Inspect this before spawn-message
|
|
373
|
+
* sanitization splits mixed encrypted slots into plaintext and ciphertext parts.
|
|
374
|
+
*/
|
|
375
|
+
export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
376
|
+
if (!Array.isArray(input)) return false;
|
|
377
|
+
|
|
378
|
+
return input.some(item => {
|
|
379
|
+
if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const content = (item as { content?: unknown }).content;
|
|
384
|
+
if (!Array.isArray(content)) return false;
|
|
385
|
+
|
|
386
|
+
let hasFernetTask = false;
|
|
387
|
+
const readableParts: string[] = [];
|
|
388
|
+
for (const part of content) {
|
|
389
|
+
if (!part || typeof part !== "object") continue;
|
|
390
|
+
const record = part as { type?: unknown; text?: unknown; encrypted_content?: unknown };
|
|
391
|
+
if (
|
|
392
|
+
(record.type === "input_text" || record.type === "text" || record.type === "output_text")
|
|
393
|
+
&& typeof record.text === "string"
|
|
394
|
+
) {
|
|
395
|
+
readableParts.push(record.text);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (record.type !== "encrypted_content" || typeof record.encrypted_content !== "string") {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const withoutFernet = record.encrypted_content.replace(FERNET_TOKEN_RUN, "\n\n");
|
|
403
|
+
if (withoutFernet !== record.encrypted_content) hasFernetTask = true;
|
|
404
|
+
readableParts.push(withoutFernet);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!hasFernetTask) return false;
|
|
408
|
+
const readableTask = readableParts
|
|
409
|
+
.join("\n\n")
|
|
410
|
+
.replace(AGENT_MESSAGE_ROUTING_ENVELOPE, "\n")
|
|
411
|
+
.replace(AGENT_MESSAGE_CONTROL_PREAMBLE, "\n")
|
|
412
|
+
.trim();
|
|
413
|
+
return readableTask.length === 0;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
314
417
|
/**
|
|
315
418
|
* Split a non-ciphertext encrypted slot into ordered parts: prose becomes input_text,
|
|
316
419
|
* embedded Fernet blobs stay encrypted_content so the backend can still decrypt the
|
|
@@ -607,10 +710,14 @@ async function handleComboResponses(
|
|
|
607
710
|
logCtx: RequestLogContext,
|
|
608
711
|
options: HandleResponsesOptions,
|
|
609
712
|
): Promise<Response> {
|
|
713
|
+
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
|
|
714
|
+
? (rawBody as { model: string }).model
|
|
715
|
+
: `combo/${comboId}`;
|
|
610
716
|
Object.assign(logCtx, {
|
|
611
|
-
requestedModel
|
|
612
|
-
model:
|
|
717
|
+
requestedModel,
|
|
718
|
+
model: requestedModel,
|
|
613
719
|
provider: "combo",
|
|
720
|
+
comboId,
|
|
614
721
|
});
|
|
615
722
|
const combo = getCombo(config, comboId);
|
|
616
723
|
if (!combo) {
|
|
@@ -713,9 +820,10 @@ async function handleComboResponses(
|
|
|
713
820
|
attemptRetained = true;
|
|
714
821
|
noteComboSuccess(comboId, combo, pick.target);
|
|
715
822
|
Object.assign(logCtx, childLog, {
|
|
716
|
-
requestedModel
|
|
717
|
-
model:
|
|
823
|
+
requestedModel,
|
|
824
|
+
model: requestedModel,
|
|
718
825
|
provider: "combo",
|
|
826
|
+
comboId,
|
|
719
827
|
attempts: logCtx.attempts,
|
|
720
828
|
activeAttempt: attempt,
|
|
721
829
|
activeAttemptStartedAt: started,
|
|
@@ -763,9 +871,10 @@ async function handleComboResponses(
|
|
|
763
871
|
lastFailure = failure.response;
|
|
764
872
|
if (comboFailureDecision(response.status, failure.classificationText) === "stop") {
|
|
765
873
|
Object.assign(logCtx, childLog, {
|
|
766
|
-
requestedModel
|
|
767
|
-
model:
|
|
874
|
+
requestedModel,
|
|
875
|
+
model: requestedModel,
|
|
768
876
|
provider: "combo",
|
|
877
|
+
comboId,
|
|
769
878
|
attempts: logCtx.attempts,
|
|
770
879
|
activeAttempt: undefined,
|
|
771
880
|
activeAttemptStartedAt: undefined,
|
|
@@ -795,13 +904,16 @@ export async function handleResponses(
|
|
|
795
904
|
} catch (err) {
|
|
796
905
|
return decodeRequestErrorResponse(err, "responses");
|
|
797
906
|
}
|
|
798
|
-
const comboId = !options.comboAttempt ? comboIdFromRawBody(body) : null;
|
|
907
|
+
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
|
|
799
908
|
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
800
909
|
return handleComboResponses(req, body, comboId, config, logCtx, options);
|
|
801
910
|
}
|
|
802
911
|
const originalBody = body;
|
|
803
912
|
body = expandPreviousResponseInput(body);
|
|
804
913
|
const previousResponseInputExpanded = body !== originalBody;
|
|
914
|
+
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
915
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
916
|
+
);
|
|
805
917
|
|
|
806
918
|
// Spawn-message compatibility (both directions): agent_message task payloads ride in
|
|
807
919
|
// encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
|
|
@@ -822,7 +934,8 @@ export async function handleResponses(
|
|
|
822
934
|
try {
|
|
823
935
|
parsed = parseRequest(body);
|
|
824
936
|
if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
|
|
825
|
-
parsed.
|
|
937
|
+
parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
|
|
938
|
+
parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
|
|
826
939
|
} catch (err) {
|
|
827
940
|
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
828
941
|
}
|
|
@@ -859,6 +972,17 @@ export async function handleResponses(
|
|
|
859
972
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
860
973
|
}
|
|
861
974
|
|
|
975
|
+
// The canonical ChatGPT backend can decrypt its V2 Fernet task tokens; routed
|
|
976
|
+
// providers cannot. Reject the raw-input classification before adapter construction
|
|
977
|
+
// or provider dispatch so an unreadable worker task cannot trigger a cost storm.
|
|
978
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
|
|
979
|
+
return formatErrorResponse(
|
|
980
|
+
400,
|
|
981
|
+
"invalid_request_error",
|
|
982
|
+
"Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.",
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
862
986
|
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace
|
|
863
987
|
// (e.g. "opencode-go/deepseek-v4-pro" → "deepseek-v4-pro"). Adapters read parsed.modelId,
|
|
864
988
|
// and the passthrough adapter serializes _rawBody, so rewrite both.
|
|
@@ -888,17 +1012,23 @@ export async function handleResponses(
|
|
|
888
1012
|
}
|
|
889
1013
|
|
|
890
1014
|
// Multi-agent guidance shim: codex-rs emits its Proactive delegation developer
|
|
891
|
-
// message only on the v2 surface. The proxy fills
|
|
892
|
-
// for v1 collab surfaces at the top tier
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
1015
|
+
// message only on the v2 surface. The proxy fills the gaps: the Proactive text
|
|
1016
|
+
// for v1 collab surfaces at the top tier (no model designation on v1), and the
|
|
1017
|
+
// sub-agent model/roster designation plus fork_turns override rules on v2.
|
|
1018
|
+
// The surface is judged from the request's own tool list. Runs BEFORE the
|
|
1019
|
+
// mock-max clamp below so the synthetic top tier (ultra arrives as max on the
|
|
1020
|
+
// codex wire) is still visible. Both request shapes are rewritten.
|
|
897
1021
|
{
|
|
898
|
-
const guidance = await multiAgentGuidanceText(parsed,
|
|
1022
|
+
const guidance = await multiAgentGuidanceText(parsed, {
|
|
1023
|
+
multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
|
|
1024
|
+
injectionModel: config.injectionModel,
|
|
1025
|
+
injectionEffort: config.injectionEffort,
|
|
1026
|
+
subagentModels: config.subagentModels,
|
|
1027
|
+
injectionPrompt: config.injectionPrompt,
|
|
1028
|
+
});
|
|
899
1029
|
if (guidance) {
|
|
900
1030
|
injectDeveloperMessage(parsed, guidance);
|
|
901
|
-
if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, ${guidance.length} chars)`);
|
|
1031
|
+
if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
|
|
902
1032
|
} else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
|
|
903
1033
|
injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
|
|
904
1034
|
}
|
|
@@ -935,16 +1065,17 @@ export async function handleResponses(
|
|
|
935
1065
|
// receive `max` when the user picks Ultra (codex converts ultra->max client-side).
|
|
936
1066
|
// Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT
|
|
937
1067
|
// passthrough serializes _rawBody verbatim, so both shapes must be rewritten.
|
|
938
|
-
// GUARD: judge nativeness by the
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
1068
|
+
// GUARD: judge nativeness by BOTH the originally requested id (logCtx.requestedModel)
|
|
1069
|
+
// and the resolved provider identity. Routing strips the "<provider>/" namespace, and
|
|
1070
|
+
// some third-party providers expose bare `defaultModel` selectors, so route.modelId
|
|
1071
|
+
// alone can make a routed model masquerade as an off-snapshot native. Only the
|
|
1072
|
+
// canonical built-in ChatGPT forward provider should receive the native clamp.
|
|
942
1073
|
{
|
|
943
1074
|
const requestedModelId = logCtx.requestedModel ?? route.modelId;
|
|
944
|
-
const { nativeEffortClamp } = await import("../codex/catalog");
|
|
945
|
-
const clamped = requestedModelId
|
|
946
|
-
?
|
|
947
|
-
:
|
|
1075
|
+
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../codex/catalog");
|
|
1076
|
+
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, requestedModelId)
|
|
1077
|
+
? nativeEffortClamp(route.modelId, parsed.options.reasoning)
|
|
1078
|
+
: null;
|
|
948
1079
|
if (clamped) {
|
|
949
1080
|
parsed.options.reasoning = clamped;
|
|
950
1081
|
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
@@ -999,7 +1130,7 @@ export async function handleResponses(
|
|
|
999
1130
|
|
|
1000
1131
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
1001
1132
|
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
1002
|
-
const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot")
|
|
1133
|
+
const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
|
|
1003
1134
|
&& route.provider.authMode === "oauth";
|
|
1004
1135
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
1005
1136
|
if (route.provider.authMode === "oauth") {
|
|
@@ -1036,6 +1167,14 @@ export async function handleResponses(
|
|
|
1036
1167
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
|
|
1037
1168
|
const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
|
|
1038
1169
|
|
|
1170
|
+
if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
1171
|
+
return formatErrorResponse(
|
|
1172
|
+
400,
|
|
1173
|
+
"invalid_request_error",
|
|
1174
|
+
"Kiro continuation state is missing; start a new session instead of reusing this previous_response_id.",
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1039
1178
|
let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
|
|
1040
1179
|
const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
|
|
1041
1180
|
const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
|
|
@@ -1074,6 +1213,30 @@ export async function handleResponses(
|
|
|
1074
1213
|
|
|
1075
1214
|
const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
|
|
1076
1215
|
|
|
1216
|
+
const continuationStateForResponse = (
|
|
1217
|
+
emitted?: OcxProviderContinuationState,
|
|
1218
|
+
): OcxProviderContinuationState | undefined => {
|
|
1219
|
+
const cursorConversationId = parsed._cursorConversationId;
|
|
1220
|
+
const inherited = parsed._providerContinuation;
|
|
1221
|
+
if (!emitted && !inherited && !cursorConversationId) return undefined;
|
|
1222
|
+
return {
|
|
1223
|
+
...(inherited ?? {}),
|
|
1224
|
+
...(emitted ?? {}),
|
|
1225
|
+
...((inherited?.kiro || emitted?.kiro)
|
|
1226
|
+
? { kiro: { ...(inherited?.kiro ?? {}), ...(emitted?.kiro ?? {}) } }
|
|
1227
|
+
: {}),
|
|
1228
|
+
...(cursorConversationId
|
|
1229
|
+
? {
|
|
1230
|
+
cursor: {
|
|
1231
|
+
...(inherited?.cursor ?? {}),
|
|
1232
|
+
...(emitted?.cursor ?? {}),
|
|
1233
|
+
conversationId: cursorConversationId,
|
|
1234
|
+
},
|
|
1235
|
+
}
|
|
1236
|
+
: {}),
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1077
1240
|
// Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
|
|
1078
1241
|
// one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
|
|
1079
1242
|
// natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
|
|
@@ -1215,6 +1378,7 @@ export async function handleResponses(
|
|
|
1215
1378
|
// background for terminal-outcome/quota inspection only.
|
|
1216
1379
|
if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
|
|
1217
1380
|
const [nativeBody, inspectBody] = upstreamResponse.body.tee();
|
|
1381
|
+
const repairConfig = route.provider.responsesItemIdRepair;
|
|
1218
1382
|
const turnAc = new AbortController();
|
|
1219
1383
|
linkAbortSignal(upstream, turnAc.signal);
|
|
1220
1384
|
registerTurn(turnAc);
|
|
@@ -1251,9 +1415,12 @@ export async function handleResponses(
|
|
|
1251
1415
|
// win32 must keep the pure native relay (Bun#32111 JS-sink segfault); elsewhere a JS pull
|
|
1252
1416
|
// relay is established practice (relayWithAbort, relaySseWithHeartbeat) and lets a
|
|
1253
1417
|
// mid-stream reset end with a clean response.failed terminal instead of a raw socket error.
|
|
1254
|
-
const
|
|
1418
|
+
const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
1419
|
+
? relaySseWithResponsesItemIdRepair(nativeBody, repairConfig!)
|
|
1420
|
+
: nativeBody;
|
|
1421
|
+
const clientBody = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig)
|
|
1255
1422
|
? nativeBody
|
|
1256
|
-
: relaySseWithFailedTail(
|
|
1423
|
+
: relaySseWithFailedTail(repairedBody, upstream);
|
|
1257
1424
|
return markNativePassthroughSseResponse(new Response(clientBody, {
|
|
1258
1425
|
status: upstreamResponse.status,
|
|
1259
1426
|
headers,
|
|
@@ -1335,7 +1502,15 @@ export async function handleResponses(
|
|
|
1335
1502
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1336
1503
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1337
1504
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1338
|
-
...(routedCompaction ? {} : {
|
|
1505
|
+
...(routedCompaction ? {} : {
|
|
1506
|
+
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
|
|
1507
|
+
rememberResponseState(
|
|
1508
|
+
parsed._rawBody,
|
|
1509
|
+
response,
|
|
1510
|
+
continuationStateForResponse(providerState),
|
|
1511
|
+
adapter.name === "kiro" ? { force: true } : undefined,
|
|
1512
|
+
),
|
|
1513
|
+
}),
|
|
1339
1514
|
},
|
|
1340
1515
|
);
|
|
1341
1516
|
const bridgeTurnAc = new AbortController();
|
|
@@ -1356,14 +1531,23 @@ export async function handleResponses(
|
|
|
1356
1531
|
return formatErrorResponse(502, "upstream_error", redactSecretString(message));
|
|
1357
1532
|
}
|
|
1358
1533
|
}
|
|
1534
|
+
let providerState: OcxProviderContinuationState | undefined;
|
|
1359
1535
|
const json = buildResponseJSON(events, parsed.modelId, {
|
|
1360
1536
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1361
1537
|
toolNsMap,
|
|
1362
1538
|
freeformToolNames,
|
|
1363
1539
|
toolSearchToolNames,
|
|
1364
1540
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1541
|
+
onProviderState: state => { providerState = state; },
|
|
1365
1542
|
});
|
|
1366
|
-
if (!routedCompaction)
|
|
1543
|
+
if (!routedCompaction) {
|
|
1544
|
+
rememberResponseState(
|
|
1545
|
+
parsed._rawBody,
|
|
1546
|
+
json,
|
|
1547
|
+
continuationStateForResponse(providerState),
|
|
1548
|
+
adapter.name === "kiro" ? { force: true } : undefined,
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1367
1551
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
1368
1552
|
}
|
|
1369
1553
|
|
|
@@ -1420,17 +1604,18 @@ export async function handleResponses(
|
|
|
1420
1604
|
const upstream = new AbortController();
|
|
1421
1605
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
1422
1606
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
1607
|
+
let activeAdapter = adapter;
|
|
1423
1608
|
|
|
1424
|
-
const request = await
|
|
1609
|
+
const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
1425
1610
|
const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
1426
1611
|
? request.usageLog.inputTokens
|
|
1427
1612
|
: undefined;
|
|
1428
1613
|
if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
|
|
1429
1614
|
let upstreamResponse: Response;
|
|
1430
1615
|
try {
|
|
1431
|
-
if (
|
|
1616
|
+
if (activeAdapter.fetchResponse) {
|
|
1432
1617
|
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
|
|
1433
|
-
upstreamResponse = await
|
|
1618
|
+
upstreamResponse = await activeAdapter.fetchResponse(request, {
|
|
1434
1619
|
abortSignal: upstream.signal,
|
|
1435
1620
|
timeoutMs: connectMs,
|
|
1436
1621
|
stream: parsed.stream,
|
|
@@ -1464,7 +1649,6 @@ export async function handleResponses(
|
|
|
1464
1649
|
// both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
|
|
1465
1650
|
// adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
|
|
1466
1651
|
// 413→429 rotation cannot silently undo the tightening.
|
|
1467
|
-
let activeAdapter = adapter;
|
|
1468
1652
|
let imageTierBias = 0;
|
|
1469
1653
|
let imageRetryAttempted = false;
|
|
1470
1654
|
let oauth401ReplayAttempted = false;
|
|
@@ -1591,7 +1775,7 @@ export async function handleResponses(
|
|
|
1591
1775
|
}
|
|
1592
1776
|
|
|
1593
1777
|
if (parsed.stream) {
|
|
1594
|
-
const eventStream =
|
|
1778
|
+
const eventStream = activeAdapter.parseStream(upstreamResponse);
|
|
1595
1779
|
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
1596
1780
|
const sseStream = bridgeToResponsesSSE(
|
|
1597
1781
|
eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
@@ -1605,7 +1789,15 @@ export async function handleResponses(
|
|
|
1605
1789
|
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
1606
1790
|
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
1607
1791
|
// giant stale chain Codex just replaced.
|
|
1608
|
-
...(routedCompaction ? {} : {
|
|
1792
|
+
...(routedCompaction ? {} : {
|
|
1793
|
+
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
|
|
1794
|
+
rememberResponseState(
|
|
1795
|
+
parsed._rawBody,
|
|
1796
|
+
response,
|
|
1797
|
+
continuationStateForResponse(providerState),
|
|
1798
|
+
activeAdapter.name === "kiro" ? { force: true } : undefined,
|
|
1799
|
+
),
|
|
1800
|
+
}),
|
|
1609
1801
|
},
|
|
1610
1802
|
);
|
|
1611
1803
|
const bridgeTurnAc = new AbortController();
|
|
@@ -1615,27 +1807,36 @@ export async function handleResponses(
|
|
|
1615
1807
|
});
|
|
1616
1808
|
}
|
|
1617
1809
|
|
|
1618
|
-
if (
|
|
1810
|
+
if (activeAdapter.parseResponse) {
|
|
1619
1811
|
let events: AdapterEvent[];
|
|
1620
1812
|
try {
|
|
1621
|
-
events = await
|
|
1813
|
+
events = await activeAdapter.parseResponse(upstreamResponse);
|
|
1622
1814
|
} finally {
|
|
1623
1815
|
cleanupUpstreamAbort();
|
|
1624
1816
|
}
|
|
1625
1817
|
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
1818
|
+
let providerState: OcxProviderContinuationState | undefined;
|
|
1626
1819
|
const json = buildResponseJSON(events, parsed.modelId, {
|
|
1627
1820
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1628
1821
|
toolNsMap,
|
|
1629
1822
|
freeformToolNames,
|
|
1630
1823
|
toolSearchToolNames,
|
|
1631
1824
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1825
|
+
onProviderState: state => { providerState = state; },
|
|
1632
1826
|
});
|
|
1633
1827
|
// See the streaming branch: compaction turns skip the continuation cache.
|
|
1634
|
-
if (!routedCompaction)
|
|
1828
|
+
if (!routedCompaction) {
|
|
1829
|
+
rememberResponseState(
|
|
1830
|
+
parsed._rawBody,
|
|
1831
|
+
json,
|
|
1832
|
+
continuationStateForResponse(providerState),
|
|
1833
|
+
activeAdapter.name === "kiro" ? { force: true } : undefined,
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1635
1836
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
1636
1837
|
}
|
|
1637
1838
|
|
|
1638
|
-
return formatErrorResponse(
|
|
1839
|
+
return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
|
|
1639
1840
|
}
|
|
1640
1841
|
|
|
1641
1842
|
export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {
|