Ollama settings #5

Merged
iamdoubz merged 2 commits from ollama-settings into main 2026-11-06 11:02:23 -06:00
6 changed files with 55 additions and 6 deletions

No files matched your search

+18
View File
@@ -16,6 +16,24 @@ OLLAMA_URL=http://localhost:11434
EXTRACT_MODEL=nemotron-3.5-bolt:e4b
# ANTHROPIC_API_KEY=
# Ollama settings. Commented-out lines show the default.
# Sampling: leave top P and top K unset to use the model's own.
# EXTRACT_TEMPERATURE=0
# EXTRACT_TOP_P=0.9
# EXTRACT_TOP_K=40
# Context size in tokens; raise it if a dense page or thinking runs out of room.
# EXTRACT_NUM_CTX=16384
# How long Ollama keeps the model loaded after a scan: 10m, 1h, 0 to unload right away, -1 forever.
# EXTRACT_KEEP_ALIVE=10m
# 1 lets the model reason before answering (slower, needs more context).
# EXTRACT_THINK=0
# Only with EXTRACT_THINK=1, for models that have levels: low, medium or high.
# EXTRACT_THINK_LEVEL=medium
# 1 reads every page twice and flags where the reads disagree, for the command-line eval in phase0/.
# In the app, each source's "double read" setting decides instead.
# EXTRACT_DOUBLE_READ=0
# Labels that are never food or events, for every source (each source can add its own).
# EXTRACT_IGNORE=BHS
+5 -1
View File
@@ -5,9 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
## [Unreleased]
## [0.0.3] - 2026-11-06
### Added
- The server prints its version, commit and commit date, then its settings, when it starts (the API key only as set or unset).
- Ollama settings `EXTRACT_TOP_P`, `EXTRACT_TOP_K`, `EXTRACT_THINK_LEVEL` (low, medium or high, with `EXTRACT_THINK=1`), `EXTRACT_NUM_CTX` (default 16384) and `EXTRACT_KEEP_ALIVE` (default 10m).
- A "Pack lunch" event's notes are the day's first menu line, for example "Orange Chicken".
## [0.0.2] - 2026-11-06
@@ -45,6 +48,7 @@ First test release: the Phase 1 MVP.
- Runs with plain Python or as a Docker image, with SQLite and a `backup` command.
- Release workflow that pushes the image to the Gitea container registry: stable releases tag `X.Y.Z`, `X.Y`, `X` and `latest`; pre-releases tag `X.Y.Z` and `dev`.
[Unreleased]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.2...main
[Unreleased]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.3...main
[0.0.3]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.2...v0.0.3
[0.0.2]: https://git.dou.bet/iamdoubz/parentcalendar/compare/v0.0.1...v0.0.2
[0.0.1]: https://git.dou.bet/iamdoubz/parentcalendar/src/tag/v0.0.1
+5
View File
@@ -96,6 +96,11 @@ Both include `python main.py backup <destination>`, which runs SQLite's online b
| `DATA_DIR` | Database and images; must be a local disk | `./data` when run directly, `/data` in Docker |
| `OLLAMA_URL` | Ollama server for the extraction model | `http://localhost:11434` |
| `EXTRACT_MODEL` | Ollama model name, or a `claude-*` model to use the Anthropic API instead | `nemotron-3.5-bolt:e4b` |
| `EXTRACT_TEMPERATURE`, `EXTRACT_TOP_P`, `EXTRACT_TOP_K` | Ollama sampling settings. Top P and top K fall back to the model's own when not set | `0`, not set, not set |
| `EXTRACT_NUM_CTX` | Ollama context size in tokens. Raise it if a dense page or thinking runs out of room | `16384` |
| `EXTRACT_KEEP_ALIVE` | How long Ollama keeps the model loaded after a call: `10m`, `1h`, `0` to unload, `-1` forever | `10m` |
| `EXTRACT_THINK` | `1` to let an Ollama model reason before answering | `0` |
| `EXTRACT_THINK_LEVEL` | `low`, `medium` or `high`, for models with thinking levels. Only used with `EXTRACT_THINK=1` | Not set |
| `ANTHROPIC_API_KEY` | Only needed for a `claude-*` model | Not set |
| `EXTRACT_IGNORE` | Comma-separated labels dropped from every source's menu items and event titles. Each source can add its own in `ignore_labels` | Not set; for example `BHS,Milk` |
| `HOST`, `PORT` | Bind address | `127.0.0.1`, `8000` |
+4 -1
View File
@@ -43,7 +43,10 @@ def serve():
settings = { # what the app actually uses, defaults included
"PUBLIC_URL": config.PUBLIC_URL, "DATA_DIR": config.DATA_DIR, "HOST": config.HOST, "PORT": config.PORT,
"OLLAMA_URL": extraction.OLLAMA_URL, "EXTRACT_MODEL": extraction.MODEL, "EXTRACT_THINK": int(extraction.THINK),
"EXTRACT_TEMPERATURE": extraction.TEMPERATURE, "EXTRACT_DOUBLE_READ": int(extraction.DOUBLE_READ),
"EXTRACT_THINK_LEVEL": extraction.THINK_LEVEL or "", "EXTRACT_TEMPERATURE": extraction.TEMPERATURE,
"EXTRACT_TOP_P": "" if extraction.TOP_P is None else extraction.TOP_P,
"EXTRACT_TOP_K": "" if extraction.TOP_K is None else extraction.TOP_K,
"EXTRACT_NUM_CTX": extraction.NUM_CTX, "EXTRACT_KEEP_ALIVE": extraction.KEEP_ALIVE, "EXTRACT_DOUBLE_READ": int(extraction.DOUBLE_READ),
"EXTRACT_IGNORE": ",".join(sorted(extraction.IGNORE)), "IMAGE_RETENTION_DAYS": config.IMAGE_RETENTION_DAYS,
"ANTHROPIC_API_KEY": "set" if env("ANTHROPIC_API_KEY") else "unset", # never print the key
}
+22 -3
View File
@@ -6,7 +6,12 @@ Settings (environment variables):
EXTRACT_MODEL Ollama model, or a claude-* model for the Anthropic API (default nemotron-3.5-bolt:e4b)
OLLAMA_URL Ollama server (default http://localhost:11434)
EXTRACT_THINK 1 to let an Ollama model reason before answering (default 0)
EXTRACT_THINK_LEVEL low, medium or high, for models with thinking levels; only used with EXTRACT_THINK=1
(default not set: plain on)
EXTRACT_TEMPERATURE Ollama sampling temperature (default 0)
EXTRACT_TOP_P, EXTRACT_TOP_K Ollama sampling top_p (0-1) and top_k (whole number) (default not set: the model's own)
EXTRACT_NUM_CTX Ollama context size in tokens; raise it if a dense page or thinking runs out of room (default 16384)
EXTRACT_KEEP_ALIVE how long Ollama keeps the model loaded after a call: 10m, 1h, 0 to unload, -1 forever (default 10m)
EXTRACT_DOUBLE_READ 1 to read every page twice and flag where the reads disagree (default 0;
in the app each source's double_read setting decides)
EXTRACT_IGNORE comma-separated labels to drop from menu items and event titles, e.g. "BHS,Milk" (default none)
@@ -40,6 +45,16 @@ MODEL = os.environ.get("EXTRACT_MODEL", "nemotron-3.5-bolt:e4b")
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")
THINK = os.environ.get("EXTRACT_THINK") == "1"
TEMPERATURE = float(os.environ.get("EXTRACT_TEMPERATURE", "0"))
TOP_P = float(os.environ["EXTRACT_TOP_P"]) if os.environ.get("EXTRACT_TOP_P") else None
TOP_K = int(os.environ["EXTRACT_TOP_K"]) if os.environ.get("EXTRACT_TOP_K") else None
NUM_CTX = int(os.environ.get("EXTRACT_NUM_CTX") or 16384)
KEEP_ALIVE = os.environ.get("EXTRACT_KEEP_ALIVE", "").strip() or "10m"
THINK_LEVEL = os.environ.get("EXTRACT_THINK_LEVEL", "").strip().lower() or None
if THINK_LEVEL not in (None, "low", "medium", "high"):
raise SystemExit(f"EXTRACT_THINK_LEVEL must be low, medium or high, not {THINK_LEVEL!r}")
OLLAMA_THINK = THINK_LEVEL if THINK and THINK_LEVEL else THINK # what Ollama's "think" field gets
OLLAMA_OPTIONS = {"num_ctx": NUM_CTX, "temperature": TEMPERATURE,
**({"top_p": TOP_P} if TOP_P is not None else {}), **({"top_k": TOP_K} if TOP_K is not None else {})}
DOUBLE_READ = os.environ.get("EXTRACT_DOUBLE_READ") == "1"
IGNORE = {w.strip().lower() for w in os.environ.get("EXTRACT_IGNORE", "").split(",") if w.strip()}
EFFORT = "high" # Claude only
@@ -283,8 +298,9 @@ def call_claude(image, message, system, schema):
def call_ollama(image, message, system, schema):
body = {
"model": MODEL, "stream": False, "think": THINK, "format": schema,
"options": {"num_ctx": 16384, "temperature": TEMPERATURE}, # ponytail: raise num_ctx if a dense page or thinking runs out of room
"model": MODEL, "stream": False, "think": OLLAMA_THINK, "format": schema, "options": OLLAMA_OPTIONS,
# Ollama wants a number for bare seconds ("300" isn't a duration), a string for "10m".
"keep_alive": int(KEEP_ALIVE) if KEEP_ALIVE.lstrip("-").isdigit() else KEEP_ALIVE,
"messages": [{"role": "system", "content": system},
{"role": "user", "content": message, "images": [image]}],
}
@@ -489,8 +505,11 @@ def extract_image(img, hint, double_read=None, ignore=None):
return {
"model": model,
"method": method + (" x2" if double_read else ""),
"think": THINK if call is call_ollama else None,
"think": OLLAMA_THINK if call is call_ollama else None,
"temperature": TEMPERATURE if call is call_ollama else None,
"top_p": TOP_P if call is call_ollama else None,
"top_k": TOP_K if call is call_ollama else None,
"num_ctx": NUM_CTX if call is call_ollama else None,
"seconds": round(time.time() - started, 1),
"usage": {"input_tokens": tokens[0], "output_tokens": tokens[1]},
"result": result,
+1 -1
View File
@@ -23,7 +23,7 @@ def main(names):
cases = [GOLDEN / f"{n}.json" for n in names] or sorted(GOLDEN.glob("*.json"))
out = RUNS / datetime.now().strftime("%Y%m%d-%H%M%S")
out.mkdir(parents=True)
ollama = "" if x.MODEL.startswith("claude") else f" at {x.OLLAMA_URL}, think={x.THINK}, temperature={x.TEMPERATURE}"
ollama = "" if x.MODEL.startswith("claude") else f" at {x.OLLAMA_URL}, think={x.OLLAMA_THINK}, temperature={x.TEMPERATURE}, top_p={x.TOP_P}, top_k={x.TOP_K}, num_ctx={x.NUM_CTX}, keep_alive={x.KEEP_ALIVE}"
print(f"Model: {x.MODEL}{ollama}")
tokens_in = tokens_out = 0
for path in cases: