mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 23:52:18 +03:00
Compare commits
2 Commits
fix/11226-
...
fix/11233-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b83df1a75 | ||
|
|
592a7efc18 |
@@ -413,6 +413,11 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
|
||||
const EMBEDDING_PROVIDER_ALIASES: Record<string, string> = {
|
||||
jina: "jina-ai",
|
||||
voyage: "voyage-ai",
|
||||
// The dashboard stores LM Studio connections under the hyphenated provider
|
||||
// id "lm-studio" while the embedding registry keys the provider "lmstudio"
|
||||
// (#11233). Alias the dashboard id so "lm-studio/<model>" resolves instead
|
||||
// of failing with an unknown-provider 400.
|
||||
"lm-studio": "lmstudio",
|
||||
};
|
||||
|
||||
/** Family name used by clients; Jina's public SKU is omni-small. */
|
||||
|
||||
@@ -182,12 +182,8 @@ export async function handleEmbedding({
|
||||
)
|
||||
: [];
|
||||
const nativeModalities = [
|
||||
...(isJinaNativeEmbeddingInput(body.input)
|
||||
? collectJinaNativeModalities(body.input)
|
||||
: []),
|
||||
...(isGeminiNativeEmbeddingInput(body.input)
|
||||
? collectGeminiNativeModalities(body.input)
|
||||
: []),
|
||||
...(isJinaNativeEmbeddingInput(body.input) ? collectJinaNativeModalities(body.input) : []),
|
||||
...(isGeminiNativeEmbeddingInput(body.input) ? collectGeminiNativeModalities(body.input) : []),
|
||||
].filter((modality) => modality !== "text");
|
||||
if (structuredItems.length > 0 || nativeModalities.length > 0) {
|
||||
const supportedModalities = getEmbeddingModelModalities(providerConfig, model);
|
||||
@@ -266,7 +262,10 @@ export async function handleEmbedding({
|
||||
}
|
||||
|
||||
let upstreamUrl = providerConfig.baseUrl;
|
||||
if (provider === "ollama-local") {
|
||||
if (provider === "ollama-local" || provider === "lmstudio") {
|
||||
// Keyless local servers (#2824 ollama-local, #11233 lmstudio): honor the
|
||||
// configured connection's baseUrl when one was hydrated, and fall back to
|
||||
// the static localhost registry default otherwise.
|
||||
const configuredBaseUrl = credentials?.providerSpecificData?.baseUrl;
|
||||
const rawBaseUrl =
|
||||
typeof configuredBaseUrl === "string" && configuredBaseUrl.trim().length > 0
|
||||
@@ -277,11 +276,11 @@ export async function handleEmbedding({
|
||||
// (CodeQL js/polynomial-redos) since baseUrl is operator-configured
|
||||
// per-connection data. See open-sse/utils/urlSanitize.ts.
|
||||
const normalizedBaseUrl = stripTrailingSlashes(rawBaseUrl.trim());
|
||||
const ollamaHost = normalizedBaseUrl
|
||||
const localServerHost = normalizedBaseUrl
|
||||
.replace(/\/v1\/(?:chat\/completions|embeddings)$/i, "")
|
||||
.replace(/\/api\/chat$/i, "")
|
||||
.replace(/\/v1$/i, "");
|
||||
upstreamUrl = `${ollamaHost}/v1/embeddings`;
|
||||
upstreamUrl = `${localServerHost}/v1/embeddings`;
|
||||
}
|
||||
let normalizeProviderResponse:
|
||||
((data: Record<string, unknown>) => Record<string, unknown>) | null = null;
|
||||
@@ -321,10 +320,7 @@ export async function handleEmbedding({
|
||||
// become N embeddings. Native multimodal parts take the same path.
|
||||
const useGeminiNativeTransport =
|
||||
providerConfig.structuredInputProtocol === "gemini-embed-content" &&
|
||||
(isGeminiEmbedding2Family(model) ||
|
||||
canonicalStructured ||
|
||||
geminiNative ||
|
||||
jinaNative);
|
||||
(isGeminiEmbedding2Family(model) || canonicalStructured || geminiNative || jinaNative);
|
||||
|
||||
if (providerConfig.structuredInputProtocol === "jina-v1" && jinaNative && canonicalStructured) {
|
||||
try {
|
||||
@@ -462,13 +458,7 @@ export async function handleEmbedding({
|
||||
// best-effort.
|
||||
if (connectionId) {
|
||||
try {
|
||||
await markAccountUnavailable(
|
||||
connectionId,
|
||||
response.status,
|
||||
errorText,
|
||||
provider,
|
||||
model
|
||||
);
|
||||
await markAccountUnavailable(connectionId, response.status, errorText, provider, model);
|
||||
} catch {
|
||||
// swallow — the upstream error response takes priority
|
||||
}
|
||||
|
||||
@@ -249,11 +249,14 @@ export async function createEmbeddingResponse(
|
||||
`[${provider}] All ${credentials.expiredCount || 1} connection(s) authentication expired — please reconnect in the dashboard`
|
||||
);
|
||||
}
|
||||
} else if (provider === "ollama-local") {
|
||||
// Ollama is keyless, but a configured connection can still provide a
|
||||
// custom local host. Hydrate that optional connection without imposing an
|
||||
// authentication requirement, then keep the static localhost default when
|
||||
// no connection exists.
|
||||
} else if (provider === "ollama-local" || provider === "lmstudio") {
|
||||
// Ollama and LM Studio are keyless, but a configured connection can still
|
||||
// provide a custom local host. Hydrate that optional connection without
|
||||
// imposing an authentication requirement, then keep the static localhost
|
||||
// default when no connection exists. getProviderCredentials("lmstudio")
|
||||
// resolves the dashboard's hyphenated "lm-studio" connection via the
|
||||
// provider search pool/alias (#11233); a selection or rate-limit failure
|
||||
// must not break the flow — proceed without credentials.
|
||||
const localCredentials = await getProviderCredentials(credentialsProviderId);
|
||||
if (
|
||||
localCredentials &&
|
||||
|
||||
144
tests/unit/lmstudio-connection-baseurl-11233.test.ts
Normal file
144
tests/unit/lmstudio-connection-baseurl-11233.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-lmstudio-embedding-11233-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { parseEmbeddingModel } = await import("../../open-sse/config/embeddingRegistry.ts");
|
||||
const { handleEmbedding } = await import("../../open-sse/handlers/embeddings.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createProviderConnection } = await import("../../src/lib/db/providers.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Issue #11233: the dashboard stores LM Studio connections under the provider
|
||||
// id "lm-studio" (hyphenated), but the embedding registry keys the provider as
|
||||
// "lmstudio" with no alias. Two symptoms resulted:
|
||||
// 1. "lm-studio/<model>" embedding requests failed with 400 unknown provider.
|
||||
// 2. "lmstudio/<model>" requests always hit the hardcoded localhost:1234
|
||||
// endpoint, ignoring the baseUrl of the configured connection.
|
||||
// The fix mirrors the ollama-local pattern from #2824/#9225: an embedding
|
||||
// provider alias plus optional (non-auth) connection hydration and the same
|
||||
// baseUrl normalization in the handler.
|
||||
|
||||
test("lm-studio model strings resolve to the lmstudio embedding provider", () => {
|
||||
assert.deepEqual(parseEmbeddingModel("lm-studio/nomic-embed-text"), {
|
||||
provider: "lmstudio",
|
||||
model: "nomic-embed-text",
|
||||
});
|
||||
});
|
||||
|
||||
test("lmstudio routes to the configured connection baseUrl", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl: string | null = null;
|
||||
globalThis.fetch = async (url) => {
|
||||
capturedUrl = String(url);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleEmbedding({
|
||||
body: { model: "lmstudio/nomic-embed-text", input: "hello" },
|
||||
resolvedProvider: {
|
||||
id: "lmstudio",
|
||||
baseUrl: "http://localhost:1234/v1/embeddings",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
models: [],
|
||||
},
|
||||
resolvedModel: "nomic-embed-text",
|
||||
credentials: {
|
||||
providerSpecificData: { baseUrl: "http://192.168.1.50:1234/v1" },
|
||||
},
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(capturedUrl, "http://192.168.1.50:1234/v1/embeddings");
|
||||
});
|
||||
|
||||
test("lmstudio keeps the static localhost default without credentials", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl: string | null = null;
|
||||
globalThis.fetch = async (url) => {
|
||||
capturedUrl = String(url);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.3, 0.4], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleEmbedding({
|
||||
body: { model: "lmstudio/nomic-embed-text", input: "hello" },
|
||||
credentials: null,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(capturedUrl, "http://localhost:1234/v1/embeddings");
|
||||
});
|
||||
|
||||
test("lmstudio service hydrates the lm-studio connection host without requiring a key", async () => {
|
||||
await createProviderConnection({
|
||||
provider: "lm-studio",
|
||||
authType: "none",
|
||||
name: "LAN LM Studio",
|
||||
isActive: true,
|
||||
providerSpecificData: { baseUrl: "http://10.20.0.60:1234/v1/" },
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured: { url: string; headers: Record<string, string> } | null = null;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: (options.headers as Record<string, string>) || {},
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.5, 0.6], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await createEmbeddingResponse({
|
||||
model: "lm-studio/nomic-embed-text",
|
||||
input: "hello",
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "http://10.20.0.60:1234/v1/embeddings");
|
||||
assert.equal(captured.headers.Authorization, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user