Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ The server listens on `127.0.0.1:17434` by default, overridable via `LLMMAN_HOST

| API | Endpoints |
|-----|-----------|
| Ollama | `/api/generate`, `/api/chat`, `/api/tags`, `/api/show`, `/api/pull`, `/api/ps`, `/api/delete` |
| Ollama | `/api/generate`, `/api/chat`, `/api/embed`, `/api/embeddings`, `/api/tags`, `/api/show`, `/api/pull`, `/api/push`, `/api/copy`, `/api/create`, `/api/blobs/{digest}`, `/api/ps`, `/api/delete`, `/api/version` |
| OpenAI | `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/models`, `/v1/responses`, `/v1/responses/input_tokens` |
| Anthropic | `/v1/messages` |
| llmman | `/llmman/providers`, `/llmman/providers/{id}` |
Expand Down Expand Up @@ -201,6 +201,15 @@ as `message.tool_calls`), `images` (vision, base64, same as Ollama's own
wire format), and `format` (`"json"` or a JSON Schema object, for
constrained structured output).

`/api/embed` and `/api/embeddings` work with any embedding model (a GGUF
with a pooling type, e.g. `embeddinggemma`, `nomic-embed-text`): `llama-server`
is started with `--embeddings` for it, so `/v1/embeddings` works too.

`/api/create` supports `from` (alias a model) and `files` (GGUFs uploaded via
`/api/blobs/{digest}`, as `ollama create` does). Modelfile fields such as
`system` or `quantize` are refused with a 400: the GGUF's own chat template
applies.

An idle, unused model is automatically unloaded after `keep_alive`
(default 5 minutes, matching Ollama; set per-request, or daemon-wide via
`LLMMAN_KEEP_ALIVE`), and `llmman ps`/`/api/ps` reports each model's
Expand Down
9 changes: 4 additions & 5 deletions src/cmd/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ pub struct BuildArgs {
}

pub fn run(args: &BuildArgs) -> anyhow::Result<()> {
// The build tag is a user-chosen label stored verbatim, not resolved, so
// this is its only validation gate. Check it before touching the store so
// a bad tag never creates the store tree.
crate::shortnames::validate_reference(&args.tag)?;
// Resolved the way run/rm/show look it up (see cp.rs), before
// touching the store so a bad tag never creates the store tree.
let tag = crate::shortnames::resolve_ollama_api(&args.tag)?;

let store_root = crate::default_store()?;
let store = OciStore::open(&store_root)?;
Expand All @@ -46,7 +45,7 @@ pub fn run(args: &BuildArgs) -> anyhow::Result<()> {
.canonicalize()
.with_context(|| format!("context dir: {}", args.context_dir.display()))?;

let desc = store.build(&context_dir, &args.tag, &labels)?;
let desc = store.build(&context_dir, &tag, &labels)?;
println!("Built {} ({})", args.tag, desc.digest);
Ok(())
}
Expand Down
27 changes: 16 additions & 11 deletions src/cmd/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,32 @@ pub struct CpArgs {
/// to match ollama's naming for anyone coming from there, not because the
/// underlying operation differs at all.
pub fn run(args: &CpArgs) -> anyhow::Result<()> {
// Validate/resolve both refs before touching the store, so a bad ref
// never creates the store tree. DESTINATION is stored verbatim, not
// resolved/canonicalized, so this is its only client-side validation
// gate. resolve_ollama_api for SOURCE, not resolve: it must match how
// the model is actually stored, same reasoning as tag.rs/rm.rs.
crate::shortnames::validate_reference(&args.destination)?;
// Both via resolve_ollama_api, before touching the store: SOURCE to
// match how it's stored, DESTINATION to be stored the way run/rm/show
// look it up (a bare `mine:v1` becomes `docker.io/ai/mine:v1`).
let source = crate::shortnames::resolve_ollama_api(&args.source)?;
let destination = crate::shortnames::resolve_ollama_api(&args.destination)?;

let store_root = crate::default_store()?;
let store = OciStore::open(&store_root)?;
copy(&store, &source, &destination)?;
println!("copied '{}' to '{}'", args.source, args.destination);
Ok(())
}

let desc = store.find(&source)?;
/// Points `destination` at the content `source` names (both already
/// resolved) and returns its digest. Shared by `llmman cp` and `/api/copy`.
pub fn copy(store: &OciStore, source: &str, destination: &str) -> anyhow::Result<String> {
let desc = store.find(source)?;
// A destination spelled as a digest names content, and the store
// answers such a reference by content (see `OciStore::find`): a
// pointer there holding some other digest would be passed over by a
// lookup for the digest it is named for and answer only for the one
// it holds, so it is refused.
check_digest_destination(&args.destination, &desc.digest)?;
store.tag(desc, &args.destination)?;
println!("copied '{}' to '{}'", args.source, args.destination);
Ok(())
check_digest_destination(destination, &desc.digest)?;
let digest = desc.digest.clone();
store.tag(desc, destination)?;
Ok(digest)
}

/// Refuses a `<name>@<digest>` destination whose digest is not `actual`,
Expand Down
Loading
Loading