fix combo vision and codex tool history

This commit is contained in:
edutvlanka
2026-05-30 16:29:36 +05:30
parent 53c8ffcc34
commit 7ffc56b7e0
3 changed files with 120 additions and 18 deletions

View File

@@ -379,6 +379,46 @@ function stripStoredItemReferences(body: Record<string, unknown>): void {
}
}
function repairMissingCodexFunctionCallOutputs(body: Record<string, unknown>): void {
if (!Array.isArray(body.input)) return;
const existingOutputIds = new Set<string>();
for (const item of body.input) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const record = item as Record<string, unknown>;
if (record.type !== "function_call_output") continue;
if (typeof record.call_id === "string" && record.call_id.trim()) {
existingOutputIds.add(record.call_id.trim());
}
}
const repaired: unknown[] = [];
let insertedCount = 0;
for (const item of body.input) {
repaired.push(item);
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const record = item as Record<string, unknown>;
if (record.type !== "function_call") continue;
const callId = typeof record.call_id === "string" ? record.call_id.trim() : "";
if (!callId || existingOutputIds.has(callId)) continue;
repaired.push({
type: "function_call_output",
call_id: callId,
output: "",
});
existingOutputIds.add(callId);
insertedCount++;
}
if (insertedCount > 0) {
body.input = repaired;
console.debug(
`[Codex] repairMissingCodexFunctionCallOutputs: inserted ${insertedCount} empty function_call_output item(s)`
);
}
}
// Responses-API hosted tool types that OpenAI/Codex executes server-side.
// These arrive shaped as `{ type, ...params }` with no `function` object and no `name` —
// e.g. Codex CLI injects `{ type: "image_generation", output_format: "png" }` or
@@ -1138,6 +1178,7 @@ export class CodexExecutor extends BaseExecutor {
if (Array.isArray(body.input)) {
body.input = sanitizeResponsesInputItems(body.input, false);
}
repairMissingCodexFunctionCallOutputs(body);
// ── Cache-aware system prompt handling (both paths) ──
//

View File

@@ -18,11 +18,14 @@ import {
isVisionBridgeForcedModel,
} from "@/shared/constants/visionBridgeDefaults";
/// Check if a model with a combo mapping should trigger vision bridge processing.
/// Resolves the combo targets and only returns true if at least one target model
/// does NOT support vision natively. This avoids unnecessary vision model calls
/// when all combo targets can handle images directly.
async function shouldProcessImagesForComboModel(model: string): Promise<boolean> {
type ComboVisionBridgeDecision = "process" | "skip" | "not-combo";
/// Check if a combo model should trigger vision bridge processing.
/// Resolves combo targets and returns:
/// - "process" if any target cannot be proven vision-capable
/// - "skip" if all model targets can handle images directly
/// - "not-combo" when the model is not a combo/mapping
async function getComboVisionBridgeDecision(model: string): Promise<ComboVisionBridgeDecision> {
try {
const { getComboByName } = await import("@/lib/localDb");
const { resolveComboForModel } = await import("@/lib/db/modelComboMappings");
@@ -33,17 +36,17 @@ async function shouldProcessImagesForComboModel(model: string): Promise<boolean>
// 2. If no exact match, try model-combo mapping
if (!combo) {
const mapping = await resolveComboForModel(model);
if (!mapping) return false;
if (!mapping) return "not-combo";
const comboName = mapping.comboName ?? mapping.name ?? null;
if (!comboName) return false;
if (!comboName) return "not-combo";
combo = await getComboByName(comboName);
}
if (!combo) return false;
if (!combo) return "not-combo";
// 3. Get the combo's models (target steps)
const rawModels = (combo as Record<string, unknown>).models;
if (!Array.isArray(rawModels)) return false;
if (!Array.isArray(rawModels)) return "process";
// 4. Check each target for vision support
// combo-ref → conservative (process images)
@@ -52,27 +55,29 @@ async function shouldProcessImagesForComboModel(model: string): Promise<boolean>
let hasModelStep = false;
for (const step of rawModels) {
const s = step as Record<string, unknown>;
if (s.kind === "combo-ref") return true;
if (s.kind === "combo-ref") return "process";
if (s.kind === "model") {
hasModelStep = true;
const targetModel = s.model;
if (typeof targetModel === "string") {
const caps = getResolvedModelCapabilities(targetModel);
if (caps.supportsVision !== true) {
return true;
return "process";
}
} else {
return "process";
}
}
}
// All model steps support vision — safe to skip
if (hasModelStep) return false;
if (hasModelStep) return "skip";
// No recognizable steps — don't force bridge
return false;
return "not-combo";
} catch {
// On error, try to process images (conservative)
return true;
return "process";
}
}
@@ -120,15 +125,24 @@ export class VisionBridgeGuardrail extends BaseGuardrail {
// 4. Check if model supports vision
const capabilities = getResolvedModelCapabilities(model);
const comboVisionBridgeDecision = forceVisionBridge
? "process"
: this.deps.checkModelHasComboMapping
? (await this.deps.checkModelHasComboMapping(model))
? "process"
: "skip"
: await getComboVisionBridgeDecision(model);
if (comboVisionBridgeDecision === "skip") {
return { block: false };
}
if (capabilities.supportsVision === true && !forceVisionBridge) {
// The request model supports vision natively, but check if a
// model-combo mapping routes this model through a combo where
// some targets may NOT support vision. In that case, the vision
// bridge must process images so combo targets can describe them.
const hasMapping = this.deps.checkModelHasComboMapping
? await this.deps.checkModelHasComboMapping(model)
: await shouldProcessImagesForComboModel(model);
if (!hasMapping) {
if (comboVisionBridgeDecision !== "process") {
return { block: false };
}
// Combo mapping found — fall through to process images

View File

@@ -510,6 +510,53 @@ test("CodexExecutor.transformRequest strips raw internal assistant commentary wi
);
});
test("CodexExecutor.transformRequest inserts missing function_call_output items", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(
"gpt-5.5-xhigh",
{
_nativeCodexPassthrough: true,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Continue." }],
},
{
type: "function_call",
call_id: "call_missing_result",
name: "read_file",
arguments: "{}",
},
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Next turn." }],
},
],
stream: false,
},
false,
{
requestEndpointPath: "/responses",
}
);
const missingOutputIndex = result.input.findIndex(
(item) => item.type === "function_call_output" && item.call_id === "call_missing_result"
);
const functionCallIndex = result.input.findIndex(
(item) => item.type === "function_call" && item.call_id === "call_missing_result"
);
assert.equal(missingOutputIndex, functionCallIndex + 1);
assert.deepEqual(result.input[missingOutputIndex], {
type: "function_call_output",
call_id: "call_missing_result",
output: "",
});
});
test("CodexExecutor.transformRequest strips internal assistant commentary before mapping messages to input", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(