Files
OmniRoute/tests/unit/guardrails/visionBridgeCredentialsPrefix.test.ts
initguru 9bc7eb8fd2 fix(vision-bridge): nested tool_result images + provider-prefix credential check (#12903)
* fix(vision-bridge): extract/replace images nested inside tool_result content

Claude Code sends tool_result images as {type:"image",source:{base64}}
nested inside a tool_result's content array, not as top-level content
parts. The vision-bridge guardrail's extractImageParts filtered nested
hits out (!p.nested), so these images were silently dropped — a
text-only executor then received a request with no image and returned
HTTP 400.

Port the path-based nested extraction/replace fix:
- MediaPart gains a path field: the key/index chain from
  message.content[partIndex] down to the media object itself.
- inspect() tracks the path through recursion; pushPart stamps it.
- extractImageParts drops the !p.nested gate and emits path for nested
  hits (extract↔replace contract preserved: same order, every hit
  replaceable).
- replaceImageParts rewrites via detectMediaParts: top-level hits swap
  their content slot, nested hits walk MediaPart.path via the new
  replaceObjectAtPath helper.
- ensureBase64ImagesForClaudeWire skips nested hits (.filter(!p.path))
  to keep its sequential index map aligned.

TDD: 7 failing tests (path field, nested extract, nested replace,
document order) → 47/47 pass. typecheck:core clean.

* fix(vision-bridge): resolve provider prefix to node id for credential check

Re-land 932002580 (2026-08-19), which was never merged: it branched off
7acddd91a and fell outside the group-D reimplementation range (4f01fba68
re-picked only cf4dfc868). The same root cause now surfaces on the reroute
path (visionBridgeRerouteTextOnly=true): hasUsableCredentialsForModel
queried provider_connections with the bare node prefix "skhynix" → 0 rows
→ false → getBestVisionModel discarded the configured fixed model and
auto-selected cloudflare-playground/moonshotai/kimi-k2.7-code → Playwright
chromium missing → 502 on every image-bearing request.

- resolveProviderCredentialIds: literal prefix + prefix-index mapped node
  id (no-op dedup), composed after #10760's alias→canonical
  resolveProviderId.
- getPrefixToNode: 60s-cached getProviderPrefixIndex lookup, fail-open
  null.
- hasUsableCredentialsForModel: loop the resolved provider ids and return
  true when any has a usable active connection; noauth empty-set
  semantics (#10702) preserved.

TDD: resolveProviderCredentialIds 4/4 + skhynix node-id integration test
(RED confirmed: false !== true on the reroute regression). Focused
regression green: visionBridgeCredentials 10/10, vision-bridge reroute/
credentials suite 12/12, vision-bridge policy/mode/cache 18/18,
visionBridgeRouter 16/16. typecheck:core clean.

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:30:54 -03:00

119 lines
4.7 KiB
TypeScript

/**
* Vision Bridge credential checks — provider-prefix → node-id resolution.
*
* Re-land of the lost fix 932002580 (2026-08-19, never merged). A compatible
* provider node (openai-compatible-chat-<uuid>) is stored in
* `provider_connections` under its generated node id, while the
* operator-facing model id uses the node's configured public prefix
* (e.g. `skhynix/HCP-Vision-Latest`). `hasUsableCredentialsForModel` queried
* the bare prefix → 0 rows → false → `getBestVisionModel` discarded the
* configured fixed model and auto-selected a noauth candidate
* (cloudflare-playground/moonshotai/kimi-k2.7-code) → whole-request reroute
* to an unreachable executor → 502 on every image-bearing request.
*
* This suite lives in its OWN file/process on purpose: the fix caches the
* prefix index for 60s, and node:test runs each file in its own process —
* seeding must happen before the first index read in this process, so the
* integration test must not share a process with tests that warm the cache
* against an empty node table.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-vb-cred-prefix-"));
// Set before any db import so getDbInstance() picks the temp dir.
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const providersDb = await import("../../../src/lib/db/providers.ts");
const { hasUsableCredentialsForModel, resolveProviderCredentialIds } =
await import("../../../src/lib/guardrails/visionBridgeCredentials.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
// ── resolveProviderCredentialIds (pure prefix→node id resolution) ───────────
test("resolveProviderCredentialIds returns the literal provider when no prefix mapping exists", () => {
assert.deepEqual(resolveProviderCredentialIds("openai", new Map()), ["openai"]);
});
test("resolveProviderCredentialIds appends the mapped node id for a known prefix", () => {
const prefixToNode = new Map([["skhynix", "openai-compatible-chat-abc-123"]]);
assert.deepEqual(resolveProviderCredentialIds("skhynix", prefixToNode), [
"skhynix",
"openai-compatible-chat-abc-123",
]);
});
test("resolveProviderCredentialIds dedupes when the mapping targets the literal provider", () => {
const prefixToNode = new Map([["openai", "openai"]]);
assert.deepEqual(resolveProviderCredentialIds("openai", prefixToNode), ["openai"]);
});
test("resolveProviderCredentialIds tolerates undefined prefix index", () => {
assert.deepEqual(resolveProviderCredentialIds("skhynix", undefined), ["skhynix"]);
});
// ── integration: node-id-stored connection found through the prefix (#re-land) ──
test("prefix-keyed model finds a row stored under the compatible node id (skhynix reroute regression)", async () => {
await resetStorage();
const uniq = `itest-${process.pid}-${Date.now()}`;
const nodeId = `openai-compatible-chat-${uniq}`;
const prefix = `skhynix-${uniq}`;
await providersDb.createProviderNode({
id: nodeId,
type: "openai-compatible",
name: "SK hynix (test)",
prefix,
apiType: "chat",
baseUrl: "http://localhost:1/v1",
});
// Phase 1 (negative): only a banned connection under the node id — the
// prefix must map to the node and the terminal row must still block it.
await providersDb.createProviderConnection({
provider: nodeId,
authType: "apikey",
apiKey: "sk-dead-key",
isActive: true,
testStatus: "banned",
});
assert.equal(
await hasUsableCredentialsForModel(`${prefix}/HCP-Vision-Latest`),
false,
"a banned connection under the mapped node id must not count as usable"
);
// Phase 2 (positive): an active keyed connection under the node id must be
// found through the prefix mapping. Before the fix this queried
// provider = "<prefix>" → 0 rows → false, so the Vision Bridge discarded
// the operator-configured model and auto-selected another provider.
await providersDb.createProviderConnection({
provider: nodeId,
authType: "apikey",
apiKey: "sk-hynix-key",
isActive: true,
testStatus: "active",
});
const usable = await hasUsableCredentialsForModel(`${prefix}/HCP-Vision-Latest`);
assert.equal(
usable,
true,
"prefix-keyed model must find the connection stored under the node id"
);
});