Pullboard
git 저장소에 존재하는 작업 보드로, 에이전트가 작업을 자동으로 할당·검토하고 별도 설명이 필요 없는 로컬 오픈소스 도구
프라이버시·비용·품질·복잡성·지연·거버넌스를 기준으로 로컬 모델(Ollama)과 클라우드 모델 간 AI 작업을 라우팅합니다
Routes AI work between local models (Ollama) and cloud models based on privacy, cost, quality, complexity, latency and governance.
개발 도구TypeScript · CSSGitHub @caleb848온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Routes every AI request to a local model (Ollama) or to Claude by privacy, cost, quality and latency, with fail-closed governance, a worst-case Budget Guard, human review and full request traces. Live demo runs in simulated Portfolio Mode.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Routes every AI request to a local model or to Claude, and enforces data policy, spend limits and human review around the answer.
Live demo: ai-ops-control-plane.netlify.app. The public site runs in Portfolio Mode: routing, governance and cost logic run for real on every request, while model execution is simulated, with no API keys and no access to any machine (how it differs from a local install).
Not every AI task should go to the most powerful cloud model. Confidential work can stay on a local model at zero API cost; complex reasoning can justify a stronger cloud model; simple work shouldn't pay for intelligence it doesn't need. This project is the layer that makes that call for every request, explains it, and records it.
max_tokens at list price is reserved in the ledger synchronously, so concurrent requests can't race past the budget; provider-reported usage settles it afterwards. When cloud doesn't fit, a capable local model takes the request.npm run verify:portfolio (35 checks) and browser QA at four screen sizes against the live site.Stack: Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS 4 · SQLite · zod · Vitest · Playwright · Anthropic SDK · Ollama · Netlify
| Local AI | Real inference through Ollama on the developer's laptop (Gemma 4 12B and Qwen 3.5 9B on an M3 Pro). Any installed Ollama model is discovered and registered automatically. |
| Cloud AI | Anthropic Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5 through the official SDK. OpenAI and Gemini adapters are included and activate when configured. |
| Default mode | DEMO: local models run for real, cloud models are simulated by MockProvider and labeled as such. Paid calls are opt-in. |
| Public demo | ai-ops-control-plane.netlify.app in Portfolio Mode: same routing and governance, simulated execution, no keys, no access to any machine. |
For every request the control plane:
flowchart TD
A[User request] --> B[Task classifier<br/><sub>type · complexity · risk</sub>]
B --> S[Sensitivity scanner<br/><sub>PII · credentials · markings</sub>]
S --> C[Governance engine<br/><sub>data-classification policy</sub>]
C --> D[Routing engine<br/><sub>gates → threshold → weighted score</sub>]
D --> G{Budget guard<br/><sub>worst-case reservation</sub>}
G --> L[Local · Ollama<br/>Gemma 4 · Qwen 3.5 · any pulled model]
G --> M[Cloud models<br/>Claude Haiku · Sonnet · Opus · others]
L --> E[Evaluation estimate]
M --> E
M -. provider failure .-> F[Fallback chain<br/><sub>only policy-approved models</sub>]
F --> L
E --> H[Human approval<br/><sub>when policy requires</sub>]
H --> O[Observability<br/><sub>trace · spend ledger · metrics</sub>]
C -. local required + Ollama down .-> X[Blocked · fail-closed]
| AI Operations Overview | Routing decision | Model Comparison Lab |
|---|---|---|
| The week as a story: a routing map of where requests went, destination tiles, traffic by day, API spend against an all-cloud baseline, the guardrails that fired, which models did the work and live Local AI status. | The request moves through classification, policy, capability and economics to one of four destinations (Local AI, Cloud AI, Human review, Blocked), with the reasons in plain words and every model's scores one click deeper. | One task across local and cloud models: a quality-against-cost value map, then output, evaluation, latency, external API cost, tokens and data location side by side, with a recommendation. |
Screenshots are from a local install: real Ollama runs, cloud in DEMO mode.
More: fail-closed block · budget guard · Sonnet over Opus · request trace · governance policy
All six run from Route a request. Organizations, people, emails (.example) and phone numbers (555-01xx) are fictional. The routing outcomes below are the same on the public demo, where local runs are replays of recorded real outputs (or a labeled notice) and cloud runs are simulated.
| # | Scenario | What happens | Why |
|---|---|---|---|
| 1 | Confidential client brief | Gemma 4 12B (local) runs it for real. Haiku, Sonnet and Opus are blocked by policy. External API cost $0.00. Held for human review. | Six sensitivity findings make it Confidential. Policy prefers local; cloud needs explicit, justified permission. Qwen 3.5 9B is considered but falls just below the quality bar (67 < 68). |
| 2 | Simple public summarization | Depends on the priority mode: Lowest cost / Privacy first → local; Balanced / Fastest → Haiku 5.5; Best quality → Opus 5.5. | For public data, Haiku costs ~$0.0002 and answers in ~2 s, versus 15–30 s locally. Free isn't automatically best. |
| 3 | Complex strategic reasoning | Sonnet 5.5 selected. Local models and Haiku are considered but below the reasoning threshold. Opus scores higher on quality but loses on value. | Very high complexity raises the bar to 85. Quality above the requirement counts at 35% in Balanced mode, so Opus's extra capability doesn't justify ~2× the cost. Best quality mode picks Opus. |
| 4 | Cloud provider failure | Haiku is selected, the (simulated) Anthropic API returns 503, the other Anthropic models are skipped as a provider-level failure, and a local model serves the request. All of it is in the trace. | Fallback walks a chain built only from models that already passed governance. |
| 5 | Cloud budget exhausted | Team default is Best quality, so Opus is selected, but the session has $0.005 left. Budget Guard reroutes to a capable local model. | The guard reserves the worst case (input + max_tokens at list price), not the expected cost. |
| 6 | Restricted data, local AI offline | Blocked, fail-closed. "Local execution required by policy, but the approved local provider is unavailable." Nothing is sent to the cloud. | Restricted data is local-only. When the local provider is down, the correct answer is no answer. |
Scenarios 4–6 use fault injection (provider outage, session spend, Ollama offline). Every injected condition is recorded in the trace and marked simulated.
The router lives in src/core/routing/engine.ts; every number it uses is in src/core/routing/config.ts.
1. Hard gates, in order: disabled in the registry → blocked by policy → provider unavailable → context window too small. A gated model gets no score.
2. Quality threshold. Each model's quality for the task blends its task fit (the capability dimensions the task type draws on) with its reasoning profile; reasoning's share grows with complexity (15% → 65%). The required quality also grows with complexity: 60 / 68 / 78 / 85. Models below it are "eligible but below threshold" and are only used if nothing else qualifies, in which case human review is required.
3. Weighted score (0–100) over five factors:
| Factor | How it's scored |
|---|---|
| Privacy | 100 local. Cloud: 85 public, 55 internal, 25 confidential (with permission). |
| Cost | Square-root scale: 100 at $0, 0 at ≥ $0.05 per request. Small spends still register. |
| Task fit / Reasoning | From the model's Configurable Capability Profile. Quality above the requirement counts at a reduced rate (diminishing returns). |
| Latency | Estimated from token counts and throughput (measured from real runs when available), including a cold-start penalty when a local model isn't resident in memory. −18 points per doubling after 1 s. |
| Priority mode | Quality | Cost | Latency | Privacy | Surplus quality counts at |
|---|---|---|---|---|---|
| Lowest cost | 0.20 | 0.70 | 0 | 0.10 | 20% |
| Balanced | 0.40 | 0.25 | 0.15 | 0.20 | 35% |
| Best quality | 0.75 | 0.05 | 0.10 | 0.10 | 100% |
| Fastest | 0.20 | 0.10 | 0.60 | 0.10 | 20% |
| Privacy first | 0.25 | 0.10 | 0.05 | 0.60 | 35%, and a local model that clears the bar always wins |
Scores within 0.5 are ties, broken by lower cost, then lower latency.
4. Budget guard. If the winner is a cloud model whose worst-case cost doesn't fit the remaining session or lifetime budget, it is marked over budget and the request goes to the best capable local model; failing that, the cheapest capable cloud model that fits; failing that, it is blocked.
5. Fallback chain. The remaining candidates that passed every gate, in rank order. Local-preferring policies keep local alternatives first.
Configurable Capability Profiles are routing configuration, not benchmarks. They express how much an operator trusts a model for a kind of work. The defaults for the two installed local models come from the owner's own local evaluation (2026-10-04, thinking off): Gemma 4 12B passed all graded prompts and a 10K-token retrieval test; Qwen 3.5 9B missed an arithmetic step, a word limit and a long-context lookup but generates ~30% faster. Cloud defaults are tier-based judgment. All values are editable in the Model registry and meant to be tuned with Comparison Lab results.
| Model | Runs on | Price (input / output per million tokens) | Typical role |
|---|---|---|---|
| Gemma 4 12B, Qwen 3.5 9B | The developer's laptop, via Ollama | $0 in API fees; costs local compute and time (about 18 s per request in testing) | Confidential and restricted work, and anything that clears the quality bar when cost or privacy dominates |
| Claude Haiku 5.5 | Anthropic API | $0.10 / $0.50 (prompts up to 100K tokens) | Fast, cheap public and internal tasks |
| Claude Sonnet 5.5 | Anthropic API | $2 / $10 | Complex reasoning that crosses the higher quality thresholds |
| Claude Opus 5.5 | Anthropic API | $4 / $20 | Best-quality mode, or when only Opus clears the bar |
List prices are from the Claude API reference (cached 2026-10-06) and live in src/core/models/cloud-catalog.ts.
| Classification | Default rule | Human review |
|---|---|---|
| Public | Local or any configured cloud provider | Only if other rules fire |
| Internal | Local or an approved cloud provider (Anthropic) | Only if other rules fire |
| Confidential | Prefer local; cloud only with an explicit permission and a written justification, which is recorded in the trace | Always |
| Restricted | Local only | Always |
Policy is editable on the Policies page, which also shows a live "who may process what" matrix computed by the same function the router uses. Some guardrails are enforced in code and can't be relaxed there:
Outputs are held for a person to approve when:
Reviewers approve or reject in Approvals, and the decision and note are appended to the request's trace. On the public demo, approvals work but store no visitor-written notes.
The interface always shows which one produced an answer.
| Badge | Meaning |
|---|---|
| LOCAL | Real inference on this device via Ollama. Tokens and timings come from Ollama itself. External API cost is $0.00. |
| DEMO | A cloud model was selected, but MockProvider answered because live mode is off. Output is marked with a "Simulated output" notice; tokens, latency and cost are labeled estimated; simulated spend is tracked separately and never added to external API spend. |
| LIVE API | A real, paid cloud call. Usage and cost come from the provider's reported tokens at list price. |
Zero API cost is not zero compute cost. The dashboard says External API cost: $0.00 for local runs, not "free".
Built for a total paid-testing budget of about $10–20:
AI_LIVE_MODE=true and DEFAULT_PROVIDER=live, plus an API key. The default is DEMO.SESSION_BUDGET_USD, default $2) and a lifetime cap on real spend (LIFETIME_BUDGET_USD, default $10).allowLive: true per request.max_tokens comes from the router's estimate, capped by LIVE_MAX_OUTPUT_TOKENS.maxRetries: 0), the same model is never retried, and there are at most 3 attempts per request.fetch with one that throws, and cloud providers refuse to run under the test runner.fallbacks parameter is deliberately not used, because a silent model substitution would bypass governance and cost accounting. Refusals are surfaced and recorded instead.The same codebase builds in two modes. The public build is a portfolio demo: the classifier, sensitivity scanner, governance policy, routing scores, Budget Guard, fallback chains, review rules and request trace all run for real on every request, but no model is ever called. No provider credentials are used, there is no route to Ollama or to the developer's machine, and nothing a visitor does can create spend.
| Local install (private) | Public portfolio demo | |
|---|---|---|
| Routing, governance, Budget Guard, trace | Real | Real (same code) |
| Local models | Real inference through Ollama on localhost | Simulated with reference profiles; replays of real recorded Gemma 4 12B / Qwen 3.5 9B outputs where one exists for that exact prompt, otherwise an explicit notice |
| Cloud models | DEMO by default; real Anthropic calls with two-key live mode | Simulated (MockProvider), labeled, never evaluated or recommended |
| API keys | Server-side, .env.local | Never read, even if one is set on the host |
| Storage | SQLite file in data/ | In-memory SQLite (node:sqlite), re-seeded per server instance |
| Policies, models, demo data | Editable | Read-only |
| Network exposure | Binds to 127.0.0.1, loopback Host allowlist | Public, read-only, rate-limited per IP |
One function, getDeploymentCapabilities() in src/config/deployment.ts, decides what the deployment may do, and every layer asks it. It fails closed: the deployment is portfolio if any signal says so.
next.config.ts inlines BUILD_DEPLOYMENT_MODE into every bundle. Any build on Netlify, Vercel or Cloudflare Pages is portfolio, and so is any DEPLOYMENT_MODE other than local.NETLIFY, AWS_LAMBDA_FUNCTION_NAME, …). A hosted environment can only tighten the mode, never loosen it.loadConfig() never reads ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY, forces live mode off, fixes the demo budgets, and logs any setting it ignored.provider, model or apiKey are rejected with 400); allowLive and maxLiveCostUsd are refused with 403 "Execution unavailable in Portfolio Mode…"; configuration endpoints return 403; mutations need a matching Origin.tests/services/portfolio.test.ts covers this with hostile settings: leaked keys, live flags and a reachable fake Ollama. All six scenarios run with zero Ollama calls and zero paid-provider calls, and direct route calls can't bypass the checks.
flowchart LR
subgraph LOCAL["Local install · developer's machine"]
B1[Browser<br/><sub>127.0.0.1</sub>] --> P1[proxy.ts<br/><sub>loopback Host + Origin</sub>]
P1 --> R1[API routes<br/><sub>strict zod</sub>]
R1 --> X1[Executor<br/><sub>classify → govern → route → budget</sub>]
X1 -->|local| O1[Ollama · localhost:11434<br/><sub>Gemma 4 12B · Qwen 3.5 9B</sub>]
X1 -->|cloud · two-key live mode| A1[Anthropic API<br/><sub>worst-case reservation</sub>]
X1 -->|cloud · default| M1[MockProvider · DEMO]
X1 --> D1[(SQLite file · data/)]
end
flowchart LR
subgraph PUBLIC["Public portfolio · Netlify"]
B2[Visitor browser] --> P2[Edge: proxy.ts<br/><sub>Origin check</sub>]
P2 --> R2[Server function<br/><sub>strict zod · rate limit · 403 on live flags</sub>]
R2 --> X2[Same executor<br/><sub>classify → govern → route → budget</sub>]
X2 -->|local candidates| S2[Simulated local<br/><sub>recorded real outputs or labeled notice</sub>]
X2 -->|cloud candidates| M2[MockProvider<br/><sub>labeled simulation</sub>]
X2 --> D2[(In-memory SQLite<br/><sub>node:sqlite · seeded</sub>)]
end
R2 -. no keys read · no provider clients · no Ollama client .- N2[Nothing outbound]
Netlify builds Next.js 16 with its OpenNext-based adapter (Next.js Runtime v5, installed automatically). netlify build was run locally against this repository: it produced the server function plus an Edge Function for proxy.ts, and the bundle passed npm run verify:portfolio under netlify serve.
.env* and data/ are gitignored).ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY and their base URLs into every function, billed to your Netlify credits. Portfolio mode never reads them, but they shouldn't exist on a public demo.netlify.toml already sets the build command, publish directory, NODE_VERSION = "24" and the portfolio variables. Optionally add PUBLIC_GITHUB_URL / PUBLIC_LINKEDIN_URL for footer links; never add a provider key.gh repo clone … && npm ci && netlify deploy --build --site <id> --alias preview, never from a working folder that holds .env.local or data/) or lock auto-publishing before connecting the repo.npm run verify:portfolio -- https://<draft-url>; it exits non-zero if any guarantee fails. The function log should show [control-plane] portfolio mode and AI-provider variables in environment: none (the server logs the names it finds, never values).Search indexing. Preview, draft, branch and permalink hosts (<alias>--<project>.netlify.app) get X-Robots-Tag: noindex, decided per request in proxy.ts. The production hostname and custom domains stay indexable, even when a verified draft is later published unchanged.
Under netlify serve, refused requests show as 404 rather than 403: the CLI's local proxy retries static paths on any 403. The verify script accepts any refusal status; on the deployed preview, check that /api/execute with allowLive returns the 403 JSON message.
Portfolio environment variables (all optional; Netlify site settings):
| Variable | Value | Notes |
|---|---|---|
DEPLOYMENT_MODE | portfolio | Set in netlify.toml. Any Netlify build is portfolio regardless. |
AI_LIVE_MODE / DEFAULT_PROVIDER | false / mock | Set in netlify.toml; ignored in portfolio mode anyway. |
NODE_VERSION | 24 | Set in netlify.toml. Functions run on the build's Node version; node:sqlite needs 22.13+. |
PUBLIC_GITHUB_URL, PUBLIC_LINKEDIN_URL | https://… | Optional footer links; non-https values are dropped. |
ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY | never set | Would be ignored and logged by name, but keys don't belong on a public site. Netlify AI Gateway sets them unless AI features are off. |
Preview the public build locally (separate output folder, so the local build in .next is untouched):
npm run build:portfolio && npm run start:portfolio # http://127.0.0.1:3001
npm run verify:portfolio -- http://127.0.0.1:3001
Portfolio limitations. History lives in memory per server instance, so it resets on cold starts and differs between instances; a visitor's own requests stay viewable from their browser session. Recorded local outputs exist only for the built-in scenario and seed prompts; any other prompt gets a labeled notice instead of text. Rate limits are per instance. Timestamps are shown in the visitor's time zone (the server renders UTC first, then the browser switches), but the overview chart groups requests by the server's day, which is UTC on Netlify.
Requirements: Node.js 22.13+ (developed on 24), and optionally Ollama with at least one chat model.
npm install
cp .env.example .env.local # optional: defaults are safe (DEMO mode)
npm run dev # http://127.0.0.1:3000
On first run the app seeds a week of labeled demo workload (routed by the real engine, execution simulated) so the dashboard isn't empty. Filter it out with Real executions, or run npm run seed:reset -- --clear.
Local models. With Ollama running, installed models are discovered automatically (ollama list). Pull another and it registers on the next request:
ollama pull llama3.2:3b
Live cloud calls (optional, paid):
# .env.local
AI_LIVE_MODE=true
DEFAULT_PROVIDER=live
ANTHROPIC_API_KEY=sk-ant-...
SESSION_BUDGET_USD=0.50
Restart the server. The top bar switches from Cloud · DEMO to Cloud · LIVE API. Set either switch back to disable paid calls.
| Command | |
|---|---|
npm test | Unit and service tests (no network, no paid APIs) |
npm run test:ollama | Optional integration tests against your local Ollama (free, ~1 min) |
npm run typecheck · npm run lint · npm run build | |
npm run seed:reset | Regenerate the demo workload; real executions are untouched |
npm run build:portfolio · npm run start:portfolio | Build and serve the public portfolio variant on port 3001 |
npm run verify:portfolio -- <url> | Check a running portfolio deployment from the outside (simulated requests only) |
A modular monolith. Business logic has no framework imports, so it is unit-tested directly; React components only render.
src/
core/ pure TypeScript: no I/O, no framework
classification/ task type, complexity, business risk
security/ sensitivity scanner, redaction, classification order
governance/ policy, per-model policy evaluation, human-review rules
routing/ gates, scoring, selection, budget guard, fallback chain
pricing/ budget/ token estimates, list-price cost, worst-case reservation
evaluation/ heuristic output checks, comparison recommendation
models/ cloud catalog, local family profiles
providers/ one AIProvider interface for all of them
ollama/ anthropic/ openai/ google/ mock/
services/ orchestration and persistence (SQLite)
execution/ plan → execute → fallback → evaluate → review → trace
observability/ execution log, analytics queries
approvals/ budget/ registry/ settings/ cache/ seed/
server/ composition root (server-only), API schemas, queries
app/ Next.js pages and API routes
components/ UI
tests/ Vitest: core, providers (mocked HTTP), services (in-memory SQLite)
interface AIProvider {
readonly id: ProviderId;
readonly kind: "local" | "cloud" | "simulated";
execute(request: AIRequest): Promise<AIResponse>;
health(): Promise<ProviderHealth>;
}
Ollama specifics stay inside providers/ollama: requests run with thinking disabled (on this hardware thinking turned 10-second answers into multi-minute waits), an explicit num_ctx, and keep_alive. Load, prompt-eval and generation timings are read from Ollama's response and feed back into the router's latency estimates. Modelfile aliases that share weights (for example gemma-chat → gemma4:12b-it-qat) are folded into one registry entry.
Stack: Next.js 16 (App Router, Cache Components / Partial Prerendering), React 19, TypeScript, Tailwind CSS 4, Radix primitives, better-sqlite3, zod, Vitest, Playwright (screenshots), @anthropic-ai/sdk. Charts are hand-built SVG/HTML with a colorblind-validated palette (local = green, cloud = blue, human review = amber, blocked = rose; red is reserved for failures). Light theme by default, dark one click away; see docs/design-system.md.
What it does:
server-only composition root); the browser receives a key-free status view.127.0.0.1, and local installs accept only loopback Host headers (DNS-rebinding protection).What it doesn't do, honestly: