# Build and check a seven-role local and cloud AI model portfolio
> Configure five local and two permission-gated cloud AI task routes, then check exact model identity, hardware fit, output quality, latency, privacy and cost before accepting each route.
By BIG CHANGE Editorial
Published: 2026-10-21T13:58:38.942Z
Updated: 2026-10-21T13:58:38.942Z
Canonical: https://bigchange.ai/blog/seven-role-local-cloud-ai-model-portfolio-guide

AI-generated conceptual illustration by BIG CHANGE.
One model can answer a prompt. A working model portfolio has a harder job: it must send each approved task to an identified model, keep private work on permitted routes, and show when a route is too slow, too costly or simply wrong. This guide gives a technical lead seven explicit routes to test: five local Ollama roles and two optional OpenAI API roles. The local coding path splits patch drafting from diff review, so the second pass can challenge the first.
The models below are candidates, not winners of a benchmark. Their tags and API paths were checked against current first-party documentation on 21 October 2026. BIG CHANGE did not download or run the models, inspect a reader's hardware, make a paid API call or measure response quality. You will make the keep, replace or disable decision from your own permitted cases.
## The big change
- **What changed:** The seven-role matrix makes routing visible: triage, extraction, summary, code draft and code review stay local; editing and planning require separate cloud permission.
- **Why it matters:** A dry run reports the selected role, backend, model and input hash without sending the task. Execution has no automatic provider fallback.
- **What to watch:** Keep only routes whose exact installed models, outputs, time, resource use and cloud charges pass your own checks. An untested route remains unapproved.
## What you need
This is for a technical lead who can approve small test inputs, inspect the machine running Ollama and decide whether any text may be sent to a paid cloud API. Use Python 3 with its standard library, Ollama, and a private working directory. You need an OpenAI API key only if you later approve a cloud test. Check your organization's data rules before creating the test files.
The example uses `qwen3.5:2b`, `qwen3.5:4b`, `gemma4:e2b`, `qwen2.5-coder:7b` and `qwen3.5:9b` locally; the two cloud candidates are `gpt-6-luna` and `gpt-6.1-sol`. The [Ollama model pages](https://ollama.com/library/qwen3.5), [Gemma 4 page](https://ollama.com/library/gemma4) and [Qwen 2.5 Coder page](https://ollama.com/library/qwen2.5-coder) list these candidate families and sizes. The [OpenAI model references](https://developers.openai.com/api/docs/models/gpt-6-luna) and [GPT-6.1 Sol reference](https://developers.openai.com/api/docs/models/gpt-6.1-sol) list the cloud IDs. A listed tag or model ID does not prove that your installation, hardware or account can use it.
Choose a few small, approved cases for each role and write down the expected result before calling a model. Triage needs your real labels; extraction needs source facts and a rule for missing fields; summary needs source line references; the coding pair needs a harmless fixture, a proposed diff and acceptance checks; cloud editing and planning need separately approved, de-identified inputs. Keep a manual path for any case that fails the check.
## 1. Pin the local environment and permissions
Install Ollama through its supported platform instructions. Check the Ollama version, machine RAM, available GPU memory and free disk space in your own environment. On Windows, inspect Task Manager's Performance view; on macOS, inspect About This Mac and Activity Monitor; on Linux, use the system's memory and GPU tools, such as `free -h` and `nvidia-smi` when an NVIDIA GPU is present. Record free capacity as well as installed capacity. Pull only the five tags you intend to test, one at a time, after checking their model pages and storage needs:
```sh
ollama --version
ollama pull qwen3.5:2b
ollama pull qwen3.5:4b
ollama pull gemma4:e2b
ollama pull qwen2.5-coder:7b
ollama pull qwen3.5:9b
curl http://127.0.0.1:11434/api/tags
ollama ps
```
The [Ollama tags endpoint](https://docs.ollama.com/api/tags) returns each installed name and digest. Save that readback, plus your Ollama version, operating system, CPU/GPU, RAM/VRAM, quantization and available disk, in a private test record. Recheck digests after any pull or update. `ollama ps` shows what is loaded and whether it is using CPU or GPU memory; a model page's size is no substitute for an observed load on your machine. The [Ollama FAQ](https://docs.ollama.com/faq) explains memory, queueing and concurrent loads. Run cases sequentially first, because loading several models at once can alter both memory use and timing.
If local-only handling is required, disable Ollama cloud features in its documented settings, restart it and confirm the log reports `Ollama cloud disabled: true`. Also verify the Ollama listener is bound to loopback and that local logs and saved result files meet your privacy rules. A `127.0.0.1` URL in this sample does not inspect the rest of your Ollama configuration.
For cloud tests, separately confirm your account can call both model IDs through the Responses API, review the [current price table](https://developers.openai.com/api/docs/pricing) and [data controls](https://developers.openai.com/api/docs/guides/your-data), set a spending limit outside this script, and obtain approval for each input. The sample sends `store: false`; that setting does not promise zero provider retention. Keep the two cloud rows untested if access, permission or price approval is missing.
## 2. Save the seven routes
Create `routing-config.json` in the private working directory. The task strings are short guardrails for the sample request, not proof that a model will obey them. Edit them to match your acceptance criteria and record any config change with the corresponding test run. Do not turn a local row into a cloud row merely to make a case pass.
```json
{
"schema": "original-seven-role-reader-recipe-v1",
"policy": "explicit-role-only; no automatic fallback or provider escalation",
"localEndpoint": "http://127.0.0.1:11434",
"cloudEndpoint": "https://api.openai.com/v1/responses",
"roles": {
"triage": {"backend": "local", "model": "qwen3.5:2b", "task": "Classify the reader's approved small request against reader-owned labels"},
"extract": {"backend": "local", "model": "qwen3.5:4b", "task": "Extract explicit requirements and missing facts without inventing them"},
"summarize": {"backend": "local", "model": "gemma4:e2b", "task": "Summarize approved project context with source line references"},
"code_draft": {"backend": "local", "model": "qwen2.5-coder:7b", "task": "Propose a small code patch for the reader's fixture; no tool execution"},
"code_review": {"backend": "local", "model": "qwen3.5:9b", "task": "Review that proposed diff against acceptance checks; no tool execution"},
"cloud_edit": {"backend": "cloud", "model": "gpt-6-luna", "task": "Edit an explicitly cloud-approved, de-identified answer"},
"cloud_plan": {"backend": "cloud", "model": "gpt-6.1-sol", "task": "Plan or second-review an explicitly cloud-approved complex case"}
}
}
```
The local code sub-stack has two explicit handoffs. Send an approved fixture and a request for a small patch to `code_draft`. Have a human inspect the proposed text before putting the proposed diff, fixture context and acceptance checks into a separate `code_review` input. Neither role runs a command or modifies a repository. A reviewer finding is evidence to inspect, not permission to apply the patch.
## 3. Save the router and run a dry check
Save the following as `route.py` alongside the JSON file. It has a fixed loopback Ollama endpoint and a fixed official HTTPS cloud endpoint. It rejects an unknown backend or endpoint edit, refuses HTTP redirects, ignores environment proxies, requires `--allow-cloud` for a cloud execution, and never switches providers after an error. These boundaries address accidental routing in this sample; review the code and your network environment before sending sensitive work.
```python
"""Reader-run seven-role sample. Dry-run by default; no automatic fallback."""
import argparse
import hashlib
import json
import os
import pathlib
import sys
import time
import urllib.error
import urllib.request
HERE = pathlib.Path(__file__).resolve().parent
CONFIG = json.loads((HERE / "routing-config.json").read_text(encoding="utf-8"))
LOCAL_ENDPOINT = "http://127.0.0.1:11434"
CLOUD_ENDPOINT = "https://api.openai.com/v1/responses"
class NoRedirect(urllib.request.HTTPRedirectHandler):
"""A provider response cannot redirect a prompt or credential elsewhere."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
raise urllib.error.HTTPError(req.full_url, code, "redirect blocked", headers, fp)
def validate_config():
if CONFIG.get("localEndpoint") != LOCAL_ENDPOINT:
raise ValueError("local endpoint must be the fixed loopback address")
if CONFIG.get("cloudEndpoint") != CLOUD_ENDPOINT:
raise ValueError("cloud endpoint must be the fixed official HTTPS address")
roles = CONFIG.get("roles")
if not isinstance(roles, dict) or len(roles) != 7:
raise ValueError("expected exactly seven named roles")
for name, route in roles.items():
if not isinstance(route, dict) or route.get("backend") not in ("local", "cloud"):
raise ValueError(f"unknown backend for role {name}")
if not isinstance(route.get("model"), str) or not route["model"]:
raise ValueError(f"missing model for role {name}")
def request_json(url, payload, headers):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
# Ignore environment proxy settings and refuse all HTTP redirects.
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
with opener.open(req, timeout=120) as response:
return json.load(response)
def main():
try:
validate_config()
except ValueError as exc:
print(f"invalid routing config: {exc}", file=sys.stderr)
return 2
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("role", choices=CONFIG["roles"])
parser.add_argument("input", type=pathlib.Path, help="Reader-owned, permitted UTF-8 task file")
parser.add_argument("--execute", action="store_true", help="Actually call selected provider")
parser.add_argument("--allow-cloud", action="store_true", help="Required for any paid cloud call")
parser.add_argument("--out", type=pathlib.Path, help="New private JSON result file; never overwritten")
args = parser.parse_args()
route = CONFIG["roles"][args.role]
raw = args.input.read_bytes()
prompt = raw.decode("utf-8")
if not prompt.strip():
parser.error("input file is empty")
identity = {"role": args.role, "backend": route["backend"], "model": route["model"],
"inputSha256": hashlib.sha256(raw).hexdigest(), "inputBytes": len(raw)}
if not args.execute:
print(json.dumps({**identity, "state": "dry-run; no provider call", "task": route["task"]}, indent=2))
return
if not args.out:
parser.error("--out is required with --execute")
if args.out.exists():
parser.error("output exists; use a new private path to preserve the previous result")
if route["backend"] == "cloud" and not args.allow_cloud:
parser.error("cloud route requires explicit --allow-cloud after permission and cost review")
started = time.monotonic()
if route["backend"] == "local":
# A local endpoint can still be exposed or configured for cloud features; verify settings first.
url = CONFIG["localEndpoint"] + "/api/chat"
payload = {"model": route["model"], "messages": [{"role": "system", "content": route["task"]},
{"role": "user", "content": prompt}], "stream": False, "keep_alive": 0}
headers = {"Content-Type": "application/json"}
elif route["backend"] == "cloud":
key = os.environ.get("OPENAI_API_KEY")
if not key:
parser.error("OPENAI_API_KEY is absent")
url = CONFIG["cloudEndpoint"]
payload = {"model": route["model"], "instructions": route["task"],
"input": prompt, "store": False}
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + key}
else:
# Defense in depth if a caller bypasses the configuration readback.
parser.error("unsupported backend")
try:
result = request_json(url, payload, headers)
except urllib.error.HTTPError as exc:
print(json.dumps({**identity, "state": "http-error; no automatic retry", "httpStatus": exc.code,
"elapsedSeconds": round(time.monotonic() - started, 3)}), file=sys.stderr)
return 2
except (urllib.error.URLError, TimeoutError) as exc:
print(json.dumps({**identity, "state": "request outcome unknown; do not blindly retry",
"errorType": type(exc).__name__,
"elapsedSeconds": round(time.monotonic() - started, 3)}), file=sys.stderr)
return 2
if route["backend"] == "local":
output = result.get("message", {}).get("content", "")
usage = {k: result.get(k) for k in ("prompt_eval_count", "eval_count", "total_duration", "load_duration")}
response_id = None
status = result.get("done_reason")
elif route["backend"] == "cloud":
output = "\n".join(part.get("text", "") for item in result.get("output", [])
if item.get("type") == "message" for part in item.get("content", [])
if part.get("type") == "output_text")
usage = result.get("usage", {})
response_id = result.get("id")
status = result.get("status")
else:
parser.error("unsupported backend")
record = {**identity, "state": "response received", "responseId": response_id,
"responseStatus": status, "elapsedSeconds": round(time.monotonic() - started, 3),
"usage": usage, "output": output}
# Output can contain private input; create only at a reader-selected private path.
with args.out.open("x", encoding="utf-8") as handle:
json.dump(record, handle, ensure_ascii=False, indent=2)
handle.write("\n")
print(json.dumps({**identity, "state": "saved", "resultFile": str(args.out),
"responseStatus": status, "elapsedSeconds": record["elapsedSeconds"]}))
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
Put one permitted UTF-8 task in `case.txt`. A dry run does not read a provider key or send the file:
```sh
python route.py triage case.txt
python route.py code_draft case.txt
python route.py cloud_plan case.txt
```
Each result should say `dry-run; no provider call` and show the exact role, backend, model, input byte count and SHA-256 hash. Compare it with the route table and the input you meant to use. The hash helps identify which local file produced a result; it is not a privacy review of that file. If a route or permission is wrong, stop here and correct the private config or input.
## 4. Test each role on permitted cases
For a local role, use a new private output path for every run:
```sh
python route.py triage case.txt --execute --out triage-001.json
```
Repeat for `extract`, `summarize`, `code_draft` and `code_review` with role-specific inputs. The saved record contains the input hash, chosen model, response text, elapsed wall time and any timing/token fields returned by Ollama's [chat API](https://docs.ollama.com/api/chat). Its `keep_alive: 0` setting asks Ollama to unload the model after a response; this makes repeated load time relevant to the measurements. Record cold and repeated runs separately if your eventual workload keeps models resident. Confirm the actual installed digest for each run from the saved tags readback. A successful HTTP response says nothing by itself about answer quality.
Cloud calls are a distinct, explicit step after permission and budget checks. Put only a cloud-approved, de-identified task in the input file, set `OPENAI_API_KEY` in your environment without placing it in the file or shell history, and use a new private output path:
```sh
python route.py cloud_edit approved-edit.txt --execute --allow-cloud --out cloud-edit-001.json
python route.py cloud_plan approved-plan.txt --execute --allow-cloud --out cloud-plan-001.json
```
Run only the approved call or calls; those commands can incur charges. The code uses the documented [Responses API text path](https://developers.openai.com/api/docs/guides/text), records the response ID, status, usage and elapsed time, and saves output privately. Confirm that `responseStatus` is complete and that the returned text actually answers the task. A missing or incomplete output is a failed case. Cloud prices and access can vary by account and date. For ordinary per-million-token input and output rates, calculate `(input tokens × input rate + output tokens × output rate) / 1,000,000`; apply any current cached-input, tool or account-specific terms separately. Set the per-case cap before testing and compare the actual billing record afterward.
For each route, keep a row with: case ID and approved input class; config revision; exact model name and local digest or cloud model ID; machine/resource observations; expected result; observed result; pass/fail and reason; elapsed seconds; local load and token fields or cloud usage; estimated charge; permission decision; reviewer; and manual fallback. Use the same frozen cases when comparing candidates. Never mix a cold local load with a warm run without labeling them. A cloud model unavailable to your account remains `untested`, not `failed quality`.
## 5. Challenge the routing boundary without provider calls
Save this `test_route.py` in the same directory, then run `python -m unittest -v test_route.py`. It mocks requests. The checks cover a mistyped backend, altered endpoint, attempted cloud execution without consent, allowed sample paths and a blocked redirect. They do not test model quality, provider availability or your hardware.
```python
"""Call-free regression checks for the reader example's routing boundary."""
import contextlib
import copy
import io
import json
import pathlib
import sys
import tempfile
import unittest
import urllib.error
import urllib.request
from unittest import mock
import route
class RoutingBoundaryTests(unittest.TestCase):
def setUp(self):
self.original = copy.deepcopy(route.CONFIG)
self.tmp = tempfile.TemporaryDirectory()
self.base = pathlib.Path(self.tmp.name)
self.input = self.base / "case.txt"
self.input.write_text("Reader-owned test case", encoding="utf-8")
def tearDown(self):
route.CONFIG = self.original
self.tmp.cleanup()
def run_main(self, *args):
with mock.patch.object(sys, "argv", ["route.py", *args]), contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
return route.main()
def test_unknown_backend_cannot_select_cloud(self):
route.CONFIG["roles"]["triage"]["backend"] = "cluod"
with mock.patch.object(route, "request_json") as send, mock.patch.dict(route.os.environ, {"OPENAI_API_KEY": "fake"}):
code = self.run_main("triage", str(self.input), "--execute", "--out", str(self.base / "out.json"))
self.assertEqual(code, 2)
send.assert_not_called()
def test_endpoint_changes_fail_before_request_or_key(self):
for field, wrong in (("localEndpoint", "http://127.0.0.2:11434"),
("cloudEndpoint", "https://api.openai.com.evil.test/v1/responses")):
with self.subTest(field=field):
route.CONFIG[field] = wrong
with mock.patch.object(route, "request_json") as send, mock.patch.dict(route.os.environ, {"OPENAI_API_KEY": "fake"}):
code = self.run_main("cloud_plan", str(self.input), "--execute", "--allow-cloud", "--out", str(self.base / "out.json"))
self.assertEqual(code, 2)
send.assert_not_called()
route.CONFIG = copy.deepcopy(self.original)
def test_cloud_needs_explicit_consent_and_valid_routes_work(self):
with mock.patch.object(route, "request_json") as send, mock.patch.dict(route.os.environ, {"OPENAI_API_KEY": "fake"}):
with self.assertRaises(SystemExit):
self.run_main("cloud_plan", str(self.input), "--execute", "--out", str(self.base / "cloud.json"))
send.assert_not_called()
self.assertIsNone(self.run_main("triage", str(self.input)))
self.assertIsNone(self.run_main("cloud_plan", str(self.input)))
send.assert_not_called()
send.return_value = {"message": {"content": "test"}, "done_reason": "stop"}
self.assertEqual(self.run_main("triage", str(self.input), "--execute", "--out", str(self.base / "local.json")), 0)
self.assertEqual(send.call_args.args[0], route.LOCAL_ENDPOINT + "/api/chat")
self.assertNotIn("Authorization", send.call_args.args[2])
send.return_value = {"status": "completed", "output": [], "usage": {}}
self.assertEqual(self.run_main("cloud_plan", str(self.input), "--execute", "--allow-cloud", "--out", str(self.base / "cloud.json")), 0)
self.assertEqual(send.call_args.args[0], route.CLOUD_ENDPOINT)
self.assertEqual(send.call_args.args[2]["Authorization"], "Bearer fake")
def test_transport_disables_proxy_and_redirect(self):
def fake_builder(*handlers):
self.assertTrue(any(isinstance(h, route.NoRedirect) for h in handlers))
self.assertTrue(any(isinstance(h, urllib.request.ProxyHandler) and h.proxies == {} for h in handlers))
class FakeOpener:
def open(self, req, timeout):
raise urllib.error.HTTPError(req.full_url, 302, "redirect blocked", {"Location": "https://evil.test"}, None)
return FakeOpener()
with mock.patch.object(urllib.request, "build_opener", side_effect=fake_builder):
with self.assertRaises(urllib.error.HTTPError):
route.request_json(route.CLOUD_ENDPOINT, {"input": "private"}, {"Authorization": "Bearer fake"})
req = urllib.request.Request(route.CLOUD_ENDPOINT, headers={"Authorization": "Bearer fake"})
with self.assertRaises(urllib.error.HTTPError):
route.NoRedirect().redirect_request(req, None, 302, "Found", {}, "https://evil.test")
if __name__ == "__main__":
unittest.main()
```
The packet behind this guide passed these four call-free regressions and 47 independent call-free checks of its sample assets. That establishes the behavior of the example under those checks. It is not a claim that any of the seven models ran or that a real network request succeeded.
## Decide what stays enabled
Review the seven rows individually. Keep a route only when its exact model and environment are identified, the permitted cases meet the written quality rule, latency fits the actual workflow, and its privacy and operating cost are approved. Local execution still consumes storage, memory, power and maintenance time. Cloud charges depend on the actual token use, rate card and any account terms. For `code_draft`, require the separate diff review and human checks before any patch is applied; for `code_review`, inspect false positives and missed defects as carefully as fluent explanations.
If a local tag is absent, a model does not fit memory, or Ollama returns an overload error, keep that route disabled and record the exact readback. Reassess a smaller documented candidate with fresh cases and a new config record. If a cloud call fails or times out, its outcome may be unknown; inspect provider state and billing before considering another paid request. This sample does not retry or escalate a failed local case to cloud. When quality is poor, revise the rubric or candidate deliberately; use the manual process until a new run passes.
For a simpler local starting point, our guides to [check one Ollama chat endpoint](https://bigchange.ai/blog/run-local-ollama-model-chat-api-guide) and [connect Ollama to Open WebUI and Continue](https://bigchange.ai/blog/ollama-open-webui-continue-local-stack-guide) cover setup tasks. The [two-model FAQ benchmark](https://bigchange.ai/blog/promptfoo-two-model-faq-benchmark-guide) is a separate way to freeze cases for one evaluation task. This portfolio asks you to maintain seven distinct routing decisions and their evidence.
## Sources & further reading
- [Ollama chat API](https://docs.ollama.com/api/chat): request and response fields for the local runner, including timing and token counts.
- [Ollama installed model list](https://docs.ollama.com/api/tags) and [FAQ](https://docs.ollama.com/faq): digest readback, loaded-model behavior, memory and local-only setting.
- [Qwen 3.5](https://ollama.com/library/qwen3.5), [Gemma 4](https://ollama.com/library/gemma4) and [Qwen 2.5 Coder](https://ollama.com/library/qwen2.5-coder) model pages: candidate tag families and sizes, not results on these seven tasks.
- [OpenAI text generation](https://developers.openai.com/api/docs/guides/text), [GPT-6 Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) and [GPT-6.1 Sol](https://developers.openai.com/api/docs/models/gpt-6.1-sol): documented endpoint and model IDs; access must be confirmed in the reader's account.
- [OpenAI data controls](https://developers.openai.com/api/docs/guides/your-data) and [pricing](https://developers.openai.com/api/docs/pricing): retention boundaries and current rates to check before any cloud call.
## Sources
- [Ollama: Generate a chat message](https://docs.ollama.com/api/chat) — Documents POST /api/chat, model/messages, nonstreaming response and duration/token fields. It supports sample request shape, not quality or hardware performance.
- [Ollama: List models](https://docs.ollama.com/api/tags) — Documents GET /api/tags with installed name and digest. Reader must read their own installed set and pin exact digests.
- [Ollama FAQ](https://docs.ollama.com/faq) — Documents Ollama local-only setting, listener exposure, loaded-model memory and keep_alive. Reader must verify actual settings, RAM/VRAM and load behavior.
- [Ollama Qwen 3.5 family](https://ollama.com/library/qwen3.5) — Lists candidate 2b, 4b and 9b tags. Family listing is not evidence that any role passes this reader's cases.
- [Ollama Gemma 4 family](https://ollama.com/library/gemma4) — Lists the candidate e2b tag. No task-quality or hardware-fit result is inferred.
- [Ollama Qwen 2.5 Coder family](https://ollama.com/library/qwen2.5-coder) — Lists the candidate 7b code-oriented tag. Diff and code quality need separate reader-run evaluation.
- [OpenAI API text generation](https://developers.openai.com/api/docs/guides/text) — Documents the Responses API text request and output path used by the optional cloud rows. No live call was made.
- [OpenAI GPT-6 Luna model](https://developers.openai.com/api/docs/models/gpt-6-luna) — Lists candidate model ID and endpoint support; actual account access and cost must be confirmed.
- [OpenAI GPT-6.1 Sol model](https://developers.openai.com/api/docs/models/gpt-6.1-sol) — Lists second candidate model ID and endpoint support; actual account access and cost must be confirmed.
- [OpenAI platform data controls](https://developers.openai.com/api/docs/guides/your-data) — Explains application state and retention limits. The example's store:false is not a zero-retention or data-permission guarantee.
- [OpenAI API pricing](https://developers.openai.com/api/docs/pricing) — Current rate-card reference for reader budget approval and usage-based calculation; no fixed account-specific price or actual charge asserted.
- [BIG CHANGE: Run a local Ollama model and check its chat API](https://bigchange.ai/blog/run-local-ollama-model-chat-api-guide) — Contextual single-model setup guide #459; related task, no dated continuation.
- [BIG CHANGE: Set up Ollama, Open WebUI and Continue on one Mac](https://bigchange.ai/blog/ollama-open-webui-continue-local-stack-guide) — Contextual local-stack guide #471; distinct whole reader task.
- [BIG CHANGE: Build a checked two-model FAQ benchmark with Promptfoo](https://bigchange.ai/blog/promptfoo-two-model-faq-benchmark-guide) — Contextual fixed-case evaluation guide #473; its two-model FAQ task is not this seven-role portfolio.
The BIG CHANGE newsletter
The big picture. At your pace.
Recent stories on AI and robotics, the shifts worth watching and practical ideas to use. Choose a daily briefing, weekly digest or monthly perspective.
Next scheduled send (UTC): . Your first edition arrives at the next scheduled send after you confirm.
Your privacy, your choice.
Necessary storage supports site security and remembers your choices. Optional Google Analytics stays off until you allow it. You can read every story with necessary storage only. Privacy details