@open-mercato/search 0.6.6 → 0.6.7-develop.6553.1.2ec7246e04

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -208,7 +208,10 @@ async function POST(req) {
208
208
  }
209
209
  const probe = resolveProbe(container);
210
210
  if (probe) {
211
- const availability = await probe.checkAvailability(newConfig.providerId);
211
+ const availability = await probe.checkAvailability(
212
+ newConfig.providerId,
213
+ newConfig.providerId === "ollama" ? { force: true, baseUrl: newConfig.baseUrl } : void 0
214
+ );
212
215
  if (!availability.available) {
213
216
  return NextResponse.json(
214
217
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/search/api/embeddings/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { envDisablesAutoIndexing, resolveAutoIndexingEnabled, SEARCH_AUTO_INDEX_CONFIG_KEY } from '../../lib/auto-indexing'\nimport {\n resolveEmbeddingConfig,\n resolveEmbeddingConfigResult,\n saveEmbeddingConfig,\n getConfiguredProviders,\n detectConfigChange,\n getEffectiveDimension,\n} from '../../lib/embedding-config'\nimport type { EmbeddingConfigSource } from '../../lib/embedding-config'\nimport { checkAllProviders } from '../../lib/provider-probe'\nimport type { EmbeddingProviderProbe, ProviderAvailabilityEntry } from '../../lib/provider-probe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EmbeddingProviderConfig, EmbeddingProviderId, VectorDriver } from '../../../../vector'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG, EmbeddingService } from '../../../../vector'\nimport {\n assertSafeOllamaBaseUrl,\n UnsafeOllamaBaseUrlError,\n} from '../../../../vector/lib/ollama-url-safety'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../lib/debug'\nimport { embeddingsOpenApi } from '../openapi'\n\nconst embeddingConfigSchema = z.object({\n providerId: z.enum(['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']),\n model: z.string(),\n dimension: z.number(),\n outputDimensionality: z.number().optional(),\n baseUrl: z.string().optional(),\n})\n\nconst updateSchema = z.object({\n autoIndexingEnabled: z.boolean().optional(),\n embeddingConfig: embeddingConfigSchema.optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.embeddings.view'] },\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\ntype SettingsResponse = {\n settings: {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n embeddingConfigSource: EmbeddingConfigSource\n configuredProviders: EmbeddingProviderId[]\n providerAvailability: ProviderAvailabilityEntry[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n }\n}\n\nconst openAiConfigured = () => Boolean(process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim().length > 0)\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nconst configUnavailable = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('search.api.errors.configUnavailable', 'Configuration service unavailable') }, { status: 503 })\n}\n\nfunction resolveProbe(container: { resolve: <T = unknown>(name: string) => T }): EmbeddingProviderProbe | null {\n try {\n return container.resolve<EmbeddingProviderProbe>('embeddingProviderProbe')\n } catch {\n return null\n }\n}\n\nasync function resolveProviderAvailability(\n container: { resolve: <T = unknown>(name: string) => T },\n): Promise<ProviderAvailabilityEntry[]> {\n const probe = resolveProbe(container)\n if (!probe) return []\n try {\n return await checkAllProviders(probe)\n } catch {\n return []\n }\n}\n\nasync function getIndexedDimension(container: { resolve: <T = unknown>(name: string) => T }): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.getTableDimension) {\n return await pgvectorDriver.getTableDimension()\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getVectorDocumentCount(\n container: { resolve: <T = unknown>(name: string) => T },\n tenantId: string,\n organizationId?: string | null,\n): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.count) {\n return await pgvectorDriver.count({ tenantId, organizationId: organizationId ?? undefined })\n }\n return null\n } catch {\n return null\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n const { config: embeddingConfig, source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n const configuredProviders = getConfiguredProviders()\n const providerAvailability = await resolveProviderAvailability(container)\n const indexedDimension = await getIndexedDimension(container)\n\n const effectiveDimension = embeddingConfig\n ? getEffectiveDimension(embeddingConfig)\n : DEFAULT_EMBEDDING_CONFIG.dimension\n\n const reindexRequired = Boolean(\n indexedDimension &&\n embeddingConfig &&\n indexedDimension !== effectiveDimension\n )\n\n // Get document count for vector index\n const documentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders,\n providerAvailability,\n indexedDimension,\n reindexRequired,\n documentCount,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: t('api.errors.invalidJson', 'Invalid JSON payload.') }, { status: 400 })\n }\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: t('api.errors.invalidPayload', 'Invalid payload.') }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n return await configUnavailable()\n }\n\n if (parsed.data.autoIndexingEnabled !== undefined) {\n if (envDisablesAutoIndexing()) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.autoIndexingDisabled',\n 'Auto-indexing is disabled via OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING (legacy alias: DISABLE_VECTOR_SEARCH_AUTOINDEXING).',\n ),\n },\n { status: 409 },\n )\n }\n await service.setValue('vector', SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled, { tenantId: auth.tenantId })\n }\n\n let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null, scope: { tenantId: auth.tenantId } })\n let reindexRequired = false\n let indexedDimension = await getIndexedDimension(container)\n\n if (parsed.data.embeddingConfig) {\n const newConfig = parsed.data.embeddingConfig\n const providerInfo = EMBEDDING_PROVIDERS[newConfig.providerId]\n\n if (!providerInfo) {\n return NextResponse.json(\n { error: t('search.api.errors.invalidProvider', 'Invalid embedding provider.') },\n { status: 400 },\n )\n }\n\n const configuredProviders = getConfiguredProviders()\n if (!configuredProviders.includes(newConfig.providerId)) {\n return NextResponse.json(\n { error: t('search.api.errors.providerNotConfigured', `Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`) },\n { status: 400 },\n )\n }\n\n // Reject an unsafe user-supplied Ollama base URL before doing anything with it (SSRF guard).\n if (newConfig.providerId === 'ollama' && newConfig.baseUrl != null) {\n try {\n assertSafeOllamaBaseUrl(newConfig.baseUrl)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.invalidOllamaBaseUrl',\n 'Ollama base URL is not allowed. Set OLLAMA_BASE_URL in the environment, or add the host to OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.',\n ),\n reason: err.reason,\n },\n { status: 400 },\n )\n }\n throw err\n }\n }\n\n // Save-time availability guard: never persist a provider the probe reports unreachable.\n const probe = resolveProbe(container)\n if (probe) {\n const availability = await probe.checkAvailability(newConfig.providerId)\n if (!availability.available) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.providerUnavailable',\n `Provider ${providerInfo.name} is not available: ${availability.reason ?? 'unreachable'}`,\n ),\n reason: availability.reason ?? null,\n },\n { status: 409 },\n )\n }\n }\n\n const change = detectConfigChange(\n embeddingConfig,\n {\n ...newConfig,\n updatedAt: new Date().toISOString(),\n },\n indexedDimension\n )\n\n if (change.requiresReindex) {\n const newDimension = getEffectiveDimension(change.newConfig)\n searchDebug('search.embeddings.update', 'config change detected, recreating table', {\n requiresReindex: change.requiresReindex,\n reason: change.reason,\n oldDimension: indexedDimension,\n newDimension,\n })\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.recreateWithDimension) {\n await pgvectorDriver.recreateWithDimension(newDimension)\n // Query the actual dimension from the database to confirm\n if (pgvectorDriver.getTableDimension) {\n indexedDimension = await pgvectorDriver.getTableDimension()\n } else {\n indexedDimension = newDimension\n }\n searchDebug('search.embeddings.update', 'table recreated successfully', { indexedDimension })\n } else {\n searchDebugWarn('search.embeddings.update', 'pgvector driver does not have recreateWithDimension method')\n }\n } catch (error) {\n searchError('search.embeddings.update', 'failed to recreate table', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json(\n { error: t('search.api.errors.recreateFailed', 'Failed to recreate vector table with new dimension.') },\n { status: 500 },\n )\n }\n }\n\n await saveEmbeddingConfig(container, change.newConfig, { scope: { tenantId: auth.tenantId } })\n embeddingConfig = change.newConfig\n\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n } catch {\n // Embedding service may not be available in all contexts\n }\n\n reindexRequired = change.requiresReindex\n }\n\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n // Get updated document count\n const updatedDocumentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n const { source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders: getConfiguredProviders(),\n providerAvailability: await resolveProviderAvailability(container),\n indexedDimension,\n reindexRequired,\n documentCount: updatedDocumentCount,\n },\n })\n } catch (error) {\n searchError('search.embeddings.update', 'failed', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json({ error: t('search.api.errors.updateFailed', 'Failed to update embedding settings.') }, { status: 500 })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsOpenApi\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { envDisablesAutoIndexing, resolveAutoIndexingEnabled, SEARCH_AUTO_INDEX_CONFIG_KEY } from '../../lib/auto-indexing'\nimport {\n resolveEmbeddingConfig,\n resolveEmbeddingConfigResult,\n saveEmbeddingConfig,\n getConfiguredProviders,\n detectConfigChange,\n getEffectiveDimension,\n} from '../../lib/embedding-config'\nimport type { EmbeddingConfigSource } from '../../lib/embedding-config'\nimport { checkAllProviders } from '../../lib/provider-probe'\nimport type { EmbeddingProviderProbe, ProviderAvailabilityEntry } from '../../lib/provider-probe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EmbeddingProviderConfig, EmbeddingProviderId, VectorDriver } from '../../../../vector'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG, EmbeddingService } from '../../../../vector'\nimport {\n assertSafeOllamaBaseUrl,\n UnsafeOllamaBaseUrlError,\n} from '../../../../vector/lib/ollama-url-safety'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../lib/debug'\nimport { embeddingsOpenApi } from '../openapi'\n\nconst embeddingConfigSchema = z.object({\n providerId: z.enum(['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']),\n model: z.string(),\n dimension: z.number(),\n outputDimensionality: z.number().optional(),\n baseUrl: z.string().optional(),\n})\n\nconst updateSchema = z.object({\n autoIndexingEnabled: z.boolean().optional(),\n embeddingConfig: embeddingConfigSchema.optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.embeddings.view'] },\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\ntype SettingsResponse = {\n settings: {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n embeddingConfigSource: EmbeddingConfigSource\n configuredProviders: EmbeddingProviderId[]\n providerAvailability: ProviderAvailabilityEntry[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n }\n}\n\nconst openAiConfigured = () => Boolean(process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim().length > 0)\n\nconst toJson = (payload: SettingsResponse, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nconst configUnavailable = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('search.api.errors.configUnavailable', 'Configuration service unavailable') }, { status: 503 })\n}\n\nfunction resolveProbe(container: { resolve: <T = unknown>(name: string) => T }): EmbeddingProviderProbe | null {\n try {\n return container.resolve<EmbeddingProviderProbe>('embeddingProviderProbe')\n } catch {\n return null\n }\n}\n\nasync function resolveProviderAvailability(\n container: { resolve: <T = unknown>(name: string) => T },\n): Promise<ProviderAvailabilityEntry[]> {\n const probe = resolveProbe(container)\n if (!probe) return []\n try {\n return await checkAllProviders(probe)\n } catch {\n return []\n }\n}\n\nasync function getIndexedDimension(container: { resolve: <T = unknown>(name: string) => T }): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.getTableDimension) {\n return await pgvectorDriver.getTableDimension()\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getVectorDocumentCount(\n container: { resolve: <T = unknown>(name: string) => T },\n tenantId: string,\n organizationId?: string | null,\n): Promise<number | null> {\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.count) {\n return await pgvectorDriver.count({ tenantId, organizationId: organizationId ?? undefined })\n }\n return null\n } catch {\n return null\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n const container = await createRequestContainer()\n try {\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n const { config: embeddingConfig, source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n const configuredProviders = getConfiguredProviders()\n const providerAvailability = await resolveProviderAvailability(container)\n const indexedDimension = await getIndexedDimension(container)\n\n const effectiveDimension = embeddingConfig\n ? getEffectiveDimension(embeddingConfig)\n : DEFAULT_EMBEDDING_CONFIG.dimension\n\n const reindexRequired = Boolean(\n indexedDimension &&\n embeddingConfig &&\n indexedDimension !== effectiveDimension\n )\n\n // Get document count for vector index\n const documentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders,\n providerAvailability,\n indexedDimension,\n reindexRequired,\n documentCount,\n },\n })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) return await unauthorized()\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: t('api.errors.invalidJson', 'Invalid JSON payload.') }, { status: 400 })\n }\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: t('api.errors.invalidPayload', 'Invalid payload.') }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n return await configUnavailable()\n }\n\n if (parsed.data.autoIndexingEnabled !== undefined) {\n if (envDisablesAutoIndexing()) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.autoIndexingDisabled',\n 'Auto-indexing is disabled via OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING (legacy alias: DISABLE_VECTOR_SEARCH_AUTOINDEXING).',\n ),\n },\n { status: 409 },\n )\n }\n await service.setValue('vector', SEARCH_AUTO_INDEX_CONFIG_KEY, parsed.data.autoIndexingEnabled, { tenantId: auth.tenantId })\n }\n\n let embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null, scope: { tenantId: auth.tenantId } })\n let reindexRequired = false\n let indexedDimension = await getIndexedDimension(container)\n\n if (parsed.data.embeddingConfig) {\n const newConfig = parsed.data.embeddingConfig\n const providerInfo = EMBEDDING_PROVIDERS[newConfig.providerId]\n\n if (!providerInfo) {\n return NextResponse.json(\n { error: t('search.api.errors.invalidProvider', 'Invalid embedding provider.') },\n { status: 400 },\n )\n }\n\n const configuredProviders = getConfiguredProviders()\n if (!configuredProviders.includes(newConfig.providerId)) {\n return NextResponse.json(\n { error: t('search.api.errors.providerNotConfigured', `Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`) },\n { status: 400 },\n )\n }\n\n // Reject an unsafe user-supplied Ollama base URL before doing anything with it (SSRF guard).\n if (newConfig.providerId === 'ollama' && newConfig.baseUrl != null) {\n try {\n assertSafeOllamaBaseUrl(newConfig.baseUrl)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.invalidOllamaBaseUrl',\n 'Ollama base URL is not allowed. Set OLLAMA_BASE_URL in the environment, or add the host to OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.',\n ),\n reason: err.reason,\n },\n { status: 400 },\n )\n }\n throw err\n }\n }\n\n // Save-time availability guard: never persist a provider the probe reports unreachable.\n const probe = resolveProbe(container)\n if (probe) {\n const availability = await probe.checkAvailability(\n newConfig.providerId,\n newConfig.providerId === 'ollama'\n ? { force: true, baseUrl: newConfig.baseUrl }\n : undefined,\n )\n if (!availability.available) {\n return NextResponse.json(\n {\n error: t(\n 'search.api.errors.providerUnavailable',\n `Provider ${providerInfo.name} is not available: ${availability.reason ?? 'unreachable'}`,\n ),\n reason: availability.reason ?? null,\n },\n { status: 409 },\n )\n }\n }\n\n const change = detectConfigChange(\n embeddingConfig,\n {\n ...newConfig,\n updatedAt: new Date().toISOString(),\n },\n indexedDimension\n )\n\n if (change.requiresReindex) {\n const newDimension = getEffectiveDimension(change.newConfig)\n searchDebug('search.embeddings.update', 'config change detected, recreating table', {\n requiresReindex: change.requiresReindex,\n reason: change.reason,\n oldDimension: indexedDimension,\n newDimension,\n })\n try {\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const pgvectorDriver = drivers.find((d) => d.id === 'pgvector')\n if (pgvectorDriver?.recreateWithDimension) {\n await pgvectorDriver.recreateWithDimension(newDimension)\n // Query the actual dimension from the database to confirm\n if (pgvectorDriver.getTableDimension) {\n indexedDimension = await pgvectorDriver.getTableDimension()\n } else {\n indexedDimension = newDimension\n }\n searchDebug('search.embeddings.update', 'table recreated successfully', { indexedDimension })\n } else {\n searchDebugWarn('search.embeddings.update', 'pgvector driver does not have recreateWithDimension method')\n }\n } catch (error) {\n searchError('search.embeddings.update', 'failed to recreate table', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json(\n { error: t('search.api.errors.recreateFailed', 'Failed to recreate vector table with new dimension.') },\n { status: 500 },\n )\n }\n }\n\n await saveEmbeddingConfig(container, change.newConfig, { scope: { tenantId: auth.tenantId } })\n embeddingConfig = change.newConfig\n\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n } catch {\n // Embedding service may not be available in all contexts\n }\n\n reindexRequired = change.requiresReindex\n }\n\n const lockedByEnv = envDisablesAutoIndexing()\n let autoIndexingEnabled = !lockedByEnv\n if (!lockedByEnv) {\n try {\n autoIndexingEnabled = await resolveAutoIndexingEnabled(container, { defaultValue: true, scope: { tenantId: auth.tenantId } })\n } catch {\n autoIndexingEnabled = true\n }\n }\n\n // Get updated document count\n const updatedDocumentCount = auth.tenantId\n ? await getVectorDocumentCount(container, auth.tenantId, auth.orgId)\n : null\n\n const { source: embeddingConfigSource } = await resolveEmbeddingConfigResult(container, {\n scope: { tenantId: auth.tenantId },\n })\n\n return toJson({\n settings: {\n openaiConfigured: openAiConfigured(),\n autoIndexingEnabled: lockedByEnv ? false : autoIndexingEnabled,\n autoIndexingLocked: lockedByEnv,\n lockReason: lockedByEnv ? 'env' : null,\n embeddingConfig,\n embeddingConfigSource,\n configuredProviders: getConfiguredProviders(),\n providerAvailability: await resolveProviderAvailability(container),\n indexedDimension,\n reindexRequired,\n documentCount: updatedDocumentCount,\n },\n })\n } catch (error) {\n searchError('search.embeddings.update', 'failed', {\n error: error instanceof Error ? error.message : error,\n })\n return NextResponse.json({ error: t('search.api.errors.updateFailed', 'Failed to update embedding settings.') }, { status: 500 })\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsOpenApi\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAEnC,SAAS,yBAAyB,4BAA4B,oCAAoC;AAClG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AAElC,SAAS,2BAA2B;AAEpC,SAAS,qBAAqB,gCAAkD;AAChF;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa,iBAAiB,mBAAmB;AAC1D,SAAS,yBAAyB;AAElC,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,YAAY,EAAE,KAAK,CAAC,UAAU,UAAU,WAAW,UAAU,WAAW,QAAQ,CAAC;AAAA,EACjF,OAAO,EAAE,OAAO;AAAA,EAChB,WAAW,EAAE,OAAO;AAAA,EACpB,sBAAsB,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1C,SAAS,EAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,qBAAqB,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC1C,iBAAiB,sBAAsB,SAAS;AAClD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,wBAAwB,EAAE;AAAA,EACtE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;AAkBA,MAAM,mBAAmB,MAAM,QAAQ,QAAQ,IAAI,kBAAkB,QAAQ,IAAI,eAAe,KAAK,EAAE,SAAS,CAAC;AAEjH,MAAM,SAAS,CAAC,SAA2B,SAAwB,aAAa,KAAK,SAAS,IAAI;AAElG,MAAM,eAAe,YAAY;AAC/B,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,aAAa,KAAK,EAAE,OAAO,EAAE,2BAA2B,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AACnG;AAEA,MAAM,oBAAoB,YAAY;AACpC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,aAAa,KAAK,EAAE,OAAO,EAAE,uCAAuC,mCAAmC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AACpI;AAEA,SAAS,aAAa,WAAyF;AAC7G,MAAI;AACF,WAAO,UAAU,QAAgC,wBAAwB;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,4BACb,WACsC;AACtC,QAAM,QAAQ,aAAa,SAAS;AACpC,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI;AACF,WAAO,MAAM,kBAAkB,KAAK;AAAA,EACtC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAe,oBAAoB,WAAkF;AACnH,MAAI;AACF,UAAM,UAAU,UAAU,QAAwB,eAAe;AACjE,UAAM,iBAAiB,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAC9D,QAAI,gBAAgB,mBAAmB;AACrC,aAAO,MAAM,eAAe,kBAAkB;AAAA,IAChD;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,uBACb,WACA,UACA,gBACwB;AACxB,MAAI;AACF,UAAM,UAAU,UAAU,QAAwB,eAAe;AACjE,UAAM,iBAAiB,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAC9D,QAAI,gBAAgB,OAAO;AACzB,aAAO,MAAM,eAAe,MAAM,EAAE,UAAU,gBAAgB,kBAAkB,OAAU,CAAC;AAAA,IAC7F;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,IAAK,QAAO,MAAM,aAAa;AAE1C,QAAM,YAAY,MAAM,uBAAuB;AAC/C,MAAI;AACF,UAAM,cAAc,wBAAwB;AAC5C,QAAI,sBAAsB,CAAC;AAC3B,QAAI,CAAC,aAAa;AAChB,UAAI;AACF,8BAAsB,MAAM,2BAA2B,WAAW,EAAE,cAAc,MAAM,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;AAAA,MAC9H,QAAQ;AACN,8BAAsB;AAAA,MACxB;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,iBAAiB,QAAQ,sBAAsB,IAAI,MAAM,6BAA6B,WAAW;AAAA,MAC/G,OAAO,EAAE,UAAU,KAAK,SAAS;AAAA,IACnC,CAAC;AACD,UAAM,sBAAsB,uBAAuB;AACnD,UAAM,uBAAuB,MAAM,4BAA4B,SAAS;AACxE,UAAM,mBAAmB,MAAM,oBAAoB,SAAS;AAE5D,UAAM,qBAAqB,kBACvB,sBAAsB,eAAe,IACrC,yBAAyB;AAE7B,UAAM,kBAAkB;AAAA,MACtB,oBACA,mBACA,qBAAqB;AAAA,IACvB;AAGA,UAAM,gBAAgB,KAAK,WACvB,MAAM,uBAAuB,WAAW,KAAK,UAAU,KAAK,KAAK,IACjE;AAEJ,WAAO,OAAO;AAAA,MACZ,UAAU;AAAA,QACR,kBAAkB,iBAAiB;AAAA,QACnC,qBAAqB,cAAc,QAAQ;AAAA,QAC3C,oBAAoB;AAAA,QACpB,YAAY,cAAc,QAAQ;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,UAAM,aAAa;AACnB,QAAI,OAAO,WAAW,YAAY,YAAY;AAC5C,YAAM,WAAW,QAAQ;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,IAAK,QAAO,MAAM,aAAa;AAE1C,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,0BAA0B,uBAAuB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3G;AACA,QAAM,SAAS,aAAa,UAAU,IAAI;AAC1C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,6BAA6B,kBAAkB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,MAAI;AACF,QAAI;AACJ,QAAI;AACF,gBAAW,UAAU,QAAQ,qBAAqB;AAAA,IACpD,QAAQ;AACN,aAAO,MAAM,kBAAkB;AAAA,IACjC;AAEA,QAAI,OAAO,KAAK,wBAAwB,QAAW;AACjD,UAAI,wBAAwB,GAAG;AAC7B,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO;AAAA,cACL;AAAA,cACA;AAAA,YACF;AAAA,UACF;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AACA,YAAM,QAAQ,SAAS,UAAU,8BAA8B,OAAO,KAAK,qBAAqB,EAAE,UAAU,KAAK,SAAS,CAAC;AAAA,IAC7H;AAEA,QAAI,kBAAkB,MAAM,uBAAuB,WAAW,EAAE,cAAc,MAAM,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;AACxH,QAAI,kBAAkB;AACtB,QAAI,mBAAmB,MAAM,oBAAoB,SAAS;AAE1D,QAAI,OAAO,KAAK,iBAAiB;AAC/B,YAAM,YAAY,OAAO,KAAK;AAC9B,YAAM,eAAe,oBAAoB,UAAU,UAAU;AAE7D,UAAI,CAAC,cAAc;AACjB,eAAO,aAAa;AAAA,UAClB,EAAE,OAAO,EAAE,qCAAqC,6BAA6B,EAAE;AAAA,UAC/E,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAEA,YAAM,sBAAsB,uBAAuB;AACnD,UAAI,CAAC,oBAAoB,SAAS,UAAU,UAAU,GAAG;AACvD,eAAO,aAAa;AAAA,UAClB,EAAE,OAAO,EAAE,2CAA2C,YAAY,aAAa,IAAI,2BAA2B,aAAa,cAAc,wBAAwB,EAAE;AAAA,UACnK,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAGA,UAAI,UAAU,eAAe,YAAY,UAAU,WAAW,MAAM;AAClE,YAAI;AACF,kCAAwB,UAAU,OAAO;AAAA,QAC3C,SAAS,KAAK;AACZ,cAAI,eAAe,0BAA0B;AAC3C,mBAAO,aAAa;AAAA,cAClB;AAAA,gBACE,OAAO;AAAA,kBACL;AAAA,kBACA;AAAA,gBACF;AAAA,gBACA,QAAQ,IAAI;AAAA,cACd;AAAA,cACA,EAAE,QAAQ,IAAI;AAAA,YAChB;AAAA,UACF;AACA,gBAAM;AAAA,QACR;AAAA,MACF;AAGA,YAAM,QAAQ,aAAa,SAAS;AACpC,UAAI,OAAO;AACT,cAAM,eAAe,MAAM,MAAM;AAAA,UAC/B,UAAU;AAAA,UACV,UAAU,eAAe,WACrB,EAAE,OAAO,MAAM,SAAS,UAAU,QAAQ,IAC1C;AAAA,QACN;AACA,YAAI,CAAC,aAAa,WAAW;AAC3B,iBAAO,aAAa;AAAA,YAClB;AAAA,cACE,OAAO;AAAA,gBACL;AAAA,gBACA,YAAY,aAAa,IAAI,sBAAsB,aAAa,UAAU,aAAa;AAAA,cACzF;AAAA,cACA,QAAQ,aAAa,UAAU;AAAA,YACjC;AAAA,YACA,EAAE,QAAQ,IAAI;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,SAAS;AAAA,QACb;AAAA,QACA;AAAA,UACE,GAAG;AAAA,UACH,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC;AAAA,QACA;AAAA,MACF;AAEA,UAAI,OAAO,iBAAiB;AAC1B,cAAM,eAAe,sBAAsB,OAAO,SAAS;AAC3D,oBAAY,4BAA4B,4CAA4C;AAAA,UAClF,iBAAiB,OAAO;AAAA,UACxB,QAAQ,OAAO;AAAA,UACf,cAAc;AAAA,UACd;AAAA,QACF,CAAC;AACD,YAAI;AACF,gBAAM,UAAU,UAAU,QAAwB,eAAe;AACjE,gBAAM,iBAAiB,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU;AAC9D,cAAI,gBAAgB,uBAAuB;AACzC,kBAAM,eAAe,sBAAsB,YAAY;AAEvD,gBAAI,eAAe,mBAAmB;AACpC,iCAAmB,MAAM,eAAe,kBAAkB;AAAA,YAC5D,OAAO;AACL,iCAAmB;AAAA,YACrB;AACA,wBAAY,4BAA4B,gCAAgC,EAAE,iBAAiB,CAAC;AAAA,UAC9F,OAAO;AACL,4BAAgB,4BAA4B,4DAA4D;AAAA,UAC1G;AAAA,QACF,SAAS,OAAO;AACd,sBAAY,4BAA4B,4BAA4B;AAAA,YAClE,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UAClD,CAAC;AACD,iBAAO,aAAa;AAAA,YAClB,EAAE,OAAO,EAAE,oCAAoC,qDAAqD,EAAE;AAAA,YACtG,EAAE,QAAQ,IAAI;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,oBAAoB,WAAW,OAAO,WAAW,EAAE,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;AAC7F,wBAAkB,OAAO;AAEzB,UAAI;AACF,cAAM,mBAAmB,UAAU,QAA0B,wBAAwB;AACrF,yBAAiB,aAAa,eAAe;AAAA,MAC/C,QAAQ;AAAA,MAER;AAEA,wBAAkB,OAAO;AAAA,IAC3B;AAEA,UAAM,cAAc,wBAAwB;AAC5C,QAAI,sBAAsB,CAAC;AAC3B,QAAI,CAAC,aAAa;AAChB,UAAI;AACF,8BAAsB,MAAM,2BAA2B,WAAW,EAAE,cAAc,MAAM,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;AAAA,MAC9H,QAAQ;AACN,8BAAsB;AAAA,MACxB;AAAA,IACF;AAGA,UAAM,uBAAuB,KAAK,WAC9B,MAAM,uBAAuB,WAAW,KAAK,UAAU,KAAK,KAAK,IACjE;AAEJ,UAAM,EAAE,QAAQ,sBAAsB,IAAI,MAAM,6BAA6B,WAAW;AAAA,MACtF,OAAO,EAAE,UAAU,KAAK,SAAS;AAAA,IACnC,CAAC;AAED,WAAO,OAAO;AAAA,MACZ,UAAU;AAAA,QACR,kBAAkB,iBAAiB;AAAA,QACnC,qBAAqB,cAAc,QAAQ;AAAA,QAC3C,oBAAoB;AAAA,QACpB,YAAY,cAAc,QAAQ;AAAA,QAClC;AAAA,QACA;AAAA,QACA,qBAAqB,uBAAuB;AAAA,QAC5C,sBAAsB,MAAM,4BAA4B,SAAS;AAAA,QACjE;AAAA,QACA;AAAA,QACA,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,gBAAY,4BAA4B,UAAU;AAAA,MAChD,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,kCAAkC,sCAAsC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClI,UAAE;AACA,UAAM,aAAa;AACnB,QAAI,OAAO,WAAW,YAAY,YAAY;AAC5C,YAAM,WAAW,QAAQ;AAAA,IAC3B;AAAA,EACF;AACF;AAEO,MAAM,UAAU;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { EMBEDDING_PROVIDERS } from "../../../vector/index.js";
2
+ import { safeOllamaFetch } from "../../../vector/lib/ollama-url-safety.js";
2
3
  const CACHE_VERSION = "v1";
3
4
  const CACHE_TTL_MS = 3e4;
4
5
  const OLLAMA_PROBE_TIMEOUT_MS = 1500;
@@ -42,7 +43,7 @@ async function probeOllama(baseUrl) {
42
43
  const controller = new AbortController();
43
44
  const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS);
44
45
  try {
45
- const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`, {
46
+ const response = await safeOllamaFetch(`${baseUrl.replace(/\/$/, "")}/api/tags`, {
46
47
  method: "GET",
47
48
  signal: controller.signal
48
49
  });
@@ -66,10 +67,10 @@ async function probeOllama(baseUrl) {
66
67
  clearTimeout(timer);
67
68
  }
68
69
  }
69
- async function computeAvailability(providerId) {
70
+ async function computeAvailability(providerId, options) {
70
71
  try {
71
72
  if (providerId === "ollama") {
72
- return await probeOllama(ollamaBaseUrl());
73
+ return await probeOllama(options?.baseUrl?.trim() || ollamaBaseUrl());
73
74
  }
74
75
  return keyPresence(providerId);
75
76
  } catch (error) {
@@ -80,7 +81,8 @@ function createEmbeddingProviderProbe(container) {
80
81
  const checkAvailability = async (providerId, options) => {
81
82
  const cache = resolveCache(container);
82
83
  const key = cacheKey(providerId);
83
- if (!options?.force && cache) {
84
+ const probesCandidateUrl = providerId === "ollama" && options?.baseUrl !== void 0;
85
+ if (!options?.force && !probesCandidateUrl && cache) {
84
86
  try {
85
87
  const cached = await cache.get(key);
86
88
  if (cached && typeof cached === "object" && "available" in cached) {
@@ -89,8 +91,8 @@ function createEmbeddingProviderProbe(container) {
89
91
  } catch {
90
92
  }
91
93
  }
92
- const result = await computeAvailability(providerId);
93
- if (cache) {
94
+ const result = await computeAvailability(providerId, options);
95
+ if (cache && !probesCandidateUrl) {
94
96
  try {
95
97
  await cache.set(key, result, { ttl: CACHE_TTL_MS });
96
98
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/lib/provider-probe.ts"],
4
- "sourcesContent": ["import type { CacheStrategy } from '@open-mercato/cache'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EmbeddingProviderId } from '../../../vector'\nimport { EMBEDDING_PROVIDERS } from '../../../vector'\n\nconst CACHE_VERSION = 'v1'\nconst CACHE_TTL_MS = 30_000\nconst OLLAMA_PROBE_TIMEOUT_MS = 1500\n\nexport type ProviderAvailability = {\n available: boolean\n reason?: string\n models?: number\n}\n\nexport type ProviderAvailabilityEntry = ProviderAvailability & {\n providerId: EmbeddingProviderId\n}\n\nexport type EmbeddingProviderProbe = {\n checkAvailability(providerId: EmbeddingProviderId, options?: { force?: boolean }): Promise<ProviderAvailability>\n}\n\nconst ALL_PROVIDERS: EmbeddingProviderId[] = ['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']\n\nexport async function checkAllProviders(\n probe: EmbeddingProviderProbe,\n options?: { force?: boolean },\n): Promise<ProviderAvailabilityEntry[]> {\n return Promise.all(\n ALL_PROVIDERS.map(async (providerId) => ({\n providerId,\n ...(await probe.checkAvailability(providerId, options)),\n })),\n )\n}\n\nconst cacheKey = (providerId: string) => `embedding-provider-probe:${CACHE_VERSION}:${providerId}`\n\nconst resolveCache = (container: AppContainer): CacheStrategy | null => {\n try {\n return container.resolve('cache') as CacheStrategy\n } catch {\n return null\n }\n}\n\nconst ollamaBaseUrl = () => process.env.OLLAMA_BASE_URL?.trim() || 'http://localhost:11434'\n\nconst keyPresence = (providerId: EmbeddingProviderId): ProviderAvailability => {\n const info = EMBEDDING_PROVIDERS[providerId]\n const envKey = info?.envKeyRequired\n switch (providerId) {\n case 'openai':\n return process.env.OPENAI_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'google':\n return process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'mistral':\n return process.env.MISTRAL_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'cohere':\n return process.env.COHERE_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'bedrock':\n return process.env.AWS_ACCESS_KEY_ID?.trim() && process.env.AWS_SECRET_ACCESS_KEY?.trim()\n ? { available: true }\n : { available: false, reason: 'Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to enable Bedrock' }\n default:\n return { available: false, reason: `Unknown provider: ${providerId}` }\n }\n}\n\nexport async function probeOllama(baseUrl: string): Promise<ProviderAvailability> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS)\n try {\n const response = await fetch(`${baseUrl.replace(/\\/$/, '')}/api/tags`, {\n method: 'GET',\n signal: controller.signal,\n })\n if (!response.ok) {\n return { available: false, reason: `Ollama responded ${response.status} at ${baseUrl}` }\n }\n let models: number | undefined\n try {\n const payload = (await response.json()) as { models?: unknown[] }\n if (Array.isArray(payload?.models)) models = payload.models.length\n } catch {}\n return { available: true, models }\n } catch (error) {\n const aborted = error instanceof Error && error.name === 'AbortError'\n return {\n available: false,\n reason: aborted ? `Ollama not reachable at ${baseUrl} (timed out)` : `Ollama not reachable at ${baseUrl}`,\n }\n } finally {\n clearTimeout(timer)\n }\n}\n\nasync function computeAvailability(providerId: EmbeddingProviderId): Promise<ProviderAvailability> {\n try {\n if (providerId === 'ollama') {\n return await probeOllama(ollamaBaseUrl())\n }\n return keyPresence(providerId)\n } catch (error) {\n // Fail closed: any unexpected error means the provider is treated as unavailable.\n return { available: false, reason: error instanceof Error ? error.message : 'Availability check failed' }\n }\n}\n\nexport function createEmbeddingProviderProbe(container: AppContainer): EmbeddingProviderProbe {\n const checkAvailability = async (\n providerId: EmbeddingProviderId,\n options?: { force?: boolean },\n ): Promise<ProviderAvailability> => {\n const cache = resolveCache(container)\n const key = cacheKey(providerId)\n if (!options?.force && cache) {\n try {\n const cached = await cache.get(key)\n if (cached && typeof cached === 'object' && 'available' in cached) {\n return cached as ProviderAvailability\n }\n } catch {}\n }\n const result = await computeAvailability(providerId)\n if (cache) {\n try {\n await cache.set(key, result, { ttl: CACHE_TTL_MS })\n } catch {}\n }\n return result\n }\n return { checkAvailability }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CacheStrategy } from '@open-mercato/cache'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EmbeddingProviderId } from '../../../vector'\nimport { EMBEDDING_PROVIDERS } from '../../../vector'\nimport { safeOllamaFetch } from '../../../vector/lib/ollama-url-safety'\n\nconst CACHE_VERSION = 'v1'\nconst CACHE_TTL_MS = 30_000\nconst OLLAMA_PROBE_TIMEOUT_MS = 1500\n\nexport type ProviderAvailability = {\n available: boolean\n reason?: string\n models?: number\n}\n\nexport type ProviderAvailabilityEntry = ProviderAvailability & {\n providerId: EmbeddingProviderId\n}\n\nexport type EmbeddingProviderProbe = {\n checkAvailability(\n providerId: EmbeddingProviderId,\n options?: { force?: boolean; baseUrl?: string },\n ): Promise<ProviderAvailability>\n}\n\nconst ALL_PROVIDERS: EmbeddingProviderId[] = ['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']\n\nexport async function checkAllProviders(\n probe: EmbeddingProviderProbe,\n options?: { force?: boolean },\n): Promise<ProviderAvailabilityEntry[]> {\n return Promise.all(\n ALL_PROVIDERS.map(async (providerId) => ({\n providerId,\n ...(await probe.checkAvailability(providerId, options)),\n })),\n )\n}\n\nconst cacheKey = (providerId: string) => `embedding-provider-probe:${CACHE_VERSION}:${providerId}`\n\nconst resolveCache = (container: AppContainer): CacheStrategy | null => {\n try {\n return container.resolve('cache') as CacheStrategy\n } catch {\n return null\n }\n}\n\nconst ollamaBaseUrl = () => process.env.OLLAMA_BASE_URL?.trim() || 'http://localhost:11434'\n\nconst keyPresence = (providerId: EmbeddingProviderId): ProviderAvailability => {\n const info = EMBEDDING_PROVIDERS[providerId]\n const envKey = info?.envKeyRequired\n switch (providerId) {\n case 'openai':\n return process.env.OPENAI_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'google':\n return process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'mistral':\n return process.env.MISTRAL_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'cohere':\n return process.env.COHERE_API_KEY?.trim()\n ? { available: true }\n : { available: false, reason: `Set ${envKey} to enable ${info?.name ?? providerId}` }\n case 'bedrock':\n return process.env.AWS_ACCESS_KEY_ID?.trim() && process.env.AWS_SECRET_ACCESS_KEY?.trim()\n ? { available: true }\n : { available: false, reason: 'Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to enable Bedrock' }\n default:\n return { available: false, reason: `Unknown provider: ${providerId}` }\n }\n}\n\nexport async function probeOllama(baseUrl: string): Promise<ProviderAvailability> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS)\n try {\n const response = await safeOllamaFetch(`${baseUrl.replace(/\\/$/, '')}/api/tags`, {\n method: 'GET',\n signal: controller.signal,\n })\n if (!response.ok) {\n return { available: false, reason: `Ollama responded ${response.status} at ${baseUrl}` }\n }\n let models: number | undefined\n try {\n const payload = (await response.json()) as { models?: unknown[] }\n if (Array.isArray(payload?.models)) models = payload.models.length\n } catch {}\n return { available: true, models }\n } catch (error) {\n const aborted = error instanceof Error && error.name === 'AbortError'\n return {\n available: false,\n reason: aborted ? `Ollama not reachable at ${baseUrl} (timed out)` : `Ollama not reachable at ${baseUrl}`,\n }\n } finally {\n clearTimeout(timer)\n }\n}\n\nasync function computeAvailability(\n providerId: EmbeddingProviderId,\n options?: { baseUrl?: string },\n): Promise<ProviderAvailability> {\n try {\n if (providerId === 'ollama') {\n return await probeOllama(options?.baseUrl?.trim() || ollamaBaseUrl())\n }\n return keyPresence(providerId)\n } catch (error) {\n // Fail closed: any unexpected error means the provider is treated as unavailable.\n return { available: false, reason: error instanceof Error ? error.message : 'Availability check failed' }\n }\n}\n\nexport function createEmbeddingProviderProbe(container: AppContainer): EmbeddingProviderProbe {\n const checkAvailability = async (\n providerId: EmbeddingProviderId,\n options?: { force?: boolean; baseUrl?: string },\n ): Promise<ProviderAvailability> => {\n const cache = resolveCache(container)\n const key = cacheKey(providerId)\n const probesCandidateUrl = providerId === 'ollama' && options?.baseUrl !== undefined\n if (!options?.force && !probesCandidateUrl && cache) {\n try {\n const cached = await cache.get(key)\n if (cached && typeof cached === 'object' && 'available' in cached) {\n return cached as ProviderAvailability\n }\n } catch {}\n }\n const result = await computeAvailability(providerId, options)\n if (cache && !probesCandidateUrl) {\n try {\n await cache.set(key, result, { ttl: CACHE_TTL_MS })\n } catch {}\n }\n return result\n }\n return { checkAvailability }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,10 @@
1
1
  import {
2
- assertStaticallySafeOutboundUrl
2
+ assertStaticallySafeOutboundUrl,
3
+ safeOutboundFetch
3
4
  } from "@open-mercato/shared/lib/url-safety";
4
5
  import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
5
6
  const SUBJECT = "Ollama base URL";
7
+ const MAX_OLLAMA_REDIRECTS = 5;
6
8
  class UnsafeOllamaBaseUrlError extends Error {
7
9
  constructor(reason, message) {
8
10
  super(message ?? `Ollama base URL rejected: ${reason}`);
@@ -24,13 +26,69 @@ function assertSafeOllamaBaseUrl(rawUrl) {
24
26
  if (typeof rawUrl !== "string" || rawUrl.trim().length === 0) {
25
27
  throw new UnsafeOllamaBaseUrlError("missing_host", `${SUBJECT} is required`);
26
28
  }
27
- const allowPrivate = allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) || isAllowPrivateOllamaBaseUrlEnabled() || process.env.NODE_ENV !== "production" && isLoopbackOnlyUrl(rawUrl);
28
29
  assertStaticallySafeOutboundUrl(rawUrl, {
29
30
  errorFactory: ollamaErrorFactory,
30
31
  subject: SUBJECT,
31
- allowPrivate
32
+ allowPrivate: shouldAllowPrivateOllamaUrl(rawUrl)
32
33
  });
33
34
  }
35
+ async function safeOllamaFetch(input, init = {}, deps = {}) {
36
+ const request = new Request(input, init);
37
+ const maxRedirects = deps.maxRedirects ?? MAX_OLLAMA_REDIRECTS;
38
+ let currentUrl = request.url;
39
+ let currentMethod = request.method;
40
+ let currentHeaders = new Headers(request.headers);
41
+ let currentBody = currentMethod === "GET" || currentMethod === "HEAD" ? void 0 : await request.clone().arrayBuffer();
42
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
43
+ const response = await safeOutboundFetch(
44
+ currentUrl,
45
+ {
46
+ method: currentMethod,
47
+ headers: currentHeaders,
48
+ body: currentBody,
49
+ signal: request.signal,
50
+ redirect: "manual"
51
+ },
52
+ {
53
+ errorFactory: ollamaErrorFactory,
54
+ subject: SUBJECT,
55
+ allowPrivate: shouldAllowPrivateOllamaUrl(currentUrl),
56
+ lookupHost: deps.lookupHost,
57
+ fetchImpl: deps.fetchImpl
58
+ }
59
+ );
60
+ if (![301, 302, 303, 307, 308].includes(response.status)) {
61
+ return response;
62
+ }
63
+ const location = response.headers.get("location");
64
+ if (!location) return response;
65
+ if (redirectCount === maxRedirects) {
66
+ throw new UnsafeOllamaBaseUrlError(
67
+ "too_many_redirects",
68
+ `${SUBJECT} exceeded ${maxRedirects} redirects`
69
+ );
70
+ }
71
+ const nextUrl = new URL(location, currentUrl);
72
+ if (nextUrl.origin !== new URL(currentUrl).origin) {
73
+ currentHeaders = new Headers(currentHeaders);
74
+ currentHeaders.delete("authorization");
75
+ currentHeaders.delete("cookie");
76
+ currentHeaders.delete("proxy-authorization");
77
+ }
78
+ if (response.status === 303 && currentMethod !== "GET" && currentMethod !== "HEAD" || (response.status === 301 || response.status === 302) && currentMethod === "POST") {
79
+ currentMethod = "GET";
80
+ currentBody = void 0;
81
+ currentHeaders.delete("content-length");
82
+ currentHeaders.delete("content-type");
83
+ }
84
+ await response.body?.cancel();
85
+ currentUrl = nextUrl.toString();
86
+ }
87
+ throw new UnsafeOllamaBaseUrlError("too_many_redirects");
88
+ }
89
+ function shouldAllowPrivateOllamaUrl(rawUrl) {
90
+ return allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) || isAllowPrivateOllamaBaseUrlEnabled() || process.env.NODE_ENV !== "production" && isLoopbackOnlyUrl(rawUrl);
91
+ }
34
92
  function allowlistMatches(rawUrl, allowlist) {
35
93
  if (allowlist.size === 0) return false;
36
94
  let parsed;
@@ -67,6 +125,7 @@ export {
67
125
  UnsafeOllamaBaseUrlError,
68
126
  assertSafeOllamaBaseUrl,
69
127
  getOllamaBaseUrlAllowlist,
70
- isAllowPrivateOllamaBaseUrlEnabled
128
+ isAllowPrivateOllamaBaseUrlEnabled,
129
+ safeOllamaFetch
71
130
  };
72
131
  //# sourceMappingURL=ollama-url-safety.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/vector/lib/ollama-url-safety.ts"],
4
- "sourcesContent": ["import {\n assertStaticallySafeOutboundUrl,\n type UrlSafetyReason,\n} from '@open-mercato/shared/lib/url-safety'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nconst SUBJECT = 'Ollama base URL'\n\nexport class UnsafeOllamaBaseUrlError extends Error {\n public readonly reason: string\n\n constructor(reason: string, message?: string) {\n super(message ?? `Ollama base URL rejected: ${reason}`)\n this.name = 'UnsafeOllamaBaseUrlError'\n this.reason = reason\n }\n}\n\nconst ollamaErrorFactory = (reason: UrlSafetyReason, message: string) =>\n new UnsafeOllamaBaseUrlError(reason, message)\n\nexport function getOllamaBaseUrlAllowlist(): ReadonlySet<string> {\n const raw = process.env.OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST ?? ''\n return new Set(\n raw\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n )\n}\n\nexport function isAllowPrivateOllamaBaseUrlEnabled(): boolean {\n return parseBooleanWithDefault(process.env.OM_SEARCH_OLLAMA_ALLOW_PRIVATE, false)\n}\n\nexport function assertSafeOllamaBaseUrl(rawUrl: string): void {\n if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) {\n throw new UnsafeOllamaBaseUrlError('missing_host', `${SUBJECT} is required`)\n }\n\n const allowPrivate =\n allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) ||\n isAllowPrivateOllamaBaseUrlEnabled() ||\n (process.env.NODE_ENV !== 'production' && isLoopbackOnlyUrl(rawUrl))\n\n assertStaticallySafeOutboundUrl(rawUrl, {\n errorFactory: ollamaErrorFactory,\n subject: SUBJECT,\n allowPrivate,\n })\n}\n\nfunction allowlistMatches(rawUrl: string, allowlist: ReadonlySet<string>): boolean {\n if (allowlist.size === 0) return false\n let parsed: URL\n try {\n parsed = new URL(rawUrl)\n } catch {\n return false\n }\n let host = parsed.hostname.toLowerCase()\n if (host.startsWith('[') && host.endsWith(']')) {\n host = host.slice(1, -1)\n }\n const port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80')\n const hostPort = `${host}:${port}`\n return allowlist.has(host) || allowlist.has(hostPort)\n}\n\nfunction isLoopbackOnlyUrl(rawUrl: string): boolean {\n let parsed: URL\n try {\n parsed = new URL(rawUrl)\n } catch {\n return false\n }\n let host = parsed.hostname.toLowerCase()\n if (host.startsWith('[') && host.endsWith(']')) {\n host = host.slice(1, -1)\n }\n if (host === 'localhost') return true\n if (host === '::1') return true\n if (/^127\\.\\d+\\.\\d+\\.\\d+$/.test(host)) return true\n return false\n}\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,OAEK;AACP,SAAS,+BAA+B;AAExC,MAAM,UAAU;AAET,MAAM,iCAAiC,MAAM;AAAA,EAGlD,YAAY,QAAgB,SAAkB;AAC5C,UAAM,WAAW,6BAA6B,MAAM,EAAE;AACtD,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,MAAM,qBAAqB,CAAC,QAAyB,YACnD,IAAI,yBAAyB,QAAQ,OAAO;AAEvC,SAAS,4BAAiD;AAC/D,QAAM,MAAM,QAAQ,IAAI,uCAAuC;AAC/D,SAAO,IAAI;AAAA,IACT,IACG,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC,EACzC,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC;AAAA,EACvC;AACF;AAEO,SAAS,qCAA8C;AAC5D,SAAO,wBAAwB,QAAQ,IAAI,gCAAgC,KAAK;AAClF;AAEO,SAAS,wBAAwB,QAAsB;AAC5D,MAAI,OAAO,WAAW,YAAY,OAAO,KAAK,EAAE,WAAW,GAAG;AAC5D,UAAM,IAAI,yBAAyB,gBAAgB,GAAG,OAAO,cAAc;AAAA,EAC7E;AAEA,QAAM,eACJ,iBAAiB,QAAQ,0BAA0B,CAAC,KACpD,mCAAmC,KAClC,QAAQ,IAAI,aAAa,gBAAgB,kBAAkB,MAAM;AAEpE,kCAAgC,QAAQ;AAAA,IACtC,cAAc;AAAA,IACd,SAAS;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEA,SAAS,iBAAiB,QAAgB,WAAyC;AACjF,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,MAAM;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,OAAO,SAAS,YAAY;AACvC,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG;AAC9C,WAAO,KAAK,MAAM,GAAG,EAAE;AAAA,EACzB;AACA,QAAM,OAAO,OAAO,SAAS,OAAO,aAAa,WAAW,QAAQ;AACpE,QAAM,WAAW,GAAG,IAAI,IAAI,IAAI;AAChC,SAAO,UAAU,IAAI,IAAI,KAAK,UAAU,IAAI,QAAQ;AACtD;AAEA,SAAS,kBAAkB,QAAyB;AAClD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,MAAM;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,OAAO,SAAS,YAAY;AACvC,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAG;AAC9C,WAAO,KAAK,MAAM,GAAG,EAAE;AAAA,EACzB;AACA,MAAI,SAAS,YAAa,QAAO;AACjC,MAAI,SAAS,MAAO,QAAO;AAC3B,MAAI,uBAAuB,KAAK,IAAI,EAAG,QAAO;AAC9C,SAAO;AACT;",
4
+ "sourcesContent": ["import {\n assertStaticallySafeOutboundUrl,\n safeOutboundFetch,\n type HostLookup,\n type SafeOutboundFetchOptions,\n type UrlSafetyReason,\n} from '@open-mercato/shared/lib/url-safety'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nconst SUBJECT = 'Ollama base URL'\nconst MAX_OLLAMA_REDIRECTS = 5\n\nexport class UnsafeOllamaBaseUrlError extends Error {\n public readonly reason: string\n\n constructor(reason: string, message?: string) {\n super(message ?? `Ollama base URL rejected: ${reason}`)\n this.name = 'UnsafeOllamaBaseUrlError'\n this.reason = reason\n }\n}\n\nconst ollamaErrorFactory = (reason: UrlSafetyReason, message: string) =>\n new UnsafeOllamaBaseUrlError(reason, message)\n\nexport function getOllamaBaseUrlAllowlist(): ReadonlySet<string> {\n const raw = process.env.OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST ?? ''\n return new Set(\n raw\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n )\n}\n\nexport function isAllowPrivateOllamaBaseUrlEnabled(): boolean {\n return parseBooleanWithDefault(process.env.OM_SEARCH_OLLAMA_ALLOW_PRIVATE, false)\n}\n\nexport function assertSafeOllamaBaseUrl(rawUrl: string): void {\n if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) {\n throw new UnsafeOllamaBaseUrlError('missing_host', `${SUBJECT} is required`)\n }\n\n assertStaticallySafeOutboundUrl(rawUrl, {\n errorFactory: ollamaErrorFactory,\n subject: SUBJECT,\n allowPrivate: shouldAllowPrivateOllamaUrl(rawUrl),\n })\n}\n\nexport type SafeOllamaFetchDeps = {\n lookupHost?: HostLookup\n fetchImpl?: SafeOutboundFetchOptions['fetchImpl']\n maxRedirects?: number\n}\n\nexport async function safeOllamaFetch(\n input: RequestInfo | URL,\n init: RequestInit = {},\n deps: SafeOllamaFetchDeps = {},\n): Promise<Response> {\n const request = new Request(input, init)\n const maxRedirects = deps.maxRedirects ?? MAX_OLLAMA_REDIRECTS\n let currentUrl = request.url\n let currentMethod = request.method\n let currentHeaders = new Headers(request.headers)\n let currentBody =\n currentMethod === 'GET' || currentMethod === 'HEAD'\n ? undefined\n : await request.clone().arrayBuffer()\n\n for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {\n const response = await safeOutboundFetch(\n currentUrl,\n {\n method: currentMethod,\n headers: currentHeaders,\n body: currentBody,\n signal: request.signal,\n redirect: 'manual',\n },\n {\n errorFactory: ollamaErrorFactory,\n subject: SUBJECT,\n allowPrivate: shouldAllowPrivateOllamaUrl(currentUrl),\n lookupHost: deps.lookupHost,\n fetchImpl: deps.fetchImpl,\n },\n )\n\n if (![301, 302, 303, 307, 308].includes(response.status)) {\n return response\n }\n const location = response.headers.get('location')\n if (!location) return response\n if (redirectCount === maxRedirects) {\n throw new UnsafeOllamaBaseUrlError(\n 'too_many_redirects',\n `${SUBJECT} exceeded ${maxRedirects} redirects`,\n )\n }\n\n const nextUrl = new URL(location, currentUrl)\n if (nextUrl.origin !== new URL(currentUrl).origin) {\n currentHeaders = new Headers(currentHeaders)\n currentHeaders.delete('authorization')\n currentHeaders.delete('cookie')\n currentHeaders.delete('proxy-authorization')\n }\n if (\n (response.status === 303 && currentMethod !== 'GET' && currentMethod !== 'HEAD') ||\n ((response.status === 301 || response.status === 302) && currentMethod === 'POST')\n ) {\n currentMethod = 'GET'\n currentBody = undefined\n currentHeaders.delete('content-length')\n currentHeaders.delete('content-type')\n }\n await response.body?.cancel()\n currentUrl = nextUrl.toString()\n }\n\n throw new UnsafeOllamaBaseUrlError('too_many_redirects')\n}\n\nfunction shouldAllowPrivateOllamaUrl(rawUrl: string): boolean {\n return (\n allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) ||\n isAllowPrivateOllamaBaseUrlEnabled() ||\n (process.env.NODE_ENV !== 'production' && isLoopbackOnlyUrl(rawUrl))\n )\n}\n\nfunction allowlistMatches(rawUrl: string, allowlist: ReadonlySet<string>): boolean {\n if (allowlist.size === 0) return false\n let parsed: URL\n try {\n parsed = new URL(rawUrl)\n } catch {\n return false\n }\n let host = parsed.hostname.toLowerCase()\n if (host.startsWith('[') && host.endsWith(']')) {\n host = host.slice(1, -1)\n }\n const port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80')\n const hostPort = `${host}:${port}`\n return allowlist.has(host) || allowlist.has(hostPort)\n}\n\nfunction isLoopbackOnlyUrl(rawUrl: string): boolean {\n let parsed: URL\n try {\n parsed = new URL(rawUrl)\n } catch {\n return false\n }\n let host = parsed.hostname.toLowerCase()\n if (host.startsWith('[') && host.endsWith(']')) {\n host = host.slice(1, -1)\n }\n if (host === 'localhost') return true\n if (host === '::1') return true\n if (/^127\\.\\d+\\.\\d+\\.\\d+$/.test(host)) return true\n return false\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,11 @@ import { createCohere } from "@ai-sdk/cohere";
6
6
  import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
7
7
  import { createOllama } from "ai-sdk-ollama";
8
8
  import { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG } from "../types.js";
9
- import { assertSafeOllamaBaseUrl, UnsafeOllamaBaseUrlError } from "../lib/ollama-url-safety.js";
9
+ import {
10
+ assertSafeOllamaBaseUrl,
11
+ safeOllamaFetch,
12
+ UnsafeOllamaBaseUrlError
13
+ } from "../lib/ollama-url-safety.js";
10
14
  const DEFAULT_EMBEDDING_TIMEOUT_MS = 3e3;
11
15
  function resolveEmbeddingTimeoutMs() {
12
16
  const rawValue = process.env.VECTOR_EMBEDDING_TIMEOUT_MS;
@@ -136,7 +140,7 @@ class EmbeddingService {
136
140
  }
137
141
  throw err;
138
142
  }
139
- client = createOllama({ baseURL });
143
+ client = createOllama({ baseURL, fetch: safeOllamaFetch });
140
144
  break;
141
145
  }
142
146
  default:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/vector/services/embedding.ts"],
4
- "sourcesContent": ["import { embed } from 'ai'\nimport type { EmbeddingModel } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\n\n// Local type definition to avoid @ai-sdk/provider version conflicts\n// Matches SharedV3ProviderOptions = Record<string, JSONObject>\ntype JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }\ntype JSONObject = { [key: string]: JSONValue }\ntype ProviderOptions = Record<string, JSONObject>\nimport { createGoogleGenerativeAI } from '@ai-sdk/google'\nimport { createMistral } from '@ai-sdk/mistral'\nimport { createCohere } from '@ai-sdk/cohere'\nimport { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'\nimport { createOllama } from 'ai-sdk-ollama'\nimport type { EmbeddingProviderId, EmbeddingProviderConfig } from '../types'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG } from '../types'\nimport { assertSafeOllamaBaseUrl, UnsafeOllamaBaseUrlError } from '../lib/ollama-url-safety'\n\nexport type EmbeddingServiceOptions = {\n apiKey?: string\n model?: string\n config?: EmbeddingProviderConfig\n}\n\ntype OllamaClient = ReturnType<typeof createOllama>\n\ntype ProviderClient = ReturnType<typeof createOpenAI>\n | ReturnType<typeof createGoogleGenerativeAI>\n | ReturnType<typeof createMistral>\n | ReturnType<typeof createCohere>\n | ReturnType<typeof createAmazonBedrock>\n | OllamaClient\n\nconst DEFAULT_EMBEDDING_TIMEOUT_MS = 3_000\n\nfunction resolveEmbeddingTimeoutMs(): number {\n const rawValue = process.env.VECTOR_EMBEDDING_TIMEOUT_MS\n if (!rawValue) return DEFAULT_EMBEDDING_TIMEOUT_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return DEFAULT_EMBEDDING_TIMEOUT_MS\n }\n return parsed\n}\n\nfunction timeoutError(providerId: EmbeddingProviderId, timeoutMs: number): Error {\n const providerInfo = EMBEDDING_PROVIDERS[providerId]\n return new Error(\n `${providerInfo.name} request timed out after ${timeoutMs}ms. Check ${providerInfo.envKeyRequired}.`,\n )\n}\n\nexport class EmbeddingService {\n private config: EmbeddingProviderConfig\n private clientCache: Map<EmbeddingProviderId, ProviderClient> = new Map()\n\n constructor(private readonly opts: EmbeddingServiceOptions = {}) {\n if (opts.config) {\n this.config = opts.config\n } else {\n this.config = {\n providerId: 'openai',\n model: opts.model ?? DEFAULT_EMBEDDING_CONFIG.model,\n dimension: DEFAULT_EMBEDDING_CONFIG.dimension,\n updatedAt: new Date().toISOString(),\n }\n }\n }\n\n updateConfig(config: EmbeddingProviderConfig): void {\n if (\n config.providerId === this.config.providerId &&\n config.model === this.config.model &&\n config.dimension === this.config.dimension &&\n config.outputDimensionality === this.config.outputDimensionality &&\n config.baseUrl === this.config.baseUrl\n ) {\n return\n }\n this.config = config\n this.clientCache.clear()\n }\n\n get currentConfig(): EmbeddingProviderConfig {\n return { ...this.config }\n }\n\n get dimension(): number {\n return this.config.outputDimensionality ?? this.config.dimension\n }\n\n get available(): boolean {\n return this.isProviderConfigured(this.config.providerId)\n }\n\n private isProviderConfigured(providerId: EmbeddingProviderId): boolean {\n switch (providerId) {\n case 'openai':\n return Boolean(this.opts.apiKey ?? process.env.OPENAI_API_KEY)\n case 'google':\n return Boolean(process.env.GOOGLE_GENERATIVE_AI_API_KEY)\n case 'mistral':\n return Boolean(process.env.MISTRAL_API_KEY)\n case 'cohere':\n return Boolean(process.env.COHERE_API_KEY)\n case 'bedrock':\n return Boolean(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)\n case 'ollama':\n return true\n default:\n return false\n }\n }\n\n private getClient(providerId: EmbeddingProviderId): ProviderClient {\n const cached = this.clientCache.get(providerId)\n if (cached) {\n return cached\n }\n\n let client: ProviderClient\n switch (providerId) {\n case 'openai': {\n const apiKey = this.opts.apiKey ?? process.env.OPENAI_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing OPENAI_API_KEY environment variable')\n }\n client = createOpenAI({ apiKey })\n break\n }\n case 'google': {\n const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing GOOGLE_GENERATIVE_AI_API_KEY environment variable')\n }\n client = createGoogleGenerativeAI({ apiKey })\n break\n }\n case 'mistral': {\n const apiKey = process.env.MISTRAL_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing MISTRAL_API_KEY environment variable')\n }\n client = createMistral({ apiKey })\n break\n }\n case 'cohere': {\n const apiKey = process.env.COHERE_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing COHERE_API_KEY environment variable')\n }\n client = createCohere({ apiKey })\n break\n }\n case 'bedrock': {\n const accessKeyId = process.env.AWS_ACCESS_KEY_ID\n const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY\n if (!accessKeyId || !secretAccessKey) {\n throw new Error('[vector.embedding] Missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variables')\n }\n client = createAmazonBedrock({\n accessKeyId,\n secretAccessKey,\n region: process.env.AWS_REGION ?? 'us-east-1',\n })\n break\n }\n case 'ollama': {\n const baseURL = this.config.baseUrl ?? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434'\n try {\n assertSafeOllamaBaseUrl(baseURL)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n throw new Error(\n `[vector.embedding] Ollama base URL rejected (${err.reason}). Set OLLAMA_BASE_URL or OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.`,\n )\n }\n throw err\n }\n client = createOllama({ baseURL })\n break\n }\n default:\n throw new Error(`[vector.embedding] Unknown provider: ${providerId}`)\n }\n\n this.clientCache.set(providerId, client)\n return client\n }\n\n private getEmbeddingModel() {\n const client = this.getClient(this.config.providerId)\n const { providerId, model, outputDimensionality } = this.config\n\n switch (providerId) {\n case 'openai':\n return (client as ReturnType<typeof createOpenAI>).embedding(model)\n case 'google':\n return (client as ReturnType<typeof createGoogleGenerativeAI>).textEmbeddingModel(model)\n case 'mistral':\n return (client as ReturnType<typeof createMistral>).textEmbeddingModel(model)\n case 'cohere':\n return (client as ReturnType<typeof createCohere>).textEmbeddingModel(model)\n case 'bedrock':\n return (client as ReturnType<typeof createAmazonBedrock>).embedding(model)\n case 'ollama':\n return (client as OllamaClient).embedding(model)\n default:\n throw new Error(`[vector.embedding] Unknown provider: ${providerId}`)\n }\n }\n\n private getProviderOptions(): ProviderOptions | undefined {\n const { providerId, outputDimensionality, model } = this.config\n\n if (!outputDimensionality) {\n if (providerId === 'cohere') {\n return { cohere: { inputType: 'search_document' } }\n }\n return undefined\n }\n\n switch (providerId) {\n case 'openai':\n if (model === 'text-embedding-3-large' || model === 'text-embedding-3-small') {\n return { openai: { dimensions: outputDimensionality } }\n }\n return undefined\n case 'google':\n return { google: { outputDimensionality } }\n case 'bedrock':\n return { bedrock: { dimensions: outputDimensionality } }\n case 'cohere':\n return { cohere: { inputType: 'search_document' } }\n default:\n return undefined\n }\n }\n\n async createEmbedding(input: string | string[]): Promise<number[]> {\n const merged = Array.isArray(input)\n ? input.map((part) => String(part ?? '')).filter((part) => part.length > 0).join('\\n\\n')\n : String(input ?? '')\n if (!merged.length) {\n throw new Error('[vector.embedding] Refusing to embed empty payload')\n }\n\n if (!this.available) {\n const providerInfo = EMBEDDING_PROVIDERS[this.config.providerId]\n throw new Error(`[vector.embedding] Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`)\n }\n\n const model = this.getEmbeddingModel() as EmbeddingModel\n const providerOptions = this.getProviderOptions()\n const timeoutMs = resolveEmbeddingTimeoutMs()\n\n const abortController = new AbortController()\n let timeoutHandle: ReturnType<typeof setTimeout> | null = null\n try {\n const result = await Promise.race([\n embed({\n model,\n value: merged,\n abortSignal: abortController.signal,\n ...(providerOptions && { providerOptions }),\n }),\n new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(\n () => {\n // Abort the in-flight request so a dead/unreachable provider releases\n // its socket \u2014 and, in a worker, the per-job DB connection held while\n // this awaits \u2014 promptly, instead of lingering until the platform's\n // default network timeout and pinning pool capacity under a storm.\n abortController.abort()\n reject(timeoutError(this.config.providerId, timeoutMs))\n },\n timeoutMs,\n )\n }),\n ])\n const emb = Array.isArray(result.embedding)\n ? result.embedding\n : Array.from(result.embedding as ArrayLike<number>)\n return emb.map((n) => Number.isFinite(n) ? Number(n) : 0)\n } catch (err: unknown) {\n const error = err as { statusCode?: number; status?: number; response?: { status?: number; statusCode?: number; data?: { error?: { message?: string; code?: string }; message?: string } }; data?: { error?: { message?: string; code?: string } }; body?: { error?: { message?: string; code?: string } }; message?: string }\n const statusCandidate =\n error?.statusCode ?? error?.status ?? error?.response?.status ?? error?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = error?.data?.error ?? error?.body?.error ?? error?.response?.data?.error\n const apiMessage = apiError?.message ?? error?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof error?.message === 'string' ? error.message : 'Embedding request failed')\n\n const providerInfo = EMBEDDING_PROVIDERS[this.config.providerId]\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = `${providerInfo.name} usage quota exceeded. Please review your plan and billing.`\n break\n case 'invalid_api_key':\n guidance = `Invalid ${providerInfo.name} API key. Update the key and retry.`\n break\n case 'account_deactivated':\n guidance = `${providerInfo.name} account is disabled. Contact support or provide a different key.`\n break\n default:\n guidance = rawMessage.startsWith('[vector.embedding] ')\n ? rawMessage.slice('[vector.embedding] '.length)\n : rawMessage.includes('https://')\n ? rawMessage\n : rawMessage.includes(providerInfo.envKeyRequired)\n ? rawMessage\n : `${rawMessage}. Check ${providerInfo.envKeyRequired}.`\n }\n const wrapped = new Error(`[vector.embedding] ${guidance}`) as Error & { status?: number; code?: string; cause?: unknown }\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n wrapped.status = normalizedStatus\n }\n }\n if (apiCode) {\n wrapped.code = apiCode\n }\n wrapped.cause = err\n throw wrapped\n } finally {\n if (timeoutHandle !== null) {\n clearTimeout(timeoutHandle)\n }\n }\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAO7B,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB,gCAAgC;AAC9D,SAAS,yBAAyB,gCAAgC;AAiBlE,MAAM,+BAA+B;AAErC,SAAS,4BAAoC;AAC3C,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,SAAS,OAAO,SAAS,UAAU,EAAE;AAC3C,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAAG;AAC3C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,aAAa,YAAiC,WAA0B;AAC/E,QAAM,eAAe,oBAAoB,UAAU;AACnD,SAAO,IAAI;AAAA,IACT,GAAG,aAAa,IAAI,4BAA4B,SAAS,aAAa,aAAa,cAAc;AAAA,EACnG;AACF;AAEO,MAAM,iBAAiB;AAAA,EAI5B,YAA6B,OAAgC,CAAC,GAAG;AAApC;AAF7B,SAAQ,cAAwD,oBAAI,IAAI;AAGtE,QAAI,KAAK,QAAQ;AACf,WAAK,SAAS,KAAK;AAAA,IACrB,OAAO;AACL,WAAK,SAAS;AAAA,QACZ,YAAY;AAAA,QACZ,OAAO,KAAK,SAAS,yBAAyB;AAAA,QAC9C,WAAW,yBAAyB;AAAA,QACpC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,aAAa,QAAuC;AAClD,QACE,OAAO,eAAe,KAAK,OAAO,cAClC,OAAO,UAAU,KAAK,OAAO,SAC7B,OAAO,cAAc,KAAK,OAAO,aACjC,OAAO,yBAAyB,KAAK,OAAO,wBAC5C,OAAO,YAAY,KAAK,OAAO,SAC/B;AACA;AAAA,IACF;AACA,SAAK,SAAS;AACd,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA,EAEA,IAAI,gBAAyC;AAC3C,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,OAAO,wBAAwB,KAAK,OAAO;AAAA,EACzD;AAAA,EAEA,IAAI,YAAqB;AACvB,WAAO,KAAK,qBAAqB,KAAK,OAAO,UAAU;AAAA,EACzD;AAAA,EAEQ,qBAAqB,YAA0C;AACrE,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAO,QAAQ,KAAK,KAAK,UAAU,QAAQ,IAAI,cAAc;AAAA,MAC/D,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,4BAA4B;AAAA,MACzD,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,eAAe;AAAA,MAC5C,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,cAAc;AAAA,MAC3C,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,qBAAqB,QAAQ,IAAI,qBAAqB;AAAA,MACnF,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEQ,UAAU,YAAiD;AACjE,UAAM,SAAS,KAAK,YAAY,IAAI,UAAU;AAC9C,QAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAEA,QAAI;AACJ,YAAQ,YAAY;AAAA,MAClB,KAAK,UAAU;AACb,cAAM,SAAS,KAAK,KAAK,UAAU,QAAQ,IAAI;AAC/C,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,gEAAgE;AAAA,QAClF;AACA,iBAAS,aAAa,EAAE,OAAO,CAAC;AAChC;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,8EAA8E;AAAA,QAChG;AACA,iBAAS,yBAAyB,EAAE,OAAO,CAAC;AAC5C;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,iEAAiE;AAAA,QACnF;AACA,iBAAS,cAAc,EAAE,OAAO,CAAC;AACjC;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,gEAAgE;AAAA,QAClF;AACA,iBAAS,aAAa,EAAE,OAAO,CAAC;AAChC;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,cAAM,cAAc,QAAQ,IAAI;AAChC,cAAM,kBAAkB,QAAQ,IAAI;AACpC,YAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,gBAAM,IAAI,MAAM,6FAA6F;AAAA,QAC/G;AACA,iBAAS,oBAAoB;AAAA,UAC3B;AAAA,UACA;AAAA,UACA,QAAQ,QAAQ,IAAI,cAAc;AAAA,QACpC,CAAC;AACD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,UAAU,KAAK,OAAO,WAAW,QAAQ,IAAI,mBAAmB;AACtE,YAAI;AACF,kCAAwB,OAAO;AAAA,QACjC,SAAS,KAAK;AACZ,cAAI,eAAe,0BAA0B;AAC3C,kBAAM,IAAI;AAAA,cACR,gDAAgD,IAAI,MAAM;AAAA,YAC5D;AAAA,UACF;AACA,gBAAM;AAAA,QACR;AACA,iBAAS,aAAa,EAAE,QAAQ,CAAC;AACjC;AAAA,MACF;AAAA,MACA;AACE,cAAM,IAAI,MAAM,wCAAwC,UAAU,EAAE;AAAA,IACxE;AAEA,SAAK,YAAY,IAAI,YAAY,MAAM;AACvC,WAAO;AAAA,EACT;AAAA,EAEQ,oBAAoB;AAC1B,UAAM,SAAS,KAAK,UAAU,KAAK,OAAO,UAAU;AACpD,UAAM,EAAE,YAAY,OAAO,qBAAqB,IAAI,KAAK;AAEzD,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAQ,OAA2C,UAAU,KAAK;AAAA,MACpE,KAAK;AACH,eAAQ,OAAuD,mBAAmB,KAAK;AAAA,MACzF,KAAK;AACH,eAAQ,OAA4C,mBAAmB,KAAK;AAAA,MAC9E,KAAK;AACH,eAAQ,OAA2C,mBAAmB,KAAK;AAAA,MAC7E,KAAK;AACH,eAAQ,OAAkD,UAAU,KAAK;AAAA,MAC3E,KAAK;AACH,eAAQ,OAAwB,UAAU,KAAK;AAAA,MACjD;AACE,cAAM,IAAI,MAAM,wCAAwC,UAAU,EAAE;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,qBAAkD;AACxD,UAAM,EAAE,YAAY,sBAAsB,MAAM,IAAI,KAAK;AAEzD,QAAI,CAAC,sBAAsB;AACzB,UAAI,eAAe,UAAU;AAC3B,eAAO,EAAE,QAAQ,EAAE,WAAW,kBAAkB,EAAE;AAAA,MACpD;AACA,aAAO;AAAA,IACT;AAEA,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,YAAI,UAAU,4BAA4B,UAAU,0BAA0B;AAC9E,iBAAO,EAAE,QAAQ,EAAE,YAAY,qBAAqB,EAAE;AAAA,QACtD;AACA,eAAO;AAAA,MACT,KAAK;AACH,eAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE;AAAA,MAC5C,KAAK;AACH,eAAO,EAAE,SAAS,EAAE,YAAY,qBAAqB,EAAE;AAAA,MACzD,KAAK;AACH,eAAO,EAAE,QAAQ,EAAE,WAAW,kBAAkB,EAAE;AAAA,MACpD;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,OAA6C;AACjE,UAAM,SAAS,MAAM,QAAQ,KAAK,IAC9B,MAAM,IAAI,CAAC,SAAS,OAAO,QAAQ,EAAE,CAAC,EAAE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAAE,KAAK,MAAM,IACrF,OAAO,SAAS,EAAE;AACtB,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AAEA,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,eAAe,oBAAoB,KAAK,OAAO,UAAU;AAC/D,YAAM,IAAI,MAAM,+BAA+B,aAAa,IAAI,2BAA2B,aAAa,cAAc,wBAAwB;AAAA,IAChJ;AAEA,UAAM,QAAQ,KAAK,kBAAkB;AACrC,UAAM,kBAAkB,KAAK,mBAAmB;AAChD,UAAM,YAAY,0BAA0B;AAE5C,UAAM,kBAAkB,IAAI,gBAAgB;AAC5C,QAAI,gBAAsD;AAC1D,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK;AAAA,QAChC,MAAM;AAAA,UACJ;AAAA,UACA,OAAO;AAAA,UACP,aAAa,gBAAgB;AAAA,UAC7B,GAAI,mBAAmB,EAAE,gBAAgB;AAAA,QAC3C,CAAC;AAAA,QACD,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,0BAAgB;AAAA,YACd,MAAM;AAKJ,8BAAgB,MAAM;AACtB,qBAAO,aAAa,KAAK,OAAO,YAAY,SAAS,CAAC;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AACD,YAAM,MAAM,MAAM,QAAQ,OAAO,SAAS,IACtC,OAAO,YACP,MAAM,KAAK,OAAO,SAA8B;AACpD,aAAO,IAAI,IAAI,CAAC,MAAM,OAAO,SAAS,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;AAAA,IAC1D,SAAS,KAAc;AACrB,YAAM,QAAQ;AACd,YAAM,kBACJ,OAAO,cAAc,OAAO,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU;AACpF,YAAM,SACJ,OAAO,oBAAoB,WACvB,OAAO,SAAS,eAAe,IAAI,kBAAkB,SACrD,OAAO,oBAAoB,WACzB,OAAO,SAAS,iBAAiB,EAAE,IACnC;AACR,YAAM,WAAW,OAAO,MAAM,SAAS,OAAO,MAAM,SAAS,OAAO,UAAU,MAAM;AACpF,YAAM,aAAa,UAAU,WAAW,OAAO,UAAU,MAAM;AAC/D,YAAM,UAAU,OAAO,UAAU,SAAS,WAAW,SAAS,OAAO;AACrE,YAAM,aAAa,OAAO,eAAe,WACrC,aACC,OAAO,OAAO,YAAY,WAAW,MAAM,UAAU;AAE1D,YAAM,eAAe,oBAAoB,KAAK,OAAO,UAAU;AAC/D,UAAI;AACJ,cAAQ,SAAS;AAAA,QACf,KAAK;AACH,qBAAW,GAAG,aAAa,IAAI;AAC/B;AAAA,QACF,KAAK;AACH,qBAAW,WAAW,aAAa,IAAI;AACvC;AAAA,QACF,KAAK;AACH,qBAAW,GAAG,aAAa,IAAI;AAC/B;AAAA,QACF;AACE,qBAAW,WAAW,WAAW,qBAAqB,IAClD,WAAW,MAAM,sBAAsB,MAAM,IAC7C,WAAW,SAAS,UAAU,IAC9B,aACA,WAAW,SAAS,aAAa,cAAc,IAC7C,aACA,GAAG,UAAU,WAAW,aAAa,cAAc;AAAA,MAC7D;AACA,YAAM,UAAU,IAAI,MAAM,sBAAsB,QAAQ,EAAE;AAC1D,UAAI,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,GAAG;AACzD,cAAM,mBAAmB,WAAW,OAAO,WAAW,MAAM,MAAM;AAClE,YAAI,oBAAoB,OAAO,mBAAmB,KAAK;AACrD,kBAAQ,SAAS;AAAA,QACnB;AAAA,MACF;AACA,UAAI,SAAS;AACX,gBAAQ,OAAO;AAAA,MACjB;AACA,cAAQ,QAAQ;AAChB,YAAM;AAAA,IACR,UAAE;AACA,UAAI,kBAAkB,MAAM;AAC1B,qBAAa,aAAa;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { embed } from 'ai'\nimport type { EmbeddingModel } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\n\n// Local type definition to avoid @ai-sdk/provider version conflicts\n// Matches SharedV3ProviderOptions = Record<string, JSONObject>\ntype JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }\ntype JSONObject = { [key: string]: JSONValue }\ntype ProviderOptions = Record<string, JSONObject>\nimport { createGoogleGenerativeAI } from '@ai-sdk/google'\nimport { createMistral } from '@ai-sdk/mistral'\nimport { createCohere } from '@ai-sdk/cohere'\nimport { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'\nimport { createOllama } from 'ai-sdk-ollama'\nimport type { EmbeddingProviderId, EmbeddingProviderConfig } from '../types'\nimport { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG } from '../types'\nimport {\n assertSafeOllamaBaseUrl,\n safeOllamaFetch,\n UnsafeOllamaBaseUrlError,\n} from '../lib/ollama-url-safety'\n\nexport type EmbeddingServiceOptions = {\n apiKey?: string\n model?: string\n config?: EmbeddingProviderConfig\n}\n\ntype OllamaClient = ReturnType<typeof createOllama>\n\ntype ProviderClient = ReturnType<typeof createOpenAI>\n | ReturnType<typeof createGoogleGenerativeAI>\n | ReturnType<typeof createMistral>\n | ReturnType<typeof createCohere>\n | ReturnType<typeof createAmazonBedrock>\n | OllamaClient\n\nconst DEFAULT_EMBEDDING_TIMEOUT_MS = 3_000\n\nfunction resolveEmbeddingTimeoutMs(): number {\n const rawValue = process.env.VECTOR_EMBEDDING_TIMEOUT_MS\n if (!rawValue) return DEFAULT_EMBEDDING_TIMEOUT_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return DEFAULT_EMBEDDING_TIMEOUT_MS\n }\n return parsed\n}\n\nfunction timeoutError(providerId: EmbeddingProviderId, timeoutMs: number): Error {\n const providerInfo = EMBEDDING_PROVIDERS[providerId]\n return new Error(\n `${providerInfo.name} request timed out after ${timeoutMs}ms. Check ${providerInfo.envKeyRequired}.`,\n )\n}\n\nexport class EmbeddingService {\n private config: EmbeddingProviderConfig\n private clientCache: Map<EmbeddingProviderId, ProviderClient> = new Map()\n\n constructor(private readonly opts: EmbeddingServiceOptions = {}) {\n if (opts.config) {\n this.config = opts.config\n } else {\n this.config = {\n providerId: 'openai',\n model: opts.model ?? DEFAULT_EMBEDDING_CONFIG.model,\n dimension: DEFAULT_EMBEDDING_CONFIG.dimension,\n updatedAt: new Date().toISOString(),\n }\n }\n }\n\n updateConfig(config: EmbeddingProviderConfig): void {\n if (\n config.providerId === this.config.providerId &&\n config.model === this.config.model &&\n config.dimension === this.config.dimension &&\n config.outputDimensionality === this.config.outputDimensionality &&\n config.baseUrl === this.config.baseUrl\n ) {\n return\n }\n this.config = config\n this.clientCache.clear()\n }\n\n get currentConfig(): EmbeddingProviderConfig {\n return { ...this.config }\n }\n\n get dimension(): number {\n return this.config.outputDimensionality ?? this.config.dimension\n }\n\n get available(): boolean {\n return this.isProviderConfigured(this.config.providerId)\n }\n\n private isProviderConfigured(providerId: EmbeddingProviderId): boolean {\n switch (providerId) {\n case 'openai':\n return Boolean(this.opts.apiKey ?? process.env.OPENAI_API_KEY)\n case 'google':\n return Boolean(process.env.GOOGLE_GENERATIVE_AI_API_KEY)\n case 'mistral':\n return Boolean(process.env.MISTRAL_API_KEY)\n case 'cohere':\n return Boolean(process.env.COHERE_API_KEY)\n case 'bedrock':\n return Boolean(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)\n case 'ollama':\n return true\n default:\n return false\n }\n }\n\n private getClient(providerId: EmbeddingProviderId): ProviderClient {\n const cached = this.clientCache.get(providerId)\n if (cached) {\n return cached\n }\n\n let client: ProviderClient\n switch (providerId) {\n case 'openai': {\n const apiKey = this.opts.apiKey ?? process.env.OPENAI_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing OPENAI_API_KEY environment variable')\n }\n client = createOpenAI({ apiKey })\n break\n }\n case 'google': {\n const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing GOOGLE_GENERATIVE_AI_API_KEY environment variable')\n }\n client = createGoogleGenerativeAI({ apiKey })\n break\n }\n case 'mistral': {\n const apiKey = process.env.MISTRAL_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing MISTRAL_API_KEY environment variable')\n }\n client = createMistral({ apiKey })\n break\n }\n case 'cohere': {\n const apiKey = process.env.COHERE_API_KEY\n if (!apiKey) {\n throw new Error('[vector.embedding] Missing COHERE_API_KEY environment variable')\n }\n client = createCohere({ apiKey })\n break\n }\n case 'bedrock': {\n const accessKeyId = process.env.AWS_ACCESS_KEY_ID\n const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY\n if (!accessKeyId || !secretAccessKey) {\n throw new Error('[vector.embedding] Missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variables')\n }\n client = createAmazonBedrock({\n accessKeyId,\n secretAccessKey,\n region: process.env.AWS_REGION ?? 'us-east-1',\n })\n break\n }\n case 'ollama': {\n const baseURL = this.config.baseUrl ?? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434'\n try {\n assertSafeOllamaBaseUrl(baseURL)\n } catch (err) {\n if (err instanceof UnsafeOllamaBaseUrlError) {\n throw new Error(\n `[vector.embedding] Ollama base URL rejected (${err.reason}). Set OLLAMA_BASE_URL or OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST.`,\n )\n }\n throw err\n }\n client = createOllama({ baseURL, fetch: safeOllamaFetch })\n break\n }\n default:\n throw new Error(`[vector.embedding] Unknown provider: ${providerId}`)\n }\n\n this.clientCache.set(providerId, client)\n return client\n }\n\n private getEmbeddingModel() {\n const client = this.getClient(this.config.providerId)\n const { providerId, model, outputDimensionality } = this.config\n\n switch (providerId) {\n case 'openai':\n return (client as ReturnType<typeof createOpenAI>).embedding(model)\n case 'google':\n return (client as ReturnType<typeof createGoogleGenerativeAI>).textEmbeddingModel(model)\n case 'mistral':\n return (client as ReturnType<typeof createMistral>).textEmbeddingModel(model)\n case 'cohere':\n return (client as ReturnType<typeof createCohere>).textEmbeddingModel(model)\n case 'bedrock':\n return (client as ReturnType<typeof createAmazonBedrock>).embedding(model)\n case 'ollama':\n return (client as OllamaClient).embedding(model)\n default:\n throw new Error(`[vector.embedding] Unknown provider: ${providerId}`)\n }\n }\n\n private getProviderOptions(): ProviderOptions | undefined {\n const { providerId, outputDimensionality, model } = this.config\n\n if (!outputDimensionality) {\n if (providerId === 'cohere') {\n return { cohere: { inputType: 'search_document' } }\n }\n return undefined\n }\n\n switch (providerId) {\n case 'openai':\n if (model === 'text-embedding-3-large' || model === 'text-embedding-3-small') {\n return { openai: { dimensions: outputDimensionality } }\n }\n return undefined\n case 'google':\n return { google: { outputDimensionality } }\n case 'bedrock':\n return { bedrock: { dimensions: outputDimensionality } }\n case 'cohere':\n return { cohere: { inputType: 'search_document' } }\n default:\n return undefined\n }\n }\n\n async createEmbedding(input: string | string[]): Promise<number[]> {\n const merged = Array.isArray(input)\n ? input.map((part) => String(part ?? '')).filter((part) => part.length > 0).join('\\n\\n')\n : String(input ?? '')\n if (!merged.length) {\n throw new Error('[vector.embedding] Refusing to embed empty payload')\n }\n\n if (!this.available) {\n const providerInfo = EMBEDDING_PROVIDERS[this.config.providerId]\n throw new Error(`[vector.embedding] Provider ${providerInfo.name} is not configured. Set ${providerInfo.envKeyRequired} environment variable.`)\n }\n\n const model = this.getEmbeddingModel() as EmbeddingModel\n const providerOptions = this.getProviderOptions()\n const timeoutMs = resolveEmbeddingTimeoutMs()\n\n const abortController = new AbortController()\n let timeoutHandle: ReturnType<typeof setTimeout> | null = null\n try {\n const result = await Promise.race([\n embed({\n model,\n value: merged,\n abortSignal: abortController.signal,\n ...(providerOptions && { providerOptions }),\n }),\n new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(\n () => {\n // Abort the in-flight request so a dead/unreachable provider releases\n // its socket \u2014 and, in a worker, the per-job DB connection held while\n // this awaits \u2014 promptly, instead of lingering until the platform's\n // default network timeout and pinning pool capacity under a storm.\n abortController.abort()\n reject(timeoutError(this.config.providerId, timeoutMs))\n },\n timeoutMs,\n )\n }),\n ])\n const emb = Array.isArray(result.embedding)\n ? result.embedding\n : Array.from(result.embedding as ArrayLike<number>)\n return emb.map((n) => Number.isFinite(n) ? Number(n) : 0)\n } catch (err: unknown) {\n const error = err as { statusCode?: number; status?: number; response?: { status?: number; statusCode?: number; data?: { error?: { message?: string; code?: string }; message?: string } }; data?: { error?: { message?: string; code?: string } }; body?: { error?: { message?: string; code?: string } }; message?: string }\n const statusCandidate =\n error?.statusCode ?? error?.status ?? error?.response?.status ?? error?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = error?.data?.error ?? error?.body?.error ?? error?.response?.data?.error\n const apiMessage = apiError?.message ?? error?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof error?.message === 'string' ? error.message : 'Embedding request failed')\n\n const providerInfo = EMBEDDING_PROVIDERS[this.config.providerId]\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = `${providerInfo.name} usage quota exceeded. Please review your plan and billing.`\n break\n case 'invalid_api_key':\n guidance = `Invalid ${providerInfo.name} API key. Update the key and retry.`\n break\n case 'account_deactivated':\n guidance = `${providerInfo.name} account is disabled. Contact support or provide a different key.`\n break\n default:\n guidance = rawMessage.startsWith('[vector.embedding] ')\n ? rawMessage.slice('[vector.embedding] '.length)\n : rawMessage.includes('https://')\n ? rawMessage\n : rawMessage.includes(providerInfo.envKeyRequired)\n ? rawMessage\n : `${rawMessage}. Check ${providerInfo.envKeyRequired}.`\n }\n const wrapped = new Error(`[vector.embedding] ${guidance}`) as Error & { status?: number; code?: string; cause?: unknown }\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n wrapped.status = normalizedStatus\n }\n }\n if (apiCode) {\n wrapped.code = apiCode\n }\n wrapped.cause = err\n throw wrapped\n } finally {\n if (timeoutHandle !== null) {\n clearTimeout(timeoutHandle)\n }\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAO7B,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB,gCAAgC;AAC9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBP,MAAM,+BAA+B;AAErC,SAAS,4BAAoC;AAC3C,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,SAAS,OAAO,SAAS,UAAU,EAAE;AAC3C,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAAG;AAC3C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,aAAa,YAAiC,WAA0B;AAC/E,QAAM,eAAe,oBAAoB,UAAU;AACnD,SAAO,IAAI;AAAA,IACT,GAAG,aAAa,IAAI,4BAA4B,SAAS,aAAa,aAAa,cAAc;AAAA,EACnG;AACF;AAEO,MAAM,iBAAiB;AAAA,EAI5B,YAA6B,OAAgC,CAAC,GAAG;AAApC;AAF7B,SAAQ,cAAwD,oBAAI,IAAI;AAGtE,QAAI,KAAK,QAAQ;AACf,WAAK,SAAS,KAAK;AAAA,IACrB,OAAO;AACL,WAAK,SAAS;AAAA,QACZ,YAAY;AAAA,QACZ,OAAO,KAAK,SAAS,yBAAyB;AAAA,QAC9C,WAAW,yBAAyB;AAAA,QACpC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,aAAa,QAAuC;AAClD,QACE,OAAO,eAAe,KAAK,OAAO,cAClC,OAAO,UAAU,KAAK,OAAO,SAC7B,OAAO,cAAc,KAAK,OAAO,aACjC,OAAO,yBAAyB,KAAK,OAAO,wBAC5C,OAAO,YAAY,KAAK,OAAO,SAC/B;AACA;AAAA,IACF;AACA,SAAK,SAAS;AACd,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA,EAEA,IAAI,gBAAyC;AAC3C,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,OAAO,wBAAwB,KAAK,OAAO;AAAA,EACzD;AAAA,EAEA,IAAI,YAAqB;AACvB,WAAO,KAAK,qBAAqB,KAAK,OAAO,UAAU;AAAA,EACzD;AAAA,EAEQ,qBAAqB,YAA0C;AACrE,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAO,QAAQ,KAAK,KAAK,UAAU,QAAQ,IAAI,cAAc;AAAA,MAC/D,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,4BAA4B;AAAA,MACzD,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,eAAe;AAAA,MAC5C,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,cAAc;AAAA,MAC3C,KAAK;AACH,eAAO,QAAQ,QAAQ,IAAI,qBAAqB,QAAQ,IAAI,qBAAqB;AAAA,MACnF,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEQ,UAAU,YAAiD;AACjE,UAAM,SAAS,KAAK,YAAY,IAAI,UAAU;AAC9C,QAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAEA,QAAI;AACJ,YAAQ,YAAY;AAAA,MAClB,KAAK,UAAU;AACb,cAAM,SAAS,KAAK,KAAK,UAAU,QAAQ,IAAI;AAC/C,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,gEAAgE;AAAA,QAClF;AACA,iBAAS,aAAa,EAAE,OAAO,CAAC;AAChC;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,8EAA8E;AAAA,QAChG;AACA,iBAAS,yBAAyB,EAAE,OAAO,CAAC;AAC5C;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,iEAAiE;AAAA,QACnF;AACA,iBAAS,cAAc,EAAE,OAAO,CAAC;AACjC;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,SAAS,QAAQ,IAAI;AAC3B,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,MAAM,gEAAgE;AAAA,QAClF;AACA,iBAAS,aAAa,EAAE,OAAO,CAAC;AAChC;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,cAAM,cAAc,QAAQ,IAAI;AAChC,cAAM,kBAAkB,QAAQ,IAAI;AACpC,YAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,gBAAM,IAAI,MAAM,6FAA6F;AAAA,QAC/G;AACA,iBAAS,oBAAoB;AAAA,UAC3B;AAAA,UACA;AAAA,UACA,QAAQ,QAAQ,IAAI,cAAc;AAAA,QACpC,CAAC;AACD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,UAAU,KAAK,OAAO,WAAW,QAAQ,IAAI,mBAAmB;AACtE,YAAI;AACF,kCAAwB,OAAO;AAAA,QACjC,SAAS,KAAK;AACZ,cAAI,eAAe,0BAA0B;AAC3C,kBAAM,IAAI;AAAA,cACR,gDAAgD,IAAI,MAAM;AAAA,YAC5D;AAAA,UACF;AACA,gBAAM;AAAA,QACR;AACA,iBAAS,aAAa,EAAE,SAAS,OAAO,gBAAgB,CAAC;AACzD;AAAA,MACF;AAAA,MACA;AACE,cAAM,IAAI,MAAM,wCAAwC,UAAU,EAAE;AAAA,IACxE;AAEA,SAAK,YAAY,IAAI,YAAY,MAAM;AACvC,WAAO;AAAA,EACT;AAAA,EAEQ,oBAAoB;AAC1B,UAAM,SAAS,KAAK,UAAU,KAAK,OAAO,UAAU;AACpD,UAAM,EAAE,YAAY,OAAO,qBAAqB,IAAI,KAAK;AAEzD,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAQ,OAA2C,UAAU,KAAK;AAAA,MACpE,KAAK;AACH,eAAQ,OAAuD,mBAAmB,KAAK;AAAA,MACzF,KAAK;AACH,eAAQ,OAA4C,mBAAmB,KAAK;AAAA,MAC9E,KAAK;AACH,eAAQ,OAA2C,mBAAmB,KAAK;AAAA,MAC7E,KAAK;AACH,eAAQ,OAAkD,UAAU,KAAK;AAAA,MAC3E,KAAK;AACH,eAAQ,OAAwB,UAAU,KAAK;AAAA,MACjD;AACE,cAAM,IAAI,MAAM,wCAAwC,UAAU,EAAE;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,qBAAkD;AACxD,UAAM,EAAE,YAAY,sBAAsB,MAAM,IAAI,KAAK;AAEzD,QAAI,CAAC,sBAAsB;AACzB,UAAI,eAAe,UAAU;AAC3B,eAAO,EAAE,QAAQ,EAAE,WAAW,kBAAkB,EAAE;AAAA,MACpD;AACA,aAAO;AAAA,IACT;AAEA,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,YAAI,UAAU,4BAA4B,UAAU,0BAA0B;AAC9E,iBAAO,EAAE,QAAQ,EAAE,YAAY,qBAAqB,EAAE;AAAA,QACtD;AACA,eAAO;AAAA,MACT,KAAK;AACH,eAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE;AAAA,MAC5C,KAAK;AACH,eAAO,EAAE,SAAS,EAAE,YAAY,qBAAqB,EAAE;AAAA,MACzD,KAAK;AACH,eAAO,EAAE,QAAQ,EAAE,WAAW,kBAAkB,EAAE;AAAA,MACpD;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,OAA6C;AACjE,UAAM,SAAS,MAAM,QAAQ,KAAK,IAC9B,MAAM,IAAI,CAAC,SAAS,OAAO,QAAQ,EAAE,CAAC,EAAE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAAE,KAAK,MAAM,IACrF,OAAO,SAAS,EAAE;AACtB,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AAEA,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,eAAe,oBAAoB,KAAK,OAAO,UAAU;AAC/D,YAAM,IAAI,MAAM,+BAA+B,aAAa,IAAI,2BAA2B,aAAa,cAAc,wBAAwB;AAAA,IAChJ;AAEA,UAAM,QAAQ,KAAK,kBAAkB;AACrC,UAAM,kBAAkB,KAAK,mBAAmB;AAChD,UAAM,YAAY,0BAA0B;AAE5C,UAAM,kBAAkB,IAAI,gBAAgB;AAC5C,QAAI,gBAAsD;AAC1D,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK;AAAA,QAChC,MAAM;AAAA,UACJ;AAAA,UACA,OAAO;AAAA,UACP,aAAa,gBAAgB;AAAA,UAC7B,GAAI,mBAAmB,EAAE,gBAAgB;AAAA,QAC3C,CAAC;AAAA,QACD,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,0BAAgB;AAAA,YACd,MAAM;AAKJ,8BAAgB,MAAM;AACtB,qBAAO,aAAa,KAAK,OAAO,YAAY,SAAS,CAAC;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AACD,YAAM,MAAM,MAAM,QAAQ,OAAO,SAAS,IACtC,OAAO,YACP,MAAM,KAAK,OAAO,SAA8B;AACpD,aAAO,IAAI,IAAI,CAAC,MAAM,OAAO,SAAS,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;AAAA,IAC1D,SAAS,KAAc;AACrB,YAAM,QAAQ;AACd,YAAM,kBACJ,OAAO,cAAc,OAAO,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU;AACpF,YAAM,SACJ,OAAO,oBAAoB,WACvB,OAAO,SAAS,eAAe,IAAI,kBAAkB,SACrD,OAAO,oBAAoB,WACzB,OAAO,SAAS,iBAAiB,EAAE,IACnC;AACR,YAAM,WAAW,OAAO,MAAM,SAAS,OAAO,MAAM,SAAS,OAAO,UAAU,MAAM;AACpF,YAAM,aAAa,UAAU,WAAW,OAAO,UAAU,MAAM;AAC/D,YAAM,UAAU,OAAO,UAAU,SAAS,WAAW,SAAS,OAAO;AACrE,YAAM,aAAa,OAAO,eAAe,WACrC,aACC,OAAO,OAAO,YAAY,WAAW,MAAM,UAAU;AAE1D,YAAM,eAAe,oBAAoB,KAAK,OAAO,UAAU;AAC/D,UAAI;AACJ,cAAQ,SAAS;AAAA,QACf,KAAK;AACH,qBAAW,GAAG,aAAa,IAAI;AAC/B;AAAA,QACF,KAAK;AACH,qBAAW,WAAW,aAAa,IAAI;AACvC;AAAA,QACF,KAAK;AACH,qBAAW,GAAG,aAAa,IAAI;AAC/B;AAAA,QACF;AACE,qBAAW,WAAW,WAAW,qBAAqB,IAClD,WAAW,MAAM,sBAAsB,MAAM,IAC7C,WAAW,SAAS,UAAU,IAC9B,aACA,WAAW,SAAS,aAAa,cAAc,IAC7C,aACA,GAAG,UAAU,WAAW,aAAa,cAAc;AAAA,MAC7D;AACA,YAAM,UAAU,IAAI,MAAM,sBAAsB,QAAQ,EAAE;AAC1D,UAAI,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,GAAG;AACzD,cAAM,mBAAmB,WAAW,OAAO,WAAW,MAAM,MAAM;AAClE,YAAI,oBAAoB,OAAO,mBAAmB,KAAK;AACrD,kBAAQ,SAAS;AAAA,QACnB;AAAA,MACF;AACA,UAAI,SAAS;AACX,gBAAQ,OAAO;AAAA,MACjB;AACA,cAAQ,QAAQ;AAChB,YAAM;AAAA,IACR,UAAE;AACA,UAAI,kBAAkB,MAAM;AAC1B,qBAAa,aAAa;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/search",
3
- "version": "0.6.6",
3
+ "version": "0.6.7-develop.6553.1.2ec7246e04",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -127,9 +127,9 @@
127
127
  "zod": "^4.4.3"
128
128
  },
129
129
  "peerDependencies": {
130
- "@open-mercato/core": "0.6.6",
131
- "@open-mercato/queue": "0.6.6",
132
- "@open-mercato/shared": "0.6.6"
130
+ "@open-mercato/core": "0.6.7-develop.6553.1.2ec7246e04",
131
+ "@open-mercato/queue": "0.6.7-develop.6553.1.2ec7246e04",
132
+ "@open-mercato/shared": "0.6.7-develop.6553.1.2ec7246e04"
133
133
  },
134
134
  "devDependencies": {
135
135
  "@types/jest": "^30.0.0",
@@ -144,5 +144,6 @@
144
144
  "type": "git",
145
145
  "url": "https://github.com/open-mercato/open-mercato",
146
146
  "directory": "packages/search"
147
- }
147
+ },
148
+ "stableVersion": "0.6.6"
148
149
  }
@@ -2,10 +2,16 @@ jest.mock('ai', () => ({
2
2
  embed: jest.fn(),
3
3
  }))
4
4
 
5
+ jest.mock('ai-sdk-ollama', () => ({
6
+ createOllama: jest.fn(() => ({ embedding: jest.fn(() => ({})) })),
7
+ }))
8
+
5
9
  import { embed } from 'ai'
10
+ import { createOllama } from 'ai-sdk-ollama'
6
11
  import { EmbeddingService } from '../vector/services/embedding'
7
12
 
8
13
  const mockedEmbed = jest.mocked(embed)
14
+ const mockedCreateOllama = jest.mocked(createOllama)
9
15
 
10
16
  describe('EmbeddingService', () => {
11
17
  const originalEnv = { ...process.env }
@@ -80,6 +86,28 @@ describe('EmbeddingService', () => {
80
86
  await expect(service.createEmbedding('test input')).resolves.toEqual([0.25, 0.5, 0.75])
81
87
  })
82
88
 
89
+ it('injects the guarded fetch transport into the Ollama SDK client', async () => {
90
+ process.env.NODE_ENV = 'production'
91
+ process.env.VECTOR_EMBEDDING_TIMEOUT_MS = '100'
92
+ mockedEmbed.mockResolvedValue({ embedding: [0.25] } as Awaited<ReturnType<typeof embed>>)
93
+
94
+ const service = new EmbeddingService({
95
+ config: {
96
+ providerId: 'ollama',
97
+ model: 'nomic-embed-text',
98
+ dimension: 768,
99
+ baseUrl: 'https://ollama.example.com',
100
+ updatedAt: new Date().toISOString(),
101
+ },
102
+ })
103
+
104
+ await expect(service.createEmbedding('test input')).resolves.toEqual([0.25])
105
+ expect(mockedCreateOllama).toHaveBeenCalledWith({
106
+ baseURL: 'https://ollama.example.com',
107
+ fetch: expect.any(Function),
108
+ })
109
+ })
110
+
83
111
  it('rejects persisted Ollama baseUrl pointing at a private IP in production', async () => {
84
112
  process.env.NODE_ENV = 'production'
85
113
  process.env.VECTOR_EMBEDDING_TIMEOUT_MS = '100'
@@ -20,6 +20,7 @@ const mockSaveEmbeddingConfig = jest.fn()
20
20
  const mockGetConfiguredProviders = jest.fn()
21
21
  const mockDetectConfigChange = jest.fn()
22
22
  const mockGetEffectiveDimension = jest.fn()
23
+ const mockCheckAvailability = jest.fn()
23
24
  jest.mock('../../../lib/embedding-config', () => ({
24
25
  resolveEmbeddingConfig: (...args: unknown[]) => mockResolveEmbeddingConfig(...args),
25
26
  resolveEmbeddingConfigResult: (...args: unknown[]) => mockResolveEmbeddingConfigResult(...args),
@@ -66,6 +67,9 @@ describe('POST /api/search/embeddings — Ollama baseUrl SSRF guard', () => {
66
67
  mockCreateRequestContainer.mockResolvedValue({
67
68
  resolve: jest.fn((name: string) => {
68
69
  if (name === 'moduleConfigService') return moduleConfigService
70
+ if (name === 'embeddingProviderProbe') {
71
+ return { checkAvailability: mockCheckAvailability }
72
+ }
69
73
  if (name === 'vectorDrivers') return []
70
74
  throw new Error(`unexpected resolve(${name})`)
71
75
  }),
@@ -76,6 +80,7 @@ describe('POST /api/search/embeddings — Ollama baseUrl SSRF guard', () => {
76
80
  mockResolveEmbeddingConfigResult.mockResolvedValue({ config: null, source: 'env' })
77
81
  mockGetConfiguredProviders.mockReturnValue(['ollama', 'openai'])
78
82
  mockGetEffectiveDimension.mockReturnValue(768)
83
+ mockCheckAvailability.mockResolvedValue({ available: true })
79
84
  mockDetectConfigChange.mockImplementation((_existing: unknown, next: unknown) => ({
80
85
  newConfig: next,
81
86
  requiresReindex: false,
@@ -124,6 +129,10 @@ describe('POST /api/search/embeddings — Ollama baseUrl SSRF guard', () => {
124
129
 
125
130
  expect(res.status).toBe(200)
126
131
  expect(mockSaveEmbeddingConfig).toHaveBeenCalledTimes(1)
132
+ expect(mockCheckAvailability).toHaveBeenCalledWith('ollama', {
133
+ force: true,
134
+ baseUrl: 'http://ollama.internal.example.com:11434',
135
+ })
127
136
  const savedConfig = mockSaveEmbeddingConfig.mock.calls[0][1] as { baseUrl?: string }
128
137
  expect(savedConfig.baseUrl).toBe('http://ollama.internal.example.com:11434')
129
138
  })
@@ -271,7 +271,12 @@ export async function POST(req: Request) {
271
271
  // Save-time availability guard: never persist a provider the probe reports unreachable.
272
272
  const probe = resolveProbe(container)
273
273
  if (probe) {
274
- const availability = await probe.checkAvailability(newConfig.providerId)
274
+ const availability = await probe.checkAvailability(
275
+ newConfig.providerId,
276
+ newConfig.providerId === 'ollama'
277
+ ? { force: true, baseUrl: newConfig.baseUrl }
278
+ : undefined,
279
+ )
275
280
  if (!availability.available) {
276
281
  return NextResponse.json(
277
282
  {
@@ -102,6 +102,29 @@ describe('createEmbeddingProviderProbe', () => {
102
102
  expect(spy).toHaveBeenCalledTimes(2)
103
103
  })
104
104
 
105
+ it('probes a candidate Ollama base URL instead of the environment default', async () => {
106
+ const previous = process.env.OLLAMA_BASE_URL
107
+ process.env.OLLAMA_BASE_URL = 'http://localhost:11434'
108
+ const spy = mockFetch(async () => okTags(1))
109
+ const { container } = createContainerWithCache()
110
+ const probe = createEmbeddingProviderProbe(container)
111
+
112
+ try {
113
+ await probe.checkAvailability('ollama', {
114
+ force: true,
115
+ baseUrl: 'http://127.0.0.1:22434',
116
+ })
117
+ } finally {
118
+ if (previous === undefined) delete process.env.OLLAMA_BASE_URL
119
+ else process.env.OLLAMA_BASE_URL = previous
120
+ }
121
+
122
+ expect(spy).toHaveBeenCalledWith(
123
+ 'http://127.0.0.1:22434/api/tags',
124
+ expect.objectContaining({ method: 'GET' }),
125
+ )
126
+ })
127
+
105
128
  it('gates key-based providers on env-key presence', async () => {
106
129
  const { container } = createContainerWithCache()
107
130
  const probe = createEmbeddingProviderProbe(container)
@@ -2,6 +2,7 @@ import type { CacheStrategy } from '@open-mercato/cache'
2
2
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
3
  import type { EmbeddingProviderId } from '../../../vector'
4
4
  import { EMBEDDING_PROVIDERS } from '../../../vector'
5
+ import { safeOllamaFetch } from '../../../vector/lib/ollama-url-safety'
5
6
 
6
7
  const CACHE_VERSION = 'v1'
7
8
  const CACHE_TTL_MS = 30_000
@@ -18,7 +19,10 @@ export type ProviderAvailabilityEntry = ProviderAvailability & {
18
19
  }
19
20
 
20
21
  export type EmbeddingProviderProbe = {
21
- checkAvailability(providerId: EmbeddingProviderId, options?: { force?: boolean }): Promise<ProviderAvailability>
22
+ checkAvailability(
23
+ providerId: EmbeddingProviderId,
24
+ options?: { force?: boolean; baseUrl?: string },
25
+ ): Promise<ProviderAvailability>
22
26
  }
23
27
 
24
28
  const ALL_PROVIDERS: EmbeddingProviderId[] = ['openai', 'google', 'mistral', 'cohere', 'bedrock', 'ollama']
@@ -80,7 +84,7 @@ export async function probeOllama(baseUrl: string): Promise<ProviderAvailability
80
84
  const controller = new AbortController()
81
85
  const timer = setTimeout(() => controller.abort(), OLLAMA_PROBE_TIMEOUT_MS)
82
86
  try {
83
- const response = await fetch(`${baseUrl.replace(/\/$/, '')}/api/tags`, {
87
+ const response = await safeOllamaFetch(`${baseUrl.replace(/\/$/, '')}/api/tags`, {
84
88
  method: 'GET',
85
89
  signal: controller.signal,
86
90
  })
@@ -104,10 +108,13 @@ export async function probeOllama(baseUrl: string): Promise<ProviderAvailability
104
108
  }
105
109
  }
106
110
 
107
- async function computeAvailability(providerId: EmbeddingProviderId): Promise<ProviderAvailability> {
111
+ async function computeAvailability(
112
+ providerId: EmbeddingProviderId,
113
+ options?: { baseUrl?: string },
114
+ ): Promise<ProviderAvailability> {
108
115
  try {
109
116
  if (providerId === 'ollama') {
110
- return await probeOllama(ollamaBaseUrl())
117
+ return await probeOllama(options?.baseUrl?.trim() || ollamaBaseUrl())
111
118
  }
112
119
  return keyPresence(providerId)
113
120
  } catch (error) {
@@ -119,11 +126,12 @@ async function computeAvailability(providerId: EmbeddingProviderId): Promise<Pro
119
126
  export function createEmbeddingProviderProbe(container: AppContainer): EmbeddingProviderProbe {
120
127
  const checkAvailability = async (
121
128
  providerId: EmbeddingProviderId,
122
- options?: { force?: boolean },
129
+ options?: { force?: boolean; baseUrl?: string },
123
130
  ): Promise<ProviderAvailability> => {
124
131
  const cache = resolveCache(container)
125
132
  const key = cacheKey(providerId)
126
- if (!options?.force && cache) {
133
+ const probesCandidateUrl = providerId === 'ollama' && options?.baseUrl !== undefined
134
+ if (!options?.force && !probesCandidateUrl && cache) {
127
135
  try {
128
136
  const cached = await cache.get(key)
129
137
  if (cached && typeof cached === 'object' && 'available' in cached) {
@@ -131,8 +139,8 @@ export function createEmbeddingProviderProbe(container: AppContainer): Embedding
131
139
  }
132
140
  } catch {}
133
141
  }
134
- const result = await computeAvailability(providerId)
135
- if (cache) {
142
+ const result = await computeAvailability(providerId, options)
143
+ if (cache && !probesCandidateUrl) {
136
144
  try {
137
145
  await cache.set(key, result, { ttl: CACHE_TTL_MS })
138
146
  } catch {}
@@ -3,6 +3,7 @@ import {
3
3
  UnsafeOllamaBaseUrlError,
4
4
  getOllamaBaseUrlAllowlist,
5
5
  isAllowPrivateOllamaBaseUrlEnabled,
6
+ safeOllamaFetch,
6
7
  } from '../ollama-url-safety'
7
8
 
8
9
  const ORIGINAL_ENV = { ...process.env }
@@ -242,6 +243,83 @@ describe('assertSafeOllamaBaseUrl', () => {
242
243
  })
243
244
  })
244
245
 
246
+ describe('safeOllamaFetch', () => {
247
+ beforeEach(() => {
248
+ process.env = { ...ORIGINAL_ENV, NODE_ENV: 'production' }
249
+ delete process.env.OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST
250
+ delete process.env.OM_SEARCH_OLLAMA_ALLOW_PRIVATE
251
+ })
252
+
253
+ afterAll(() => {
254
+ process.env = ORIGINAL_ENV
255
+ })
256
+
257
+ it('rejects private DNS resolution before opening a connection', async () => {
258
+ const fetchImpl = jest.fn() as jest.MockedFunction<typeof fetch>
259
+
260
+ await expect(
261
+ safeOllamaFetch('https://ollama.example.com/api/embed', {}, {
262
+ lookupHost: async () => [{ address: '10.0.0.7', family: 4 }],
263
+ fetchImpl,
264
+ }),
265
+ ).rejects.toMatchObject({ reason: 'private_ip_resolved' })
266
+ expect(fetchImpl).not.toHaveBeenCalled()
267
+ })
268
+
269
+ it('rejects mixed public and private DNS answers', async () => {
270
+ const fetchImpl = jest.fn() as jest.MockedFunction<typeof fetch>
271
+
272
+ await expect(
273
+ safeOllamaFetch('https://ollama.example.com/api/embed', {}, {
274
+ lookupHost: async () => [
275
+ { address: '93.184.216.34', family: 4 },
276
+ { address: '127.0.0.1', family: 4 },
277
+ ],
278
+ fetchImpl,
279
+ }),
280
+ ).rejects.toMatchObject({ reason: 'private_ip_resolved' })
281
+ expect(fetchImpl).not.toHaveBeenCalled()
282
+ })
283
+
284
+ it('revalidates redirects and rejects a public-to-private hop', async () => {
285
+ const fetchImpl = jest
286
+ .fn()
287
+ .mockResolvedValueOnce(
288
+ new Response(null, {
289
+ status: 302,
290
+ headers: { location: 'http://169.254.169.254/latest/meta-data' },
291
+ }),
292
+ ) as jest.MockedFunction<typeof fetch>
293
+
294
+ await expect(
295
+ safeOllamaFetch('https://ollama.example.com/api/embed', {}, {
296
+ lookupHost: async () => [{ address: '93.184.216.34', family: 4 }],
297
+ fetchImpl,
298
+ }),
299
+ ).rejects.toMatchObject({ reason: 'private_ip_literal' })
300
+ expect(fetchImpl).toHaveBeenCalledTimes(1)
301
+ expect(fetchImpl).toHaveBeenCalledWith(
302
+ 'https://ollama.example.com/api/embed',
303
+ expect.objectContaining({ redirect: 'manual' }),
304
+ )
305
+ })
306
+
307
+ it('preserves explicitly allowlisted internal Ollama hosts', async () => {
308
+ process.env.OM_SEARCH_OLLAMA_BASE_URL_ALLOWLIST = 'ollama.internal.example.com:11434'
309
+ const fetchImpl = jest
310
+ .fn()
311
+ .mockResolvedValue(new Response('{}', { status: 200 })) as jest.MockedFunction<typeof fetch>
312
+
313
+ await expect(
314
+ safeOllamaFetch('http://ollama.internal.example.com:11434/api/embed', {}, {
315
+ lookupHost: async () => [{ address: '10.0.0.7', family: 4 }],
316
+ fetchImpl,
317
+ }),
318
+ ).resolves.toMatchObject({ status: 200 })
319
+ expect(fetchImpl).toHaveBeenCalledTimes(1)
320
+ })
321
+ })
322
+
245
323
  describe('getOllamaBaseUrlAllowlist', () => {
246
324
  beforeEach(() => {
247
325
  process.env = { ...ORIGINAL_ENV }
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  assertStaticallySafeOutboundUrl,
3
+ safeOutboundFetch,
4
+ type HostLookup,
5
+ type SafeOutboundFetchOptions,
3
6
  type UrlSafetyReason,
4
7
  } from '@open-mercato/shared/lib/url-safety'
5
8
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
6
9
 
7
10
  const SUBJECT = 'Ollama base URL'
11
+ const MAX_OLLAMA_REDIRECTS = 5
8
12
 
9
13
  export class UnsafeOllamaBaseUrlError extends Error {
10
14
  public readonly reason: string
@@ -38,18 +42,96 @@ export function assertSafeOllamaBaseUrl(rawUrl: string): void {
38
42
  throw new UnsafeOllamaBaseUrlError('missing_host', `${SUBJECT} is required`)
39
43
  }
40
44
 
41
- const allowPrivate =
42
- allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) ||
43
- isAllowPrivateOllamaBaseUrlEnabled() ||
44
- (process.env.NODE_ENV !== 'production' && isLoopbackOnlyUrl(rawUrl))
45
-
46
45
  assertStaticallySafeOutboundUrl(rawUrl, {
47
46
  errorFactory: ollamaErrorFactory,
48
47
  subject: SUBJECT,
49
- allowPrivate,
48
+ allowPrivate: shouldAllowPrivateOllamaUrl(rawUrl),
50
49
  })
51
50
  }
52
51
 
52
+ export type SafeOllamaFetchDeps = {
53
+ lookupHost?: HostLookup
54
+ fetchImpl?: SafeOutboundFetchOptions['fetchImpl']
55
+ maxRedirects?: number
56
+ }
57
+
58
+ export async function safeOllamaFetch(
59
+ input: RequestInfo | URL,
60
+ init: RequestInit = {},
61
+ deps: SafeOllamaFetchDeps = {},
62
+ ): Promise<Response> {
63
+ const request = new Request(input, init)
64
+ const maxRedirects = deps.maxRedirects ?? MAX_OLLAMA_REDIRECTS
65
+ let currentUrl = request.url
66
+ let currentMethod = request.method
67
+ let currentHeaders = new Headers(request.headers)
68
+ let currentBody =
69
+ currentMethod === 'GET' || currentMethod === 'HEAD'
70
+ ? undefined
71
+ : await request.clone().arrayBuffer()
72
+
73
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
74
+ const response = await safeOutboundFetch(
75
+ currentUrl,
76
+ {
77
+ method: currentMethod,
78
+ headers: currentHeaders,
79
+ body: currentBody,
80
+ signal: request.signal,
81
+ redirect: 'manual',
82
+ },
83
+ {
84
+ errorFactory: ollamaErrorFactory,
85
+ subject: SUBJECT,
86
+ allowPrivate: shouldAllowPrivateOllamaUrl(currentUrl),
87
+ lookupHost: deps.lookupHost,
88
+ fetchImpl: deps.fetchImpl,
89
+ },
90
+ )
91
+
92
+ if (![301, 302, 303, 307, 308].includes(response.status)) {
93
+ return response
94
+ }
95
+ const location = response.headers.get('location')
96
+ if (!location) return response
97
+ if (redirectCount === maxRedirects) {
98
+ throw new UnsafeOllamaBaseUrlError(
99
+ 'too_many_redirects',
100
+ `${SUBJECT} exceeded ${maxRedirects} redirects`,
101
+ )
102
+ }
103
+
104
+ const nextUrl = new URL(location, currentUrl)
105
+ if (nextUrl.origin !== new URL(currentUrl).origin) {
106
+ currentHeaders = new Headers(currentHeaders)
107
+ currentHeaders.delete('authorization')
108
+ currentHeaders.delete('cookie')
109
+ currentHeaders.delete('proxy-authorization')
110
+ }
111
+ if (
112
+ (response.status === 303 && currentMethod !== 'GET' && currentMethod !== 'HEAD') ||
113
+ ((response.status === 301 || response.status === 302) && currentMethod === 'POST')
114
+ ) {
115
+ currentMethod = 'GET'
116
+ currentBody = undefined
117
+ currentHeaders.delete('content-length')
118
+ currentHeaders.delete('content-type')
119
+ }
120
+ await response.body?.cancel()
121
+ currentUrl = nextUrl.toString()
122
+ }
123
+
124
+ throw new UnsafeOllamaBaseUrlError('too_many_redirects')
125
+ }
126
+
127
+ function shouldAllowPrivateOllamaUrl(rawUrl: string): boolean {
128
+ return (
129
+ allowlistMatches(rawUrl, getOllamaBaseUrlAllowlist()) ||
130
+ isAllowPrivateOllamaBaseUrlEnabled() ||
131
+ (process.env.NODE_ENV !== 'production' && isLoopbackOnlyUrl(rawUrl))
132
+ )
133
+ }
134
+
53
135
  function allowlistMatches(rawUrl: string, allowlist: ReadonlySet<string>): boolean {
54
136
  if (allowlist.size === 0) return false
55
137
  let parsed: URL
@@ -14,7 +14,11 @@ import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'
14
14
  import { createOllama } from 'ai-sdk-ollama'
15
15
  import type { EmbeddingProviderId, EmbeddingProviderConfig } from '../types'
16
16
  import { EMBEDDING_PROVIDERS, DEFAULT_EMBEDDING_CONFIG } from '../types'
17
- import { assertSafeOllamaBaseUrl, UnsafeOllamaBaseUrlError } from '../lib/ollama-url-safety'
17
+ import {
18
+ assertSafeOllamaBaseUrl,
19
+ safeOllamaFetch,
20
+ UnsafeOllamaBaseUrlError,
21
+ } from '../lib/ollama-url-safety'
18
22
 
19
23
  export type EmbeddingServiceOptions = {
20
24
  apiKey?: string
@@ -177,7 +181,7 @@ export class EmbeddingService {
177
181
  }
178
182
  throw err
179
183
  }
180
- client = createOllama({ baseURL })
184
+ client = createOllama({ baseURL, fetch: safeOllamaFetch })
181
185
  break
182
186
  }
183
187
  default: