fix(types): resolve context-relay payload resolution and typing issues

- Fix `handoffProviders` and `nextBody` unknown property TS errors in contextHandoff
- Correct fallback payload parsing from responses `input` array in combo service
This commit is contained in:
diegosouzapw
2026-04-08 16:43:46 -03:00
parent f5cd841056
commit 02128618b0
8 changed files with 304 additions and 15 deletions
+7 -1
View File
@@ -1038,13 +1038,19 @@ export async function handleComboChat({
const resetCandidates = [quotaInfo.window5h?.resetAt, quotaInfo.window7d?.resetAt]
.filter((value): value is string => typeof value === "string" && value.length > 0)
.sort();
const handoffSourceMessages =
Array.isArray(body?.messages) && body.messages.length > 0
? body.messages
: Array.isArray(body?.input)
? body.input
: [];
maybeGenerateHandoff({
sessionId: relayOptions.sessionId,
comboName: combo.name,
connectionId,
percentUsed: quotaInfo.percentUsed,
messages: Array.isArray(body?.messages) ? body.messages : [],
messages: handoffSourceMessages,
model: modelStr,
expiresAt: resetCandidates[0] || null,
config: relayConfig,
+27 -2
View File
@@ -63,7 +63,7 @@ export function resolveContextRelayConfig(
const rawMaxMessages = Number(config?.maxMessagesForSummary);
const hasExplicitProviders = Array.isArray(config?.handoffProviders);
const handoffProviders = hasExplicitProviders
? config?.handoffProviders
? (config?.handoffProviders as unknown[])
.map((item) => (typeof item === "string" ? item.trim().toLowerCase() : ""))
.filter(Boolean)
: ["codex"];
@@ -370,9 +370,34 @@ export function injectHandoffIntoBody(
body: Record<string, unknown>,
payload: HandoffPayload
): Record<string, unknown> {
const handoffContent = buildHandoffSystemMessage(payload);
const isResponsesRequest =
Object.prototype.hasOwnProperty.call(body, "input") ||
Object.prototype.hasOwnProperty.call(body, "instructions");
if (isResponsesRequest) {
const existingInstructions =
typeof body.instructions === "string" && body.instructions.trim().length > 0
? body.instructions
: "";
const nextBody: Record<string, unknown> = {
...body,
instructions: existingInstructions
? `${handoffContent}\n\n${existingInstructions}`
: handoffContent,
};
if (Array.isArray(nextBody.messages) && nextBody.messages.length === 0) {
const { messages: _messages, ...rest } = nextBody;
return rest;
}
return nextBody;
}
const handoffMessage = {
role: "system",
content: buildHandoffSystemMessage(payload),
content: handoffContent,
};
const messages = Array.isArray(body.messages) ? [...body.messages] : [];
+66 -4
View File
@@ -1210,9 +1210,34 @@ function TestResultsView({ results }) {
// Combo Form Modal
// ─────────────────────────────────────────────
function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
type CreateDraftSnapshot = {
name: string;
models: unknown[];
strategy: string;
config: Record<string, unknown>;
showAdvanced: boolean;
nameError: string;
agentSystemMessage: string;
agentToolFilter: string;
agentContextCache: boolean;
};
const getEmptyCreateDraftSnapshot = (): CreateDraftSnapshot => ({
name: "",
models: [],
strategy: "priority",
config: {},
showAdvanced: false,
nameError: "",
agentSystemMessage: "",
agentToolFilter: "",
agentContextCache: false,
});
const t = useTranslations("combos");
const tc = useTranslations("common");
const notify = useNotificationStore();
const createDraftStateRef = useRef<CreateDraftSnapshot>(getEmptyCreateDraftSnapshot());
const [name, setName] = useState(combo?.name || "");
const [models, setModels] = useState(() => {
return (combo?.models || []).map((m) => normalizeModelEntry(m));
@@ -1260,6 +1285,30 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
[setAgentContextCache]
);
useEffect(() => {
createDraftStateRef.current = {
name,
models,
strategy,
config,
showAdvanced,
nameError,
agentSystemMessage,
agentToolFilter,
agentContextCache,
};
}, [
name,
models,
strategy,
config,
showAdvanced,
nameError,
agentSystemMessage,
agentToolFilter,
agentContextCache,
]);
// DnD state
const hasPricingForModel = useCallback(
(modelValue) => {
@@ -1405,17 +1454,30 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
};
}
createDraftStateRef.current = getEmptyCreateDraftSnapshot();
resetFormForCombo(null, null);
const loadDefaults = async () => {
try {
const response = await fetch("/api/settings/combo-defaults");
const data = response.ok ? await response.json() : {};
if (!cancelled) {
const draft = createDraftStateRef.current;
const isPristineDraft =
draft.name.trim().length === 0 &&
draft.models.length === 0 &&
draft.strategy === "priority" &&
Object.keys(draft.config || {}).length === 0 &&
draft.showAdvanced === false &&
draft.nameError.length === 0 &&
draft.agentSystemMessage.length === 0 &&
draft.agentToolFilter.length === 0 &&
draft.agentContextCache === false;
if (!cancelled && isPristineDraft) {
resetFormForCombo(null, data.comboDefaults || null);
}
} catch {
if (!cancelled) {
resetFormForCombo(null, null);
}
// Keep the blank create form if defaults fail to load.
}
};
+1 -2
View File
@@ -509,8 +509,7 @@ async function handleSingleModelChat(
comboStrategy === "context-relay" &&
comboName &&
runtimeOptions.sessionId &&
body?._omnirouteSkipContextRelay !== true &&
!excludeConnectionId
body?._omnirouteSkipContextRelay !== true
) {
const handoff = getHandoff(runtimeOptions.sessionId, comboName);
if (handoff && handoff.fromAccount !== credentials.connectionId) {
@@ -203,6 +203,8 @@ export async function createChatPipelineHarness(prefix) {
async function resetStorage() {
globalThis.fetch = originalFetch;
clearInflight();
idempotencyLayerModule.clearIdempotency();
semanticCacheModule.clearCache();
resetAllAvailability();
resetAllCircuitBreakers();
apiKeysDb.resetApiKeyState();
@@ -220,6 +222,8 @@ export async function createChatPipelineHarness(prefix) {
BaseExecutor.RETRY_CONFIG.delayMs = originalRetryDelayMs;
globalThis.fetch = originalFetch;
clearInflight();
idempotencyLayerModule.clearIdempotency();
semanticCacheModule.clearCache();
clearSkillState();
resetAllAvailability();
resetAllCircuitBreakers();
+153 -1
View File
@@ -4,7 +4,7 @@ import assert from "node:assert/strict";
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.mjs";
const harness = await createChatPipelineHarness("chat-context-relay");
const { buildRequest, combosDb, handleChat, resetStorage, waitFor } = harness;
const { BaseExecutor, buildRequest, combosDb, handleChat, resetStorage, waitFor } = harness;
const providersDb = await import("../../src/lib/db/providers.ts");
const handoffDb = await import("../../src/lib/db/contextHandoffs.ts");
@@ -195,3 +195,155 @@ test("handleChat generates and injects context-relay handoffs across Codex accou
assert.equal(handoffDb.getHandoff(sessionId, "relay-combo"), null);
await new Promise((resolve) => setTimeout(resolve, 50));
});
test("handleChat injects context-relay handoffs during live failover for Responses-native Codex requests", async () => {
BaseExecutor.RETRY_CONFIG.delayMs = 1;
const primary = await seedCodexOAuthConnection({
name: "codex-live-a",
email: "relay-live-a@example.com",
accessToken: "token-a",
refreshToken: "refresh-a",
workspaceId: "ws-a",
priority: 1,
});
await seedCodexOAuthConnection({
name: "codex-live-b",
email: "relay-live-b@example.com",
accessToken: "token-b",
refreshToken: "refresh-b",
workspaceId: "ws-b",
priority: 2,
});
await combosDb.createCombo({
name: "relay-live-combo",
strategy: "context-relay",
config: {
maxRetries: 0,
retryDelayMs: 0,
handoffThreshold: 0.85,
maxMessagesForSummary: 12,
},
models: ["codex/gpt-5.4"],
});
const upstreamBodies = [];
let primaryRequestCount = 0;
globalThis.fetch = async (url, init = {}) => {
const urlStr = String(url);
const headers = Object.fromEntries(new Headers(init.headers || {}).entries());
const authHeader = headers.authorization || headers.Authorization || "";
if (urlStr.includes("/backend-api/wham/usage")) {
return authHeader === "Bearer token-a" ? buildQuotaResponse(87) : buildQuotaResponse(20);
}
const body = init.body ? JSON.parse(String(init.body)) : {};
const serializedBody = JSON.stringify(body);
const isSummaryRequest =
body._omnirouteInternalRequest === "context-handoff" ||
serializedBody.includes("You are a context summarizer");
if (isSummaryRequest) {
return buildResponsesResponse(
JSON.stringify({
summary: "Carry over the Responses-native Codex session",
keyDecisions: ["Keep the request in /responses format"],
taskProgress: "Continue after the first account is exhausted",
activeEntities: ["src/sse/handlers/chat.ts", "open-sse/services/contextHandoff.ts"],
}),
"gpt-5.4"
);
}
upstreamBodies.push({ authHeader, body, serializedBody });
if (authHeader === "Bearer token-a") {
primaryRequestCount += 1;
if (primaryRequestCount >= 2) {
return new Response(JSON.stringify({ error: { message: "quota exceeded" } }), {
status: 429,
headers: { "content-type": "application/json" },
});
}
}
return buildResponsesResponse("relay-success", "gpt-5.4");
};
const firstResponse = await handleChat(
buildRequest({
headers: {
"X-Session-Id": "relay-live-session",
"X-OmniRoute-No-Cache": "true",
},
body: {
model: "relay-live-combo",
stream: false,
instructions: "Preserve the original Responses instructions",
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Keep implementing the new combo" }],
},
],
},
})
);
assert.equal(firstResponse.status, 200);
const sessionId = "ext:relay-live-session";
const savedHandoff = await waitFor(
() => handoffDb.getHandoff(sessionId, "relay-live-combo"),
2000
);
assert.ok(savedHandoff);
assert.equal(savedHandoff.fromAccount, primary.id);
const secondResponse = await handleChat(
buildRequest({
headers: {
"X-Session-Id": "relay-live-session",
"X-OmniRoute-No-Cache": "true",
},
body: {
model: "relay-live-combo",
stream: false,
instructions: "Continue with the current task",
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Continue from where you left off" }],
},
],
},
})
);
assert.equal(secondResponse.status, 200);
const relayedSecondaryCall = upstreamBodies.find(
(call) =>
call.authHeader === "Bearer token-b" &&
typeof call.body.instructions === "string" &&
call.body.instructions.includes("<context_handoff>")
);
assert.ok(relayedSecondaryCall);
assert.equal("messages" in relayedSecondaryCall.body, false);
assert.deepEqual(
relayedSecondaryCall.body.input[0].content[0].text,
"Continue from where you left off"
);
assert.match(
relayedSecondaryCall.body.instructions,
/Carry over the Responses-native Codex session/
);
assert.match(relayedSecondaryCall.body.instructions, /Continue with the current task/);
assert.equal(handoffDb.getHandoff(sessionId, "relay-live-combo"), null);
});
+39 -5
View File
@@ -48,8 +48,8 @@ test("buildHandoffSystemMessage and injectHandoffIntoBody preserve existing hist
messageCount: 42,
model: "codex/gpt-5.4",
warningThresholdPct: 0.85,
generatedAt: "2026-04-08T12:00:00.000Z",
expiresAt: "2026-04-08T17:00:00.000Z",
generatedAt: "2099-04-08T12:00:00.000Z",
expiresAt: "2099-04-08T17:00:00.000Z",
};
const body = {
messages: [
@@ -69,6 +69,40 @@ test("buildHandoffSystemMessage and injectHandoffIntoBody preserve existing hist
assert.equal(body.messages.length, 2);
});
test("injectHandoffIntoBody preserves Responses API shape for native Codex requests", () => {
const payload = {
sessionId: "sess-1",
comboName: "relay-combo",
fromAccount: "conn-a",
summary: "Keep the current plan and continue seamlessly",
keyDecisions: ["prefer Responses-native payloads"],
taskProgress: "Need to carry state across account switches",
activeEntities: ["chat.ts", "contextHandoff.ts"],
messageCount: 8,
model: "codex/gpt-5.4",
warningThresholdPct: 0.85,
generatedAt: "2099-04-08T12:00:00.000Z",
expiresAt: "2099-04-08T17:00:00.000Z",
};
const body = {
instructions: "Original instructions",
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Continue" }],
},
],
};
const injected = contextHandoff.injectHandoffIntoBody(body, payload);
assert.equal("messages" in injected, false);
assert.deepEqual(injected.input, body.input);
assert.match(String(injected.instructions), /<context_handoff>/);
assert.match(String(injected.instructions), /Original instructions/);
});
test("parseHandoffJSON accepts fenced JSON and normalizes fields", () => {
const parsed = contextHandoff.parseHandoffJSON(`\`\`\`json
{"summary":" Ready to continue ","keyDecisions":["A","B"],"taskProgress":"Pending tests","activeEntities":["file.ts","combo.ts"]}
@@ -131,7 +165,7 @@ test("maybeGenerateHandoff persists a structured handoff once the threshold is r
{ role: "assistant", content: "Working on it" },
],
model: "codex/gpt-5.4",
expiresAt: "2026-04-08T17:00:00.000Z",
expiresAt: "2099-04-08T17:00:00.000Z",
handleSingleModel: async (body, modelStr) => {
calls.push({ body, modelStr });
return new Response(
@@ -308,7 +342,7 @@ test("context handoff DB module upserts and deletes active handoffs", () => {
messageCount: 3,
model: "codex/gpt-5.4",
warningThresholdPct: 0.85,
generatedAt: "2026-04-08T10:00:00.000Z",
generatedAt: "2099-04-08T10:00:00.000Z",
expiresAt: "2099-01-01T00:00:00.000Z",
});
handoffDb.upsertHandoff({
@@ -322,7 +356,7 @@ test("context handoff DB module upserts and deletes active handoffs", () => {
messageCount: 4,
model: "codex/gpt-5.4",
warningThresholdPct: 0.86,
generatedAt: "2026-04-08T11:00:00.000Z",
generatedAt: "2099-04-08T11:00:00.000Z",
expiresAt: "2099-01-01T00:00:00.000Z",
});
@@ -18,6 +18,7 @@ import {
QODER_CONFIG,
QWEN_CONFIG,
} from "../../src/lib/oauth/constants/oauth.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
const originalFetch = globalThis.fetch;
@@ -277,6 +278,12 @@ test("provider-specific config shapes remain valid for special cases", () => {
assert.equal(typeof KILOCODE_CONFIG.pollUrlBase, "string");
});
test("Gemini OAuth defaults do not hardcode a client secret fallback", () => {
assert.equal(GEMINI_CONFIG.clientSecret, process.env.GEMINI_OAUTH_CLIENT_SECRET || "");
assert.equal(REGISTRY.gemini.oauth.clientSecretDefault, "");
assert.equal(REGISTRY["gemini-cli"].oauth.clientSecretDefault, "");
});
test("Qoder remains a safe special case when browser OAuth is disabled", () => {
if (!QODER_CONFIG.enabled) {
assert.equal(