mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix combo vision and codex tool history
This commit is contained in:
@@ -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) ──
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user