Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
408
extensions/ollama/src/embedding-provider.ts
Normal file
408
extensions/ollama/src/embedding-provider.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
// Ollama provider module implements model/runtime integration.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
|
||||
import {
|
||||
isKnownEnvApiKeyMarker,
|
||||
isNonSecretApiKeyMarker,
|
||||
normalizeOptionalSecretInput,
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveEnvApiKey } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import {
|
||||
formatErrorMessage,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedOrigin,
|
||||
type SsrFPolicy,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { fetchConfiguredLocalOriginWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime-internal";
|
||||
import { OLLAMA_CLOUD_BASE_URL } from "./defaults.js";
|
||||
import { normalizeOllamaWireModelId } from "./model-id.js";
|
||||
import { readProviderBaseUrl } from "./provider-base-url.js";
|
||||
import { resolveOllamaApiBase } from "./provider-models.js";
|
||||
|
||||
export type OllamaEmbeddingProvider = {
|
||||
id: string;
|
||||
model: string;
|
||||
maxInputTokens?: number;
|
||||
embedQuery: (text: string, options?: { signal?: AbortSignal }) => Promise<number[]>;
|
||||
embedBatch: (texts: string[], options?: { signal?: AbortSignal }) => Promise<number[][]>;
|
||||
};
|
||||
|
||||
type OllamaEmbeddingOptions = {
|
||||
config: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
provider?: string;
|
||||
remote?: {
|
||||
baseUrl?: string;
|
||||
apiKey?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
model: string;
|
||||
fallback?: string;
|
||||
local?: unknown;
|
||||
outputDimensionality?: number;
|
||||
taskType?: unknown;
|
||||
};
|
||||
|
||||
export type OllamaEmbeddingClient = {
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
model: string;
|
||||
outputDimensionality?: number;
|
||||
embedBatch: (texts: string[]) => Promise<number[][]>;
|
||||
};
|
||||
|
||||
type OllamaEmbeddingClientConfig = Omit<OllamaEmbeddingClient, "embedBatch">;
|
||||
|
||||
export const DEFAULT_OLLAMA_EMBEDDING_MODEL = "nomic-embed-text";
|
||||
const OLLAMA_EMBED_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
|
||||
const QUERY_INSTRUCTION_TEMPLATES = [
|
||||
{
|
||||
prefix: "qwen3-embedding",
|
||||
template:
|
||||
"Instruct: Given a user query, retrieve relevant memory notes and documents\nQuery:{query}",
|
||||
},
|
||||
{
|
||||
prefix: "nomic-embed-text",
|
||||
template: "search_query: {query}",
|
||||
},
|
||||
{
|
||||
prefix: "mxbai-embed-large",
|
||||
template: "Represent this sentence for searching relevant passages: {query}",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function sanitizeAndNormalizeEmbedding(vec: unknown[], outputDimensionality?: number): number[] {
|
||||
const selected =
|
||||
typeof outputDimensionality === "number" ? vec.slice(0, outputDimensionality) : vec;
|
||||
const sanitized = selected.map((value) => {
|
||||
if (typeof value !== "number") {
|
||||
throw new Error("Ollama embed response contains a non-number embedding value");
|
||||
}
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
});
|
||||
const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0));
|
||||
if (magnitude < 1e-10) {
|
||||
return sanitized;
|
||||
}
|
||||
return sanitized.map((value) => value / magnitude);
|
||||
}
|
||||
|
||||
async function withRemoteHttpResponse<T>(params: {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
signal?: AbortSignal;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
configuredLocalOriginBaseUrl: string;
|
||||
onResponse: (response: Response) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const { response, release } = await fetchConfiguredLocalOriginWithSsrFGuard({
|
||||
url: params.url,
|
||||
init: params.init,
|
||||
signal: params.signal,
|
||||
policy: params.ssrfPolicy,
|
||||
configuredLocalOriginBaseUrl: params.configuredLocalOriginBaseUrl,
|
||||
auditContext: "ollama-memory-embedding",
|
||||
});
|
||||
try {
|
||||
return await params.onResponse(response);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function readOllamaEmbeddingJsonResponse(
|
||||
response: Response,
|
||||
): Promise<{ embeddings?: unknown }> {
|
||||
const payload = await readProviderJsonResponse<unknown>(response, "Ollama embed response");
|
||||
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
||||
throw new Error("Ollama embed response returned a non-object JSON payload");
|
||||
}
|
||||
return payload as { embeddings?: unknown };
|
||||
}
|
||||
|
||||
function normalizeEmbeddingModel(model: string, providerId?: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_OLLAMA_EMBEDDING_MODEL;
|
||||
}
|
||||
return normalizeOllamaWireModelId(trimmed, providerId);
|
||||
}
|
||||
|
||||
function applyQueryInstructionTemplate(model: string, queryText: string): string {
|
||||
const normalizedModel = model.trim().toLowerCase();
|
||||
const match = QUERY_INSTRUCTION_TEMPLATES.find(({ prefix }) =>
|
||||
normalizedModel.startsWith(prefix),
|
||||
);
|
||||
return match ? match.template.replace("{query}", () => queryText) : queryText;
|
||||
}
|
||||
|
||||
function resolveConfiguredProvider(options: OllamaEmbeddingOptions) {
|
||||
const providers = options.config.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
const providerId = options.provider?.trim() || "ollama";
|
||||
const direct = providers[providerId];
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
for (const [candidateId, candidate] of Object.entries(providers)) {
|
||||
if (normalizeProviderId(candidateId) === normalized) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return providers.ollama;
|
||||
}
|
||||
|
||||
function resolveMemorySecretInputString(params: {
|
||||
value: unknown;
|
||||
path: string;
|
||||
}): string | undefined {
|
||||
if (!hasConfiguredSecretInput(params.value)) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeResolvedSecretInputString({
|
||||
value: params.value,
|
||||
path: params.path,
|
||||
});
|
||||
}
|
||||
|
||||
type OllamaEmbeddingBaseUrlOrigin = "remote-config" | "provider-config" | "default";
|
||||
type OllamaEmbeddingSourceResolution = "unset" | "opt-out" | { apiKey: string };
|
||||
|
||||
type OllamaEmbeddingResolvedKeys = {
|
||||
remote: OllamaEmbeddingSourceResolution;
|
||||
provider: OllamaEmbeddingSourceResolution;
|
||||
env: string | undefined;
|
||||
};
|
||||
|
||||
function resolveSourcedOllamaEmbeddingKey(params: {
|
||||
configString: string | undefined;
|
||||
declared: boolean;
|
||||
}): OllamaEmbeddingSourceResolution {
|
||||
if (params.configString !== undefined) {
|
||||
if (!isNonSecretApiKeyMarker(params.configString)) {
|
||||
return { apiKey: params.configString };
|
||||
}
|
||||
if (!isKnownEnvApiKeyMarker(params.configString)) {
|
||||
return "opt-out";
|
||||
}
|
||||
const envKey = resolveEnvApiKey("ollama")?.apiKey;
|
||||
return envKey && !isNonSecretApiKeyMarker(envKey) ? { apiKey: envKey } : "opt-out";
|
||||
}
|
||||
if (params.declared) {
|
||||
const envKey = resolveEnvApiKey("ollama")?.apiKey;
|
||||
return envKey && !isNonSecretApiKeyMarker(envKey) ? { apiKey: envKey } : "opt-out";
|
||||
}
|
||||
return "unset";
|
||||
}
|
||||
|
||||
function resolveOllamaEmbeddingResolvedKeys(
|
||||
options: OllamaEmbeddingOptions,
|
||||
providerConfig: ReturnType<typeof resolveConfiguredProvider>,
|
||||
): OllamaEmbeddingResolvedKeys {
|
||||
const remoteValue = options.remote?.apiKey;
|
||||
const remote = resolveSourcedOllamaEmbeddingKey({
|
||||
configString: resolveMemorySecretInputString({
|
||||
value: remoteValue,
|
||||
path: "agents.*.memorySearch.remote.apiKey",
|
||||
}),
|
||||
declared: hasConfiguredSecretInput(remoteValue),
|
||||
});
|
||||
const providerValue = providerConfig?.apiKey;
|
||||
const provider = resolveSourcedOllamaEmbeddingKey({
|
||||
configString: normalizeOptionalSecretInput(providerValue),
|
||||
declared: hasConfiguredSecretInput(providerValue),
|
||||
});
|
||||
const envKey = resolveEnvApiKey("ollama")?.apiKey;
|
||||
const env = envKey && !isNonSecretApiKeyMarker(envKey) ? envKey : undefined;
|
||||
return { remote, provider, env };
|
||||
}
|
||||
|
||||
function resolveOllamaEmbeddingBaseUrl(params: {
|
||||
remoteBaseUrl?: string;
|
||||
providerConfig: ReturnType<typeof resolveConfiguredProvider>;
|
||||
}): { baseUrl: string; origin: OllamaEmbeddingBaseUrlOrigin } {
|
||||
const remoteBaseUrl = params.remoteBaseUrl?.trim();
|
||||
if (remoteBaseUrl) {
|
||||
return { baseUrl: resolveOllamaApiBase(remoteBaseUrl), origin: "remote-config" };
|
||||
}
|
||||
const providerBaseUrl = readProviderBaseUrl(params.providerConfig);
|
||||
if (providerBaseUrl) {
|
||||
return { baseUrl: resolveOllamaApiBase(providerBaseUrl), origin: "provider-config" };
|
||||
}
|
||||
return { baseUrl: resolveOllamaApiBase(undefined), origin: "default" };
|
||||
}
|
||||
|
||||
function normalizeOllamaHostKey(baseUrl: string): string | undefined {
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
let hostname = parsed.hostname.toLowerCase();
|
||||
if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
||||
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
||||
return `${parsed.protocol}//${hostname}:${port}${path}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function areOllamaHostsEquivalent(a: string, b: string): boolean {
|
||||
const aKey = normalizeOllamaHostKey(a);
|
||||
const bKey = normalizeOllamaHostKey(b);
|
||||
return aKey !== undefined && bKey !== undefined && aKey === bKey;
|
||||
}
|
||||
|
||||
function isOllamaCloudBaseUrl(baseUrl: string): boolean {
|
||||
return areOllamaHostsEquivalent(baseUrl, OLLAMA_CLOUD_BASE_URL);
|
||||
}
|
||||
|
||||
function selectOllamaEmbeddingApiKey(params: {
|
||||
resolved: OllamaEmbeddingResolvedKeys;
|
||||
baseUrl: string;
|
||||
baseUrlOrigin: OllamaEmbeddingBaseUrlOrigin;
|
||||
providerOwnedHost: string;
|
||||
}): string | undefined {
|
||||
if (params.resolved.remote !== "unset") {
|
||||
return typeof params.resolved.remote === "object" ? params.resolved.remote.apiKey : undefined;
|
||||
}
|
||||
const reachesProviderHost =
|
||||
params.baseUrlOrigin === "provider-config" ||
|
||||
params.baseUrlOrigin === "default" ||
|
||||
areOllamaHostsEquivalent(params.baseUrl, params.providerOwnedHost);
|
||||
if (params.resolved.provider !== "unset" && reachesProviderHost) {
|
||||
return typeof params.resolved.provider === "object"
|
||||
? params.resolved.provider.apiKey
|
||||
: undefined;
|
||||
}
|
||||
if (params.resolved.env && isOllamaCloudBaseUrl(params.baseUrl)) {
|
||||
return params.resolved.env;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveOllamaEmbeddingClient(
|
||||
options: OllamaEmbeddingOptions,
|
||||
): OllamaEmbeddingClientConfig {
|
||||
const providerConfig = resolveConfiguredProvider(options);
|
||||
const { baseUrl, origin: baseUrlOrigin } = resolveOllamaEmbeddingBaseUrl({
|
||||
remoteBaseUrl: options.remote?.baseUrl,
|
||||
providerConfig,
|
||||
});
|
||||
const model = normalizeEmbeddingModel(options.model, options.provider);
|
||||
const headerOverrides = Object.assign({}, providerConfig?.headers, options.remote?.headers);
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...headerOverrides,
|
||||
};
|
||||
const apiKey = selectOllamaEmbeddingApiKey({
|
||||
resolved: resolveOllamaEmbeddingResolvedKeys(options, providerConfig),
|
||||
baseUrl,
|
||||
baseUrlOrigin,
|
||||
providerOwnedHost: resolveOllamaApiBase(readProviderBaseUrl(providerConfig)),
|
||||
});
|
||||
if (apiKey) {
|
||||
headers.Authorization = `Bearer ${apiKey}`;
|
||||
}
|
||||
return {
|
||||
baseUrl,
|
||||
headers,
|
||||
ssrfPolicy: ssrfPolicyFromHttpBaseUrlAllowedOrigin(baseUrl),
|
||||
model,
|
||||
outputDimensionality: options.outputDimensionality,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createOllamaEmbeddingProvider(
|
||||
options: OllamaEmbeddingOptions,
|
||||
): Promise<{ provider: OllamaEmbeddingProvider; client: OllamaEmbeddingClient }> {
|
||||
const client = resolveOllamaEmbeddingClient(options);
|
||||
const embedUrl = `${client.baseUrl.replace(/\/$/, "")}/api/embed`;
|
||||
|
||||
const embedMany = async (input: string | string[], signal?: AbortSignal): Promise<number[][]> => {
|
||||
const json = await withRemoteHttpResponse({
|
||||
url: embedUrl,
|
||||
ssrfPolicy: client.ssrfPolicy,
|
||||
configuredLocalOriginBaseUrl: client.baseUrl,
|
||||
signal,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: client.headers,
|
||||
body: JSON.stringify({ model: client.model, input }),
|
||||
},
|
||||
onResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const detail = await readResponseTextLimited(
|
||||
response,
|
||||
OLLAMA_EMBED_ERROR_BODY_LIMIT_BYTES,
|
||||
).catch(() => "unknown error");
|
||||
throw new Error(`Ollama embed HTTP ${response.status}: ${detail}`);
|
||||
}
|
||||
return await readOllamaEmbeddingJsonResponse(response);
|
||||
},
|
||||
});
|
||||
if (!Array.isArray(json.embeddings)) {
|
||||
throw new Error("Ollama embed response missing embeddings[]");
|
||||
}
|
||||
const expectedCount = Array.isArray(input) ? input.length : 1;
|
||||
if (json.embeddings.length !== expectedCount) {
|
||||
throw new Error(
|
||||
`Ollama embed response returned ${json.embeddings.length} embeddings for ${expectedCount} inputs`,
|
||||
);
|
||||
}
|
||||
return json.embeddings.map((embedding) => {
|
||||
if (!Array.isArray(embedding)) {
|
||||
throw new Error("Ollama embed response contains a non-array embedding");
|
||||
}
|
||||
return sanitizeAndNormalizeEmbedding(embedding, client.outputDimensionality);
|
||||
});
|
||||
};
|
||||
|
||||
const embedOne = async (text: string, signal?: AbortSignal): Promise<number[]> => {
|
||||
const [embedding] = await embedMany(text, signal);
|
||||
if (!embedding) {
|
||||
throw new Error("Ollama embed response returned no embedding");
|
||||
}
|
||||
return embedding;
|
||||
};
|
||||
|
||||
const embedQuery = async (
|
||||
text: string,
|
||||
optionsValue?: { signal?: AbortSignal },
|
||||
): Promise<number[]> =>
|
||||
await embedOne(applyQueryInstructionTemplate(client.model, text), optionsValue?.signal);
|
||||
|
||||
const provider: OllamaEmbeddingProvider = {
|
||||
id: "ollama",
|
||||
model: client.model,
|
||||
embedQuery,
|
||||
embedBatch: async (texts, optionsLocal) =>
|
||||
texts.length === 0 ? [] : await embedMany(texts, optionsLocal?.signal),
|
||||
};
|
||||
|
||||
return {
|
||||
provider,
|
||||
client: {
|
||||
...client,
|
||||
embedBatch: async (texts) => {
|
||||
try {
|
||||
return await provider.embedBatch(texts);
|
||||
} catch (err) {
|
||||
throw new Error(formatErrorMessage(err), { cause: err });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user