Embedding driver: support OpenAI-compatible servers with non-/v1 base paths and non-table embedding dimensions
Summary
First off — thanks for OpenFang. While wiring agent-memory embeddings to a local
OpenAI-compatible server (OpenVINO Model Server / OVMS, which serves its API under /v3
rather than /v1), I ran into two small but related rough edges in the embedding driver
(crates/openfang-runtime/src/embedding.rs). Both are reasonable MVP shortcuts — they work
perfectly for OpenAI / Ollama / vLLM, which all serve /v1 — so this is just about widening
support for OpenAI-compatible servers that don't fit that exact shape. Line numbers are from
current main.
1. Embedding base_url is force-appended with /v1, with no way to opt out
When a custom embedding base_url is supplied via [provider_urls], the driver appends
/v1 for a fixed set of providers unless the URL already ends in /v1
(embedding.rs, around line 208):
let needs_v1 = matches!(
provider,
"openai"
| "groq"
| "together"
| "fireworks"
| "mistral"
| "ollama"
| "vllm"
| "lmstudio"
);
if needs_v1 && !trimmed.ends_with("/v1") {
format!("{trimmed}/v1")
} else {
trimmed.to_string()
}This means there's no base_url value that produces a /v3 (or any non-/v1) path:
- a bare host →
/v1appended, http://my-server:8004/v3→ becomeshttp://my-server:8004/v3/v1(broken),http://my-server:8004/v1→ reaches the server but OVMS rejects it (it serves/v3).
So an OpenAI-compatible server on a different base path can't be reached without an
external reverse proxy that rewrites /v1/embeddings → /v3/embeddings.
Suggested fix (small): only append /v1 when the URL has no /vN segment already —
respect any version the user explicitly provided. Something like:
// append /v1 only if the user didn't already specify a /vN path
let has_version = trimmed.rsplit('/').next().map_or(false, |seg| {
seg.len() > 1 && seg.starts_with('v') && seg[1..].chars().all(|c| c.is_ascii_digit())
});
if needs_v1 && !has_version {
format!("{trimmed}/v1")
} else {
trimmed.to_string()
}Then [provider_urls].openai = "http://my-server:8004/v3" would work directly, and /v1
behavior is unchanged for everyone already on /v1.
2. Embedding dimensions are inferred from a hardcoded model-name table
Dimensions come from infer_dimensions(), a match on the model name
(embedding.rs, around line 106):
fn infer_dimensions(model: &str) -> usize {
match model {
"text-embedding-3-small" => 1536,
"text-embedding-3-large" => 3072,
"text-embedding-ada-002" => 1536,
"all-MiniLM-L6-v2" => 384,
"all-MiniLM-L12-v2" => 384,
"all-mpnet-base-v2" => 768,
"nomic-embed-text" => 768,
"mxbai-embed-large" => 1024,
_ => 1536, // default
}
}Any model not in the table (e.g. a Qwen3-Embedding variant) silently defaults to 1536, regardless of what the server actually returns. To get a correct value today, you have to rename your model to match a table key — which is awkward when the same name is also what gets sent to the server in the request body.
There's also a comment a bit further down (around line 160) that reads like this was meant to be handled from the response, but the value isn't actually updated there:
// Update dimensions from actual response if available
let embeddings: Vec<Vec<f32>> = data.data.into_iter().map(|d| d.embedding).collect();(For what it's worth, in my reading the inferred dims value doesn't seem to be used to
size anything downstream — storage looks variable-length and recall compares vectors of the
actual returned length — so on main this seems mostly cosmetic. But it's still a foot-gun:
the reported/configured dimension can silently disagree with reality, and that's exactly the
kind of thing that bites once something does start relying on it. I may be missing a
caller — happy to be corrected.)
Suggested fix (either, or both):
- derive dimensions from the actual embedding response length (the comment above suggests that was the intent), and/or
- allow an explicit override, e.g.
[memory] embedding_dimensions = 1024, that wins over the table for known and unknown model names alike.
Why these two together
Both let OpenFang work with OpenAI-compatible servers that don't match the OpenAI/Ollama "/v1 + a known model name" shape — without renaming models or running an external proxy. Happy to open a PR for #1 (it's small) if that's welcome, and to help test #2 against OVMS.
Environment
- OpenFang v0.6.9 (built from source), Linux, OpenAI-compatible embedding server on
/v3.
Source: RightNow-AI/openfang