Skip to content

/olla/openai requests never reach lemonade or dmr backends (compatibility omission + no path translation) #162

@matthewjhunter

Description

@matthewjhunter

/olla/openai/... requests never reach lemonade or dmr backends (compatibility omission + no path translation)

Summary

Backends whose profile uses a non-/v1 base path -- lemonade (/api/v1/...) and dmr (/engines/v1/...) -- declare api.openai_compatible: true, but requests through the generic /olla/openai/... path are never routed to them. Two separate gaps combine to cause this. (Every other openai-compatible backend uses /v1/... and routes fine, so this is specific to lemonade and dmr.)

This is the companion to #160 / #161. With that state fix applied, a downloaded lemonade model is routable via the native /olla/lemonade/... path, but still unreachable via /olla/openai/....

Gap 1 -- compatibility check omits lemonade/dmr

RequestProfile.IsCompatibleWith (internal/core/domain/routing.go) aliases openai-compatible to a hardcoded set:

if supported == ProfileOpenAICompatible && (endpointType == ProfileOllama || endpointType == ProfileLmStudio || endpointType == ProfileOpenAI) {
    return true
}

lemonade and dmr aren't listed, even though both declare api.openai_compatible: true. For an openai-format request the path inspector sets SupportedBy=[openai-compatible, ...]; ollama/lmstudio/vllm/sglang/etc. match (they declare /v1/... paths so they're added directly, or are covered by the alias), but lemonade/dmr are filtered out at stage 1 of filterEndpointsByProfile before routing runs.

Observed: same binary, same downloaded model on a lemonade endpoint --

  • POST /olla/lemonade/api/v1/chat/completions -> 200, routed to the lemonade endpoint.
  • POST /olla/openai/v1/chat/completions -> 404 "No openai endpoints available"; log: WARN "Model only on unhealthy endpoints" model_endpoints=1 healthy_endpoints=2 (the lemonade endpoint was excluded from the candidate set, leaving only the ollama boxes).

This mirrors #148/#151, which had to add openai to that same line -- a hardcoded allowlist that keeps needing entries.

Gap 2 -- no per-backend path translation

Even with Gap 1 fixed, the request would still fail. The URL builder (internal/adapter/proxy/common/url_builder.go) forwards the stripped path verbatim: targetPath = StripPrefix(path, proxyPrefix), then endpoint.URL.ResolveReference(targetPath). So /olla/openai/v1/chat/completions -> <lemonade>/v1/chat/completions, but lemonade serves /api/v1/chat/completions -> backend 404. The native /olla/lemonade/... path works precisely because it preserves /api/v1/....

Routing a generic openai request to a backend whose base path differs requires translating /v1/... to that backend's actual path (e.g. lemonade /api/v1/..., dmr /engines/v1/...), using the path each profile already declares.

Suggested direction

  1. Drive openai-compatibility off each profile's declared api.openai_compatible flag rather than the hardcoded list in IsCompatibleWith (the same source createProviderProfile already consults), so new openai-compatible backends are picked up automatically.
  2. When proxying a generic openai path to a backend with a different base path, remap to that backend profile's corresponding path before forwarding.

Happy to contribute a PR if you'd like, but Gap 2 touches the proxy core so I wanted to align on the approach first. For reference, #161 (the state fix) is independent of this and stands on its own for the native lemonade path.

Metadata

Metadata

Assignees

Labels

investigatingWe're actively investigating the issue.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions