_embedWithFallback() ignores isOllamaNative, always tries /api/embed first even when known to be non-Ollama

Author: horsleybCreated Aug 27, 2026Updated Sep 13, 2026
Labelsreleased on @rc

Summary

OllamaService._embedWithFallback() (in app/app/services/ollama_service.js) always tries the native Ollama /api/embed endpoint first and only falls back to the OpenAI-compatible openai.embeddings.create() (/v1/embeddings) call in its catch block:

javascript
async _embedWithFallback(model, input) {
    try {
        const response = await axios.post(`${this.baseUrl}/api/embed`, {
            model,
            input,
            truncate: true,
            options: { num_ctx: 8192 },
        }, { timeout: 60000 });
        ...
    }
    catch (err) {
        if (OllamaService_1.isContextLengthError(err))
            throw err;
        logger.warn('[OllamaService] /api/embed failed, falling back to /v1/embeddings: %s', ...);
        const results = await this.openai.embeddings.create({ ... });
        ...
    }
}

This never checks this.isOllamaNative, even though that flag already exists and is populated by getModels() (which probes /api/tags vs. falling back to /v1/models), and is already used elsewhere in this same file (e.g. _doDownloadModel skips /api/pull when isOllamaNative === false).

Impact

When NOMAD's "AI Assistant"/RAG embedding provider is a non-Ollama, OpenAI-compatible backend (ai.remoteOllamaUrl pointed at a proxy/router that only speaks the OpenAI embeddings API, not Ollama's native API), every single embedding call pays one guaranteed-failing round trip to /api/embed before falling back to /v1/embeddings. For a bulk file-embeddings indexing job processing a large corpus (in our case, several million chunks into Qdrant), this doubles the request volume and log noise against the embedding backend for the lifetime of the job, with zero benefit.

Suggested fix

Check this.isOllamaNative before attempting /api/embed, mirroring the pattern already used in _doDownloadModel:

javascript
async _embedWithFallback(model, input) {
    if (this.isOllamaNative !== false) {
        try {
            const response = await axios.post(`${this.baseUrl}/api/embed`, { ... });
            ...
            return { embeddings: response.data.embeddings };
        } catch (err) {
            if (OllamaService_1.isContextLengthError(err)) throw err;
            logger.warn('[OllamaService] /api/embed failed, falling back to /v1/embeddings: %s', ...);
        }
    }
    const results = await this.openai.embeddings.create({ ... });
    return { embeddings: results.data.map((e) => e.embedding) };
}

That way the native attempt is skipped entirely once isOllamaNative is known to be false, instead of being retried (and failing) on every single embed call.

Environment

  • Image: ghcr.io/crosstalk-solutions/project-nomad@sha256:832f7b1516593bc6a83f5554c98cf753a7b96aaa7e3d991b538ac4e5edc01146 (:latest as of 2026-08-07)
  • Reproduced via ai.remoteOllamaUrl pointed at a small local proxy that translates NOMAD's Ollama-style calls to an OpenAI-compatible embeddings backend — every embed request logged one /api/embed failure followed immediately by a successful /v1/embeddings call.

Worked around it on our end for now with a proxy-side shim that answers /api/embed directly, but figured this was worth reporting since isOllamaNative is clearly meant to prevent exactly this.

Source: Crosstalk-Solutions/project-nomad