mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 04:12:17 +03:00
fix(embeddings): send stored API key on private-host embeddings nodes (#13398)
When an embeddings provider is classified `authType: "none"` and no credentials resolved, the service now looks up the stored connection and promotes to bearer if it holds a key — so private-host/CGNAT embeddings nodes that do require a key stop being called anonymously (#13234). Security posture holds: `isNoAuthLocalEmbeddingHost` is `isPrivateHost(hostname) && !isCloudMetadataHost(hostname)`, so cloud metadata addresses never reach `authType: "none"` and therefore never reach the new branch; the key only ever goes to the host the operator configured on that connection. Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thanks @HouMinXi!
This commit is contained in:
1
changelog.d/fixes/13234-embed-lan-keyed-auth.md
Normal file
1
changelog.d/fixes/13234-embed-lan-keyed-auth.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(embeddings):** LAN/CGNAT OpenAI-compatible embeddings nodes with a stored API key now send `Authorization: Bearer` on the outbound request, matching dashboard Check. Keyless LAN nodes stay no-auth ([#6925](https://github.com/diegosouzapw/OmniRoute/issues/6925)) ([#13234](https://github.com/diegosouzapw/OmniRoute/issues/13234))
|
||||
@@ -344,6 +344,31 @@ export async function createEmbeddingResponse(
|
||||
) {
|
||||
credentials = localCredentials;
|
||||
}
|
||||
} else if (!credentials && providerConfig.authType === "none") {
|
||||
// #13234: private-host nodes are classified no-auth so a keyless
|
||||
// LAN Ollama still works (#6925). A stored API key on that same
|
||||
// node must still ride outbound, matching dashboard Check.
|
||||
const keyedCredentials = await getProviderCredentials(credentialsProviderId);
|
||||
if (
|
||||
keyedCredentials &&
|
||||
!("allRateLimited" in keyedCredentials) &&
|
||||
!("allExpired" in keyedCredentials)
|
||||
) {
|
||||
const token =
|
||||
(typeof (keyedCredentials as { apiKey?: unknown }).apiKey === "string" &&
|
||||
(keyedCredentials as { apiKey?: string }).apiKey) ||
|
||||
(typeof (keyedCredentials as { accessToken?: unknown }).accessToken === "string" &&
|
||||
(keyedCredentials as { accessToken?: string }).accessToken) ||
|
||||
"";
|
||||
if (token) {
|
||||
credentials = keyedCredentials;
|
||||
providerConfig = {
|
||||
...providerConfig,
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #474: when the request used a bare model name (no "/" — e.g. an alias that
|
||||
|
||||
165
tests/unit/embeddings-lan-keyed-auth-13234.test.ts
Normal file
165
tests/unit/embeddings-lan-keyed-auth-13234.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* #13234: a LAN/CGNAT OpenAI-compatible embeddings node with a stored API key
|
||||
* must send Authorization: Bearer on the outbound proxy request.
|
||||
*
|
||||
* Dashboard Check already does this via buildBearerHeaders. The embeddings
|
||||
* proxy did not: #6925 classified every private-host node as authType "none"
|
||||
* before credentials were loaded, so buildAuth dropped the key and the
|
||||
* upstream returned 401. Chat/completions against the same node/key worked.
|
||||
*
|
||||
* Keyless LAN nodes stay no-auth (#6925). Cloud-metadata hosts stay blocked.
|
||||
*/
|
||||
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-embed-lan-key-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createProviderNode } = await import("../../src/lib/db/providers/nodes.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
function stubEmbeddingFetch() {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured: { url: string; headers: Record<string, string> } | null = null;
|
||||
globalThis.fetch = async (url: RequestInfo | URL, options: RequestInit = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: (options.headers as Record<string, string>) || {},
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 3, total_tokens: 3 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
return {
|
||||
get captured() {
|
||||
return captured;
|
||||
},
|
||||
restore() {
|
||||
globalThis.fetch = originalFetch;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("#13234: 100.64 CGNAT embeddings node with stored key sends Authorization", async () => {
|
||||
const node = await createProviderNode({
|
||||
type: "openai-compatible-embeddings",
|
||||
name: "CGNAT Embed",
|
||||
prefix: "cgnatembed13234",
|
||||
apiType: "embeddings",
|
||||
baseUrl: "http://100.64.1.10:8080/v1",
|
||||
});
|
||||
|
||||
await providersDb.createProviderConnection({
|
||||
provider: node.id,
|
||||
authType: "apikey",
|
||||
name: "CGNAT Embed Key",
|
||||
apiKey: "sk-embed-13234",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {
|
||||
prefix: "cgnatembed13234",
|
||||
baseUrl: "http://100.64.1.10:8080/v1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchStub = stubEmbeddingFetch();
|
||||
try {
|
||||
const res = await createEmbeddingResponse({
|
||||
model: "cgnatembed13234/nomic-embed-text",
|
||||
input: "hello world",
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
} finally {
|
||||
fetchStub.restore();
|
||||
}
|
||||
|
||||
assert.ok(fetchStub.captured);
|
||||
assert.equal(
|
||||
fetchStub.captured!.url,
|
||||
"http://100.64.1.10:8080/v1/embeddings",
|
||||
"should hit the node's own embeddings endpoint"
|
||||
);
|
||||
assert.equal(
|
||||
fetchStub.captured!.headers.Authorization,
|
||||
"Bearer sk-embed-13234",
|
||||
"stored key must ride on the outbound embeddings request, matching Check"
|
||||
);
|
||||
});
|
||||
|
||||
test("#13234: keyless 10.x LAN embeddings node still sends no Authorization", async () => {
|
||||
await createProviderNode({
|
||||
type: "openai-compatible-embeddings",
|
||||
name: "LAN Ollama Keyless",
|
||||
prefix: "lanollama13234",
|
||||
apiType: "embeddings",
|
||||
baseUrl: "http://10.10.0.181:11434/v1",
|
||||
});
|
||||
|
||||
const fetchStub = stubEmbeddingFetch();
|
||||
try {
|
||||
const res = await createEmbeddingResponse({
|
||||
model: "lanollama13234/nomic-embed-text",
|
||||
input: "hello world",
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
} finally {
|
||||
fetchStub.restore();
|
||||
}
|
||||
|
||||
assert.ok(fetchStub.captured);
|
||||
assert.equal(
|
||||
fetchStub.captured!.headers.Authorization,
|
||||
undefined,
|
||||
"a keyless LAN provider must not receive a fabricated Authorization header"
|
||||
);
|
||||
});
|
||||
|
||||
test("#13234: LAN node with a keyless connection record still sends no Authorization", async () => {
|
||||
const node = await createProviderNode({
|
||||
type: "openai-compatible-embeddings",
|
||||
name: "LAN empty key",
|
||||
prefix: "lanempty13234",
|
||||
apiType: "embeddings",
|
||||
baseUrl: "http://10.20.0.5:11434/v1",
|
||||
});
|
||||
|
||||
await providersDb.createProviderConnection({
|
||||
provider: node.id,
|
||||
authType: "apikey",
|
||||
name: "LAN empty key conn",
|
||||
apiKey: "",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const fetchStub = stubEmbeddingFetch();
|
||||
try {
|
||||
const res = await createEmbeddingResponse({
|
||||
model: "lanempty13234/nomic-embed-text",
|
||||
input: "hello world",
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
} finally {
|
||||
fetchStub.restore();
|
||||
}
|
||||
|
||||
assert.ok(fetchStub.captured);
|
||||
assert.equal(
|
||||
fetchStub.captured!.headers.Authorization,
|
||||
undefined,
|
||||
"empty stored key must not fabricate Authorization",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user