@bitkyc08/opencodex 2.47.0 → 2.48.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-B5r7LNHN.js +115 -0
- package/gui/dist/assets/index-D5SiRo8X.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/openai-responses.ts +16 -2
- package/src/adapters/opencode-go-additional-tools.ts +35 -0
- package/src/adapters/routed-agent-messages.ts +46 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/codex/auth-api.ts +211 -67
- package/src/codex/features.ts +3 -1
- package/src/codex/multi-agent-mode-policy.ts +24 -0
- package/src/codex/reset-credit-auto-redeem.ts +54 -13
- package/src/codex/routing.ts +85 -4
- package/src/codex/shim.ts +10 -1
- package/src/config/atomic-write.ts +2 -3
- package/src/config/initialize.ts +11 -6
- package/src/generated/compatibility-version.json +35 -23
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/providers/default-aliases.ts +4 -1
- package/src/providers/registry.ts +6 -1
- package/src/router.ts +5 -2
- package/src/server/claude-messages.ts +21 -4
- package/src/server/live.ts +8 -19
- package/src/server/management/agent-settings-routes.ts +3 -0
- package/src/server/responses/collaboration.ts +23 -12
- package/src/server/responses/combo-session-recall.ts +89 -0
- package/src/server/responses/compact.ts +19 -1
- package/src/server/responses/core.ts +189 -51
- package/src/types/config.ts +3 -3
- package/gui/dist/assets/index-B8ZZ0ivI.css +0 -1
- package/gui/dist/assets/index-BoXZzNaV.js +0 -115
- package/src/adapters/opencode-go.ts +0 -35
|
@@ -30,12 +30,13 @@ import {
|
|
|
30
30
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
31
31
|
import { estimateTokens } from "../lib/token-estimate";
|
|
32
32
|
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
33
|
+
import { registryEntryForProviderDestination } from "../providers/registry";
|
|
33
34
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
34
35
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
35
36
|
import type { OcxConfig } from "../types";
|
|
36
37
|
import { readJsonRequestBody } from "./request-decompress";
|
|
37
38
|
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
|
|
38
|
-
import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
|
|
39
|
+
import { conversationIdFromClaudeMetadata, normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
39
40
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
40
41
|
import { handleResponses } from "./responses";
|
|
41
42
|
import {
|
|
@@ -786,8 +787,12 @@ async function handleClaudeMessagesWithBudget(
|
|
|
786
787
|
// bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
|
|
787
788
|
// verified live 2026-07-11). Strip them for that route; routed providers keep them.
|
|
788
789
|
let nativeRoute = false;
|
|
790
|
+
let opencodeGoRoute = false;
|
|
789
791
|
try {
|
|
790
792
|
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
793
|
+
// Match the fixed key-auth destination before per-model wire overrides, including
|
|
794
|
+
// renamed Go providers without treating custom or lookalike URLs as Go.
|
|
795
|
+
opencodeGoRoute = registryEntryForProviderDestination(route.provider)?.id === "opencode-go";
|
|
791
796
|
// Settle the wire once so the sampling decision below reads the effective
|
|
792
797
|
// adapter rather than the provider-wide default (#404).
|
|
793
798
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic");
|
|
@@ -851,15 +856,27 @@ async function handleClaudeMessagesWithBudget(
|
|
|
851
856
|
headers.set("chatgpt-account-id", token.chatgptAccountId);
|
|
852
857
|
}
|
|
853
858
|
}
|
|
854
|
-
if (
|
|
859
|
+
if (opencodeGoRoute) {
|
|
860
|
+
const session = req.headers.get("x-opencode-session");
|
|
861
|
+
if (session) headers.set("x-opencode-session", session);
|
|
862
|
+
}
|
|
863
|
+
const hasExplicitGoSession = opencodeGoRoute
|
|
864
|
+
&& (sessionLaneIdFromRequest(headers) !== undefined
|
|
865
|
+
|| normalizeLogConversationId(headers.get("x-opencode-session")) !== undefined);
|
|
866
|
+
const synthesizeGoSession = opencodeGoRoute && !hasExplicitGoSession
|
|
867
|
+
&& isRec(anthropicBody)
|
|
868
|
+
&& conversationIdFromClaudeMetadata(isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined) !== undefined;
|
|
869
|
+
// Go can also use the Responses adapter; its eligibility gate must win on both wires.
|
|
870
|
+
if (opencodeGoRoute ? synthesizeGoSession : nativeRoute) {
|
|
855
871
|
// ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex
|
|
856
872
|
// clients always send their session uuid; devlog 090 follow-up: body-level
|
|
857
873
|
// prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends
|
|
858
|
-
// the header, so synthesize a stable per-session uuid from the same cache key
|
|
874
|
+
// the header, so synthesize a stable per-session uuid from the same cache key.
|
|
875
|
+
// Routed Go requests need this lane too for their x-opencode-session affinity —
|
|
859
876
|
// but ONLY for a real per-session key (metadata.user_id). The system-hash fallback
|
|
860
877
|
// key is shared across Desktop conversations, and a shared session_id's backend
|
|
861
878
|
// semantics are unproven (audit 133 R2#3): body prompt_cache_key only there.
|
|
862
|
-
if (cacheKeySource === "metadata" && !headers.has("session_id") && typeof internalBody.prompt_cache_key === "string") {
|
|
879
|
+
if (cacheKeySource === "metadata" && (synthesizeGoSession || !headers.has("session_id")) && typeof internalBody.prompt_cache_key === "string") {
|
|
863
880
|
headers.set("session_id", uuidFromHex(internalBody.prompt_cache_key));
|
|
864
881
|
}
|
|
865
882
|
}
|
package/src/server/live.ts
CHANGED
|
@@ -86,39 +86,29 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
|
|
|
86
86
|
*
|
|
87
87
|
* When `OCX_LIVE_FRAME_LOG` is set to a file path, every relayed sideband frame appends one
|
|
88
88
|
* JSONL record: direction, frame kind, byte length, and whether the payload contains U+FFFD.
|
|
89
|
-
* Privacy:
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* Privacy: no frame content is written, including excerpts around replacement characters.
|
|
90
|
+
* For binary frames, U+FFFD may also be introduced by UTF-8 decoding; the flag alone does not
|
|
91
|
+
* identify the source of corruption. Disabled entirely when the env var is unset.
|
|
92
92
|
*/
|
|
93
93
|
export const LIVE_FRAME_LOG_ENV = "OCX_LIVE_FRAME_LOG";
|
|
94
|
-
const LIVE_FRAME_LOG_CONTEXT_CHARS = 24;
|
|
95
|
-
|
|
96
|
-
function fffdContext(text: string): string | undefined {
|
|
97
|
-
const idx = text.indexOf("\uFFFD");
|
|
98
|
-
if (idx < 0) return undefined;
|
|
99
|
-
const start = Math.max(0, idx - LIVE_FRAME_LOG_CONTEXT_CHARS);
|
|
100
|
-
const end = Math.min(text.length, idx + LIVE_FRAME_LOG_CONTEXT_CHARS);
|
|
101
|
-
return text.slice(start, end);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
94
|
export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
|
|
105
95
|
const logPath = process.env[LIVE_FRAME_LOG_ENV];
|
|
106
96
|
if (!logPath) return;
|
|
107
97
|
try {
|
|
108
98
|
let kind: "text" | "binary" = "binary";
|
|
109
99
|
let bytes = 0;
|
|
110
|
-
let
|
|
100
|
+
let fffd = false;
|
|
111
101
|
if (typeof data === "string") {
|
|
112
102
|
kind = "text";
|
|
113
103
|
bytes = Buffer.byteLength(data);
|
|
114
|
-
|
|
104
|
+
fffd = data.includes("\uFFFD");
|
|
115
105
|
} else if (data instanceof ArrayBuffer) {
|
|
116
106
|
bytes = data.byteLength;
|
|
117
|
-
|
|
107
|
+
fffd = new TextDecoder().decode(new Uint8Array(data)).includes("\uFFFD");
|
|
118
108
|
} else if (ArrayBuffer.isView(data)) {
|
|
119
109
|
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
120
110
|
bytes = data.byteLength;
|
|
121
|
-
|
|
111
|
+
fffd = new TextDecoder().decode(view).includes("\uFFFD");
|
|
122
112
|
} else {
|
|
123
113
|
return;
|
|
124
114
|
}
|
|
@@ -127,8 +117,7 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
|
|
|
127
117
|
dir,
|
|
128
118
|
kind,
|
|
129
119
|
bytes,
|
|
130
|
-
fffd
|
|
131
|
-
...(context !== undefined ? { context } : {}),
|
|
120
|
+
fffd,
|
|
132
121
|
};
|
|
133
122
|
appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
134
123
|
} catch {
|
|
@@ -40,6 +40,7 @@ import { clearThreadAccountMap } from "../../codex/routing";
|
|
|
40
40
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
41
41
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
42
42
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
43
|
+
import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
|
|
43
44
|
import { readUsageEntries } from "../../usage/log";
|
|
44
45
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
45
46
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
@@ -248,6 +249,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
248
249
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
249
250
|
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
250
251
|
multiAgentModeHintText: getMultiAgentModeHintText(),
|
|
252
|
+
multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION,
|
|
251
253
|
// max_depth is V1-only upstream; this is the global-flag statement, derived
|
|
252
254
|
// server-side so no client can present it as an effective V2 limit.
|
|
253
255
|
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
@@ -421,6 +423,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
421
423
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
422
424
|
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
423
425
|
multiAgentModeHintText: getMultiAgentModeHintText(),
|
|
426
|
+
multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION,
|
|
424
427
|
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
425
428
|
warnings,
|
|
426
429
|
catalogRefresh,
|
|
@@ -241,6 +241,9 @@ export const PROACTIVE_MULTI_AGENT_MODE_TEXT = [
|
|
|
241
241
|
"This mode remains active until a later multi-agent mode developer message changes it.",
|
|
242
242
|
].join(" ");
|
|
243
243
|
|
|
244
|
+
const OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG = "<opencodex_subagent_guidance>";
|
|
245
|
+
const OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG = "</opencodex_subagent_guidance>";
|
|
246
|
+
|
|
244
247
|
export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
|
|
245
248
|
return collabSurface(parsed) === "v1";
|
|
246
249
|
}
|
|
@@ -468,18 +471,15 @@ export async function multiAgentGuidanceText(
|
|
|
468
471
|
// fallback only for explicit routed/account-qualified ids.
|
|
469
472
|
const promptModel = preferred?.model
|
|
470
473
|
?? (injectionModel?.includes("/") ? injectionModel : undefined);
|
|
471
|
-
return
|
|
474
|
+
return `${OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG}${applyInjectionPlaceholders(injectionPrompt, promptModel, injectionEffort, roster, fallbackGuidance)}${OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG}`;
|
|
472
475
|
}
|
|
473
476
|
if (!preferred && roster === "" && fallbackGuidance === "") return null;
|
|
474
|
-
let text = "
|
|
475
|
-
+ "
|
|
476
|
-
+ "When setting either override, set fork_turns to \"none\" "
|
|
477
|
-
+ "(or a positive turn count such as \"3\"; full-history forks reject overrides) "
|
|
478
|
-
+ "and make the task message self-contained.";
|
|
477
|
+
let text = "OpenCodex sub-agent routing metadata for this collaboration surface. "
|
|
478
|
+
+ "This metadata does not override Codex delegation or model-selection rules.";
|
|
479
479
|
if (preferred) {
|
|
480
480
|
text += ` Preferred sub-agent: model "${preferred.model}"`
|
|
481
481
|
+ (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
|
|
482
|
-
+ "
|
|
482
|
+
+ ".";
|
|
483
483
|
}
|
|
484
484
|
text += fallbackGuidance;
|
|
485
485
|
text += roster;
|
|
@@ -487,7 +487,7 @@ export async function multiAgentGuidanceText(
|
|
|
487
487
|
// Roster is the only unbounded part — drop it before breaking the budget.
|
|
488
488
|
text = text.slice(0, text.length - roster.length);
|
|
489
489
|
}
|
|
490
|
-
return
|
|
490
|
+
return `${OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG}${text}${OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG}`;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
const effort = parsed.options.reasoning;
|
|
@@ -544,6 +544,17 @@ function isGeneratedDeveloperItem(item: unknown, text: string): boolean {
|
|
|
544
544
|
return generatedDeveloperText(item) === text;
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
function generatedGuidanceFamily(text: string): "multi_agent_mode" | "opencodex_subagent_guidance" | undefined {
|
|
548
|
+
if (text.startsWith("<multi_agent_mode>") && text.endsWith("</multi_agent_mode>")) {
|
|
549
|
+
return "multi_agent_mode";
|
|
550
|
+
}
|
|
551
|
+
if (text.startsWith(OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG)
|
|
552
|
+
&& text.endsWith(OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG)) {
|
|
553
|
+
return "opencodex_subagent_guidance";
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
547
558
|
function isDeveloperPrefixItem(item: unknown): boolean {
|
|
548
559
|
if (!isRecord(item)) return false;
|
|
549
560
|
if (item.type === "additional_tools") return item.role === "developer";
|
|
@@ -583,13 +594,13 @@ export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string):
|
|
|
583
594
|
const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
|
|
584
595
|
if (rawInput) {
|
|
585
596
|
const replayPrefix = rawInput.slice(0, replayPrefixLen);
|
|
586
|
-
const
|
|
587
|
-
const lastTaggedGuidance =
|
|
597
|
+
const guidanceFamily = generatedGuidanceFamily(text);
|
|
598
|
+
const lastTaggedGuidance = guidanceFamily
|
|
588
599
|
? replayPrefix.map(generatedDeveloperText)
|
|
589
|
-
.filter(item => item
|
|
600
|
+
.filter(item => item !== undefined && generatedGuidanceFamily(item) === guidanceFamily)
|
|
590
601
|
.at(-1)
|
|
591
602
|
: undefined;
|
|
592
|
-
if (
|
|
603
|
+
if (guidanceFamily ? lastTaggedGuidance === text : replayPrefix.some(item => isGeneratedDeveloperItem(item, text))) {
|
|
593
604
|
return;
|
|
594
605
|
}
|
|
595
606
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Process-local recall of the last completed combo response on an explicit session lane. */
|
|
2
|
+
import { getCombo, targetKey } from "../../combos/types";
|
|
3
|
+
import { captureConfigGeneration, type GenerationContext } from "../../lib/state-store-sweeper";
|
|
4
|
+
import type { OcxConfig, OcxComboTarget } from "../../types";
|
|
5
|
+
|
|
6
|
+
interface ComboRecallEntry {
|
|
7
|
+
comboId: string;
|
|
8
|
+
target: Pick<OcxComboTarget, "provider" | "model">;
|
|
9
|
+
responseModel: string;
|
|
10
|
+
at: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RECALL_CAPACITY = 256;
|
|
14
|
+
const RECALL_TTL_MS = 30 * 60 * 1000;
|
|
15
|
+
const recall = new Map<string, ComboRecallEntry>();
|
|
16
|
+
let lastReconciledGeneration = 0;
|
|
17
|
+
let liveOwners: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames"> | undefined;
|
|
18
|
+
|
|
19
|
+
function ownsEntry(context: Pick<GenerationContext, "comboIds" | "comboTargets" | "providerNames">, entry: ComboRecallEntry): boolean {
|
|
20
|
+
return context.comboIds.has(entry.comboId)
|
|
21
|
+
&& context.providerNames.has(entry.target.provider)
|
|
22
|
+
&& context.comboTargets.has(`${entry.comboId}::${targetKey(entry.target)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rememberComboForLane(
|
|
26
|
+
lane: string | undefined,
|
|
27
|
+
comboId: string,
|
|
28
|
+
target: Pick<OcxComboTarget, "provider" | "model">,
|
|
29
|
+
responseModel: string,
|
|
30
|
+
writerGeneration: number,
|
|
31
|
+
): void {
|
|
32
|
+
if (!lane || !comboId || !responseModel.trim()) return;
|
|
33
|
+
// Reject even a same-named recreated owner: its previous in-flight turn is obsolete.
|
|
34
|
+
if (writerGeneration < Math.max(lastReconciledGeneration, captureConfigGeneration())) return;
|
|
35
|
+
const entry = { comboId, target: { provider: target.provider, model: target.model }, responseModel, at: Date.now() };
|
|
36
|
+
if (liveOwners && !ownsEntry(liveOwners, entry)) return;
|
|
37
|
+
recall.delete(lane);
|
|
38
|
+
recall.set(lane, entry);
|
|
39
|
+
while (recall.size > RECALL_CAPACITY) {
|
|
40
|
+
const oldest = recall.keys().next().value;
|
|
41
|
+
if (oldest === undefined) break;
|
|
42
|
+
recall.delete(oldest);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function recallComboForLane(
|
|
47
|
+
config: OcxConfig,
|
|
48
|
+
lane: string | undefined,
|
|
49
|
+
model: string,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (!lane || !model || model.includes("/")) return undefined;
|
|
52
|
+
const entry = recall.get(lane);
|
|
53
|
+
if (!entry) return undefined;
|
|
54
|
+
const combo = getCombo(config, entry.comboId);
|
|
55
|
+
const provider = config.providers[entry.target.provider];
|
|
56
|
+
if (Date.now() - entry.at >= RECALL_TTL_MS
|
|
57
|
+
|| !Object.hasOwn(config.providers, entry.target.provider)
|
|
58
|
+
|| !provider || provider.disabled === true
|
|
59
|
+
|| !combo?.targets.some(target => targetKey(target) === targetKey(entry.target))) {
|
|
60
|
+
recall.delete(lane);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return entry.responseModel === model ? entry.comboId : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function reconcileComboRecall(context: GenerationContext): number {
|
|
67
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
68
|
+
lastReconciledGeneration = context.generation;
|
|
69
|
+
liveOwners = {
|
|
70
|
+
comboIds: new Set(context.comboIds),
|
|
71
|
+
comboTargets: new Set(context.comboTargets),
|
|
72
|
+
providerNames: new Set(context.providerNames),
|
|
73
|
+
};
|
|
74
|
+
let removed = 0;
|
|
75
|
+
for (const [lane, entry] of recall) {
|
|
76
|
+
if (!ownsEntry(context, entry) || Date.now() - entry.at >= RECALL_TTL_MS) {
|
|
77
|
+
recall.delete(lane);
|
|
78
|
+
removed += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return removed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Test-only reset, alongside the combo rotation/cooldown resets. */
|
|
85
|
+
export function clearComboRecallForTests(): void {
|
|
86
|
+
recall.clear();
|
|
87
|
+
lastReconciledGeneration = 0;
|
|
88
|
+
liveOwners = undefined;
|
|
89
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
comboIdFromRawBody,
|
|
19
19
|
concreteComboRequestBody,
|
|
20
20
|
getCombo,
|
|
21
|
+
resolveComboId,
|
|
21
22
|
isComboTargetInCooldown,
|
|
22
23
|
NoAvailableComboTargetsError,
|
|
23
24
|
noteComboSuccess,
|
|
@@ -152,6 +153,7 @@ import {
|
|
|
152
153
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
153
154
|
import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
|
|
154
155
|
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
156
|
+
import { recallComboForLane } from "./combo-session-recall";
|
|
155
157
|
|
|
156
158
|
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
157
159
|
|
|
@@ -536,13 +538,29 @@ export async function handleResponsesCompact(
|
|
|
536
538
|
// a local rather than written back to `raw.model`: assigning to the property widens it out
|
|
537
539
|
// of the `string` narrowing the guard above just established.
|
|
538
540
|
const compactFastRow = parseFastOnlyRowId(config, () => raw.model as string);
|
|
539
|
-
|
|
541
|
+
let compactModel = compactFastRow ? compactFastRow.baseId : raw.model;
|
|
540
542
|
if (compactFastRow) (raw as Record<string, unknown>).model = compactModel;
|
|
541
543
|
// The client's own selector, kept for the request log: `raw.model` is rewritten to the
|
|
542
544
|
// base id above, and logCtx.requestedModel is assigned from it further down, so without
|
|
543
545
|
// this the log would lose which id the client actually asked for.
|
|
544
546
|
const compactRequestedModel = compactFastRow ? compactFastRow.baseId + "--fast" : raw.model;
|
|
545
547
|
|
|
548
|
+
// Recall the last completed client-visible bare model after a combo switch (#3891).
|
|
549
|
+
// Configured selectors take precedence over this implicit session hint.
|
|
550
|
+
if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
551
|
+
&& !resolveComboId(config, compactModel)) {
|
|
552
|
+
const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
|
|
553
|
+
if (recalledComboId) {
|
|
554
|
+
(raw as Record<string, unknown>).model = `combo/${recalledComboId}`;
|
|
555
|
+
// Keep the routed identity in sync: the bare model can 404 outright (no
|
|
556
|
+
// canonical openai provider) or resolve straight onto a native-compact
|
|
557
|
+
// provider, both bypassing combo failover. The combo selector resolves
|
|
558
|
+
// through tryPickComboModel, whose route.combo skips the native compact
|
|
559
|
+
// endpoint.
|
|
560
|
+
compactModel = `combo/${recalledComboId}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
546
564
|
let route;
|
|
547
565
|
try {
|
|
548
566
|
// Compact requests route through the same policy evaluation as normal
|