/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
- 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.
- 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.
/olla/openai/...requests never reach lemonade or dmr backends (compatibility omission + no path translation)Summary
Backends whose profile uses a non-
/v1base path -- lemonade (/api/v1/...) and dmr (/engines/v1/...) -- declareapi.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) aliasesopenai-compatibleto a hardcoded set:lemonadeanddmraren't listed, even though both declareapi.openai_compatible: true. For an openai-format request the path inspector setsSupportedBy=[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 offilterEndpointsByProfilebefore 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
openaito 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), thenendpoint.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
api.openai_compatibleflag rather than the hardcoded list inIsCompatibleWith(the same sourcecreateProviderProfilealready consults), so new openai-compatible backends are picked up automatically.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.