mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
* feat(sse): unified media-part detection helper (image+audio, input_image) * refactor(guardrails): extractImageParts/comboStructure delegate to unified media detector * fix(sse): media detector — audio parts no longer shadow sibling/nested image indicators * fix(guardrails): close extract↔replace contract for input_image (allowlist + splice) * perf(guardrails): skip media traversal when bridge disabled; short-circuit combo image check * feat(guardrails): in-memory LRU bridge cache (sha256 keyed) * feat(settings): modalityBridge* schema with legacy visionBridge* fallback * feat(db): migrate visionBridge* settings to modalityBridge* (idempotent) * refactor(guardrails): harden bridge cache key/config + settings resolution (review minors) * feat(guardrails): vision bridge mode selector (auto/describe/reroute) short-circuit * feat(guardrails): task-aware vision description prompt (default on) * feat(guardrails): describe-path cache integration * docs(guardrails): review polish — cache-key coupling notes + helper header * feat(guardrails): in-memory bridge stats + modality-bridge response header * feat(api): modality bridge stats endpoint + header wiring in chat handler * docs(guardrails): document modality bridge mode/task-aware/cache/header + stats endpoint * chore: untrack _tasks symlink (inherited from base tip; blocks pre-commit tracked-artifacts gate) * fix(db): renumber modality bridge migration 139->140 (base renumbered ccr_blocks to 139) * docs(guardrails): migration filename touch-up 139->140 * docs(db): stale comment touch-ups after 139->140 renumber and #9688 landing * fix(db): renumber modality bridge migration 140->141 (base renumbered connection_runtime_state to 140) * test(db): migration test titles 139->141 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
/**
|
|
* Task-aware vision description prompt (codex-vision-proxy pattern) —
|
|
* Modality Bridge PR-1. The describe path appends the user's last question as
|
|
* a focus hint so the vision model describes what is relevant to answering it
|
|
* instead of producing a generic caption. Default ON; disabled via
|
|
* `modalityBridgeVisionTaskAware: false`.
|
|
*
|
|
* Guardrail-level cases use `model: "auto/..."` + `mode: "describe"` so the
|
|
* whole flow is DB-free (the auto prefix skips the capability/combo lookups
|
|
* that open SQLite, and the forced describe mode skips the reroute block).
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { composeVisionPrompt } from "../../src/lib/guardrails/visionBridgeHelpers.ts";
|
|
import { VisionBridgeGuardrail } from "../../src/lib/guardrails/visionBridge.ts";
|
|
import type { VisionModelConfig } from "../../src/lib/guardrails/visionBridgeHelpers.ts";
|
|
|
|
// ── composeVisionPrompt (pure) ──────────────────────────────────────────────
|
|
|
|
test("appends user focus hint when taskAware", () => {
|
|
const p = composeVisionPrompt("Describe the image.", "qual o erro no screenshot?", true);
|
|
assert.ok(p.startsWith("Describe the image."));
|
|
assert.ok(p.includes("qual o erro no screenshot?"));
|
|
});
|
|
|
|
test("no hint when disabled or no user text", () => {
|
|
assert.equal(composeVisionPrompt("Base.", "pergunta", false), "Base.");
|
|
assert.equal(composeVisionPrompt("Base.", undefined, true), "Base.");
|
|
assert.equal(composeVisionPrompt("Base.", " ", true), "Base.");
|
|
});
|
|
|
|
test("hint truncated to 500 chars", () => {
|
|
const p = composeVisionPrompt("Base.", "x".repeat(2000), true);
|
|
assert.ok(p.length < 700, `expected truncated prompt, got length ${p.length}`);
|
|
assert.ok(p.includes("x".repeat(500)));
|
|
assert.ok(!p.includes("x".repeat(501)));
|
|
});
|
|
|
|
// ── Guardrail describe path wiring ──────────────────────────────────────────
|
|
|
|
function describeGuardrail(
|
|
settings: Record<string, unknown>,
|
|
capturedPrompts: string[]
|
|
): InstanceType<typeof VisionBridgeGuardrail> {
|
|
return new VisionBridgeGuardrail({
|
|
deps: {
|
|
getSettings: async () => ({ modalityBridgeVisionMode: "describe", ...settings }),
|
|
callVisionModel: async (_imageDataUri: string, config: VisionModelConfig) => {
|
|
capturedPrompts.push(config.prompt);
|
|
return "descrição";
|
|
},
|
|
hasUsableCredentials: async () => null,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Unique per-test payload: the describe path caches by image+prompt+model
|
|
* (Task 8), so reusing the same data URI across tests would make a later
|
|
* describe a cache hit and hide the upstream call whose prompt is asserted.
|
|
*/
|
|
function autoImageBody(uniqueRef: string, userText: string): Record<string, unknown> {
|
|
return {
|
|
model: "auto/task-aware",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: userText },
|
|
{
|
|
type: "image_url",
|
|
image_url: {
|
|
url: `data:image/png;base64,${Buffer.from(uniqueRef).toString("base64")}`,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test("describe call prompt contains the last user question (taskAware default on)", async () => {
|
|
const prompts: string[] = [];
|
|
const guardrail = describeGuardrail({}, prompts);
|
|
|
|
const result = await guardrail.preCall(
|
|
autoImageBody("task-aware-default-on-test", "qual o erro no screenshot?"),
|
|
{ model: "auto/task-aware", log: console }
|
|
);
|
|
|
|
assert.equal((result.meta ?? {}).imagesProcessed, 1);
|
|
assert.equal(prompts.length, 1);
|
|
assert.ok(
|
|
prompts[0].includes("qual o erro no screenshot?"),
|
|
`prompt should carry the user question, got: ${prompts[0]}`
|
|
);
|
|
});
|
|
|
|
test("modalityBridgeVisionTaskAware=false keeps the base prompt untouched", async () => {
|
|
const prompts: string[] = [];
|
|
const guardrail = describeGuardrail(
|
|
{ modalityBridgeVisionTaskAware: false, modalityBridgeVisionPrompt: "Base prompt." },
|
|
prompts
|
|
);
|
|
|
|
const result = await guardrail.preCall(
|
|
autoImageBody("task-aware-disabled-test", "pergunta que não deve vazar"),
|
|
{ model: "auto/task-aware", log: console }
|
|
);
|
|
|
|
assert.equal((result.meta ?? {}).imagesProcessed, 1);
|
|
assert.equal(prompts.length, 1);
|
|
assert.equal(prompts[0], "Base prompt.");
|
|
});
|