openprism 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ba1lly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: openprism
3
+ Version: 0.1.0
4
+ Summary: Many diverse model voices, split and recombined into one judged answer (Fusion-style panel + judge) — across your own provider keys or any model in opencode.
5
+ Author: ba1lly
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ba1lly/OpenPrism
8
+ Project-URL: Repository, https://github.com/ba1lly/OpenPrism
9
+ Keywords: llm,mcp,opencode,claude-code,ensemble,fusion,ai
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: openai<3,>=1.40
14
+ Requires-Dist: anthropic<1,>=0.40
15
+ Requires-Dist: python-dotenv<2,>=1.0
16
+ Requires-Dist: mcp<2,>=1.2
17
+ Requires-Dist: httpx<1,>=0.27
18
+ Dynamic: license-file
19
+
20
+ # OpenPrism
21
+
22
+ > Many diverse model voices, split and recombined into one judged answer.
23
+
24
+ ![CI](https://github.com/ba1lly/OpenPrism/actions/workflows/ci.yml/badge.svg)
25
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
26
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)
27
+
28
+ OpenPrism sends your prompt to a **panel** of different models in parallel, then a
29
+ **judge** model reconciles their answers, an explicit consensus / contradiction /
30
+ gaps / blind-spots pass, then a single response better than any individual model
31
+ produced. It's the [OpenRouter "Fusion"](https://openrouter.ai/blog) pattern as a
32
+ small, self-hostable tool.
33
+
34
+ **Works with:** Claude Code · opencode · Cursor · Windsurf · Gemini CLI · Codex —
35
+ anything that speaks MCP. It runs on models you already have: **any model
36
+ configured in [opencode](https://opencode.ai)** (75+ providers, recommended — zero
37
+ extra keys), or your own OpenAI-compatible provider keys.
38
+
39
+ ---
40
+
41
+ ## Why it works
42
+
43
+ - **Diversity beats raw strength.** Independent models from different houses catch
44
+ different things; a judge that reconciles them outperforms any single model on
45
+ open-ended problems.
46
+ - **Parallel panel.** Wall-clock ≈ the slowest panelist, not the sum — so a 4th
47
+ model is nearly free in latency.
48
+ - **Use it deliberately.** A call costs ~slowest-model + judge. Point it at *hard*
49
+ questions (research, architecture, ambiguous trade-offs), not routine work.
50
+ - **Diversity is measured in model *families*, and never silently faked.** OpenPrism
51
+ picks default panels by family (Anthropic / OpenAI / Google / Qwen / GLM / Kimi /
52
+ MiniMax / …), so two models from the same maker don't count as two voices. Each
53
+ run is graded: **`full`** (all panelists answered, ≥ `OPENPRISM_MIN_FAMILIES`
54
+ distinct families), **`degraded`** (a panelist failed but the family floor still
55
+ holds), or **`stop`** (fewer than the floor answered — effectively a single-family
56
+ monoculture). The grade always prints; it's never silently passed off as
57
+ confident, and `OPENPRISM_STRICT=1` refuses a `stop` outright.
58
+
59
+ ## How it works
60
+
61
+ ```
62
+ ┌──────────── panel (parallel) ────────────┐
63
+ prompt ───▶ │ model A model B model C model D │ ──▶ judge ──▶ answer
64
+ └───────────────────────────────────────────┘ (reconcile +
65
+ (different providers / houses) synthesize)
66
+ ```
67
+
68
+ Two **backends** decide where panel calls go (set with `OPENPRISM_BACKEND`):
69
+
70
+ | Backend | Models from | Use |
71
+ |---|---|---|
72
+ | `opencode` **(default, recommended)** | **every provider/model authed in opencode** — discovered live, nothing hardcoded, no keys in OpenPrism | easiest; just `opencode auth login` your providers |
73
+ | `direct` | your own OpenAI-compatible provider keys (`.env` / `providers.json`) | when you don't run opencode |
74
+
75
+ Two **modes**:
76
+
77
+ - **research** (default) — Fusion-style synthesis. This is where the lift is proven.
78
+ - **code** — code is verifiable, so blending prose is wrong: each model gives a
79
+ candidate, the judge does **best-of-N selection + repair** and emits a *Verify by:*
80
+ command. (v1 doesn't auto-run tests yet.)
81
+
82
+ ## Install
83
+
84
+ Pick whichever fits — all cross-platform (Linux / macOS / Windows / WSL).
85
+
86
+ **A. Claude Code marketplace** (easiest):
87
+ ```
88
+ /plugin marketplace add ba1lly/OpenPrism
89
+ /plugin install openprism@openprism
90
+ ```
91
+
92
+ **B. Any MCP host via `uvx`** (needs [`uv`](https://docs.astral.sh/uv/); no clone, no venv).
93
+ Generate the config for your host with the built-in wizard:
94
+ ```bash
95
+ uvx --from git+https://github.com/ba1lly/OpenPrism openprism init --host opencode
96
+ # ...or --host cursor | windsurf | gemini | codex | claude-code
97
+ ```
98
+ It prints (or `--write`s) the exact MCP config block, with the server launching
99
+ via `uvx --from git+…` — works straight from GitHub, nothing to maintain. (After a
100
+ PyPI release this shortens to `uvx openprism …`; pass `--pypi` to emit that form.)
101
+
102
+ **C. From source** (for development):
103
+ ```bash
104
+ git clone https://github.com/ba1lly/OpenPrism && cd OpenPrism
105
+ ./install.sh # Linux / macOS / WSL (Windows: ./install.ps1)
106
+ ```
107
+ The installer creates a venv, installs OpenPrism, seeds `.env`, and prints host
108
+ config. Use `openprism init --host <h> --local` to point a host at this checkout.
109
+
110
+ Then add a provider key to `.env` (`direct` backend) **or** set
111
+ `OPENPRISM_BACKEND=opencode` to use opencode's providers with no keys here.
112
+
113
+ ### Judge options
114
+
115
+ | `OPENPRISM_JUDGE_BACKEND` | `OPENPRISM_JUDGE_MODEL` | Notes |
116
+ |---|---|---|
117
+ | `claude-code` (default) | alias, e.g. `opus` | `claude -p` headless via a Claude Max plan |
118
+ | `anthropic-api` | `opus` or a model id | metered Anthropic API key |
119
+ | `opencode` | `provider/model` ref | routes the judge through opencode too |
120
+
121
+ ## Usage (CLI)
122
+
123
+ ```bash
124
+ openprism "Best architecture for X, and the trade-offs?" # research
125
+ openprism "Implement a token-bucket rate limiter" --mode code
126
+ openprism "..." --panel "google/gemini-2.5-flash,anthropic/claude-haiku-4-5"
127
+ openprism --bakeoff modelA modelB "a hard prompt" # which model wins?
128
+ openprism --list # presets + known models
129
+ openprism doctor # check your setup
130
+ ```
131
+
132
+ With `OPENPRISM_BACKEND=opencode`, panels are `provider/model` refs; with no
133
+ `--panel`, OpenPrism auto-picks a diverse default across your connected providers.
134
+
135
+ ## Use as a plugin
136
+
137
+ OpenPrism is one MCP server with four tools — `research`, `code`, `bakeoff`,
138
+ `models` — so every host calls the same core. `openprism init --host <host>`
139
+ generates the right config for each:
140
+
141
+ | Host | Config file | Tools surface as |
142
+ |---|---|---|
143
+ | Claude Code | marketplace, or `.mcp.json` | `mcp__openprism__research`, …; commands `/openprism*` |
144
+ | opencode | `~/.config/opencode/opencode.json` | `openprism_research`, …; commands `/openprism*` |
145
+ | Cursor | `~/.cursor/mcp.json` | `openprism` MCP tools |
146
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | `openprism` MCP tools |
147
+ | Gemini CLI | `~/.gemini/settings.json` | `openprism` MCP tools |
148
+ | Codex | `~/.codex/config.toml` | `openprism` MCP tools |
149
+
150
+ ```bash
151
+ openprism init --host cursor # print the block
152
+ openprism init --host opencode --write # merge it into the config file (makes a .bak)
153
+ ```
154
+
155
+ For opencode, also copy the command files for slash commands:
156
+ ```bash
157
+ cp integrations/opencode/commands/*.md ~/.config/opencode/commands/
158
+ ```
159
+ Use `/openprism-models` (or the `models` tool) to see which `provider/model` refs
160
+ are connected.
161
+
162
+ ### Panelist tools (opencode backend)
163
+
164
+ Like Fusion, panelists can use **live web** tools. On the opencode backend the
165
+ panel runs with `OPENPRISM_PANEL_TOOLS`:
166
+
167
+ | Value | Panelists get |
168
+ |---|---|
169
+ | `research` (default, research mode) | `webfetch` + `websearch` + read-only; **all mutation tools denied** (`write`/`edit`/`apply_patch`/`bash`/`task`/`todowrite`) |
170
+ | `none` | pure completions (model knowledge only) — used for `code`/`bakeoff` |
171
+ | `all` | every opencode tool incl. `bash`/`edit`/`write` — **opt-in, runs on your machine** |
172
+
173
+ The `direct` backend is plain completions and ignores tools.
174
+
175
+ ## Adding models & providers
176
+
177
+ **opencode backend (recommended) — you don't add anything to OpenPrism.** Add the
178
+ provider *in opencode* and it appears automatically:
179
+ ```bash
180
+ opencode auth login # pick a provider, paste its key / OAuth
181
+ ```
182
+ Then in any host run `/openprism-models` (or the `models` tool) to see every
183
+ `provider/model` ref now available, and use them:
184
+ ```bash
185
+ openprism "…" --panel "anthropic/claude-opus-4-7,google/gemini-3-pro,openai/gpt-5.2"
186
+ ```
187
+ No `--panel`? OpenPrism auto-picks a diverse default across your connected providers.
188
+
189
+ **direct backend — add providers to OpenPrism.** The default provider is set in
190
+ `.env` (`ALIBABA_API_KEY` + `ALIBABA_BASE_URL`). Add more OpenAI-compatible
191
+ providers in `providers.json` (copy [`providers.json.example`](providers.json.example)):
192
+ ```json
193
+ { "openrouter": { "base_url": "https://openrouter.ai/api/v1",
194
+ "api_key": "env:OPENROUTER_API_KEY",
195
+ "models": ["anthropic/claude-opus-4", "google/gemini-2.5-pro"] } }
196
+ ```
197
+ Reference them as `provider/model` in `--panel`.
198
+
199
+ **Judge model** is independent of the panel — set `OPENPRISM_JUDGE_BACKEND` +
200
+ `OPENPRISM_JUDGE_MODEL` (see the [judge table](#judge-options)).
201
+
202
+ ## Health check & uninstall
203
+
204
+ ```bash
205
+ openprism doctor # checks backend, opencode server, judge, keys — prints no secrets
206
+ ./uninstall.sh # or ./uninstall.ps1 — removes venv/build; leaves your .env
207
+ ```
208
+
209
+ ## Configuration
210
+
211
+ All via env vars (`.env` in the repo root — see [`.env.example`](.env.example)):
212
+ `OPENPRISM_BACKEND`, `OPENPRISM_JUDGE_BACKEND`, `OPENPRISM_JUDGE_MODEL`,
213
+ `ANTHROPIC_API_KEY` (for the `anthropic-api` judge), `OPENPRISM_DEFAULT_PROVIDER`
214
+ (which `direct` provider a bare model id uses), `OPENPRISM_PANEL_TOOLS`
215
+ (research/none/all), `OPENPRISM_MIN_FAMILIES` (diversity floor, default 2),
216
+ `OPENPRISM_STRICT` (refuse below the floor), `OPENPRISM_MAX_PANEL` (size cap),
217
+ `OPENPRISM_PANEL_TIMEOUT`, `OPENPRISM_MAX_TOKENS`, and the opencode-backend
218
+ `OPENPRISM_OPENCODE_*` settings. Extra `direct`-backend providers go in
219
+ `providers.json` (see [`providers.json.example`](providers.json.example)).
220
+
221
+ ## Security notes
222
+
223
+ - OpenPrism never logs provider keys. The opencode backend queries only the
224
+ keyless `/provider` endpoint (never `/config/providers`, which would return keys),
225
+ and the judge always runs tool-less. Still, bind the opencode server to localhost
226
+ and set `OPENCODE_SERVER_PASSWORD` if you expose it. See [SECURITY.md](SECURITY.md).
227
+ - Keep `.env` and `providers.json` out of git (already in `.gitignore`).
228
+
229
+ ## Roadmap
230
+
231
+ - Publish to PyPI so the launch shortens from `uvx --from git+… openprism-mcp` to
232
+ `uvx --from openprism openprism-mcp` (and the CLI to `uvx openprism …`).
233
+ - `code` mode: optional sandboxed test execution (`--verify "<cmd>"`).
234
+ - Panelist tools on the `direct` backend (a tool-call loop, so non-opencode panels
235
+ can browse too).
236
+ - Smarter default-panel selection (capability-aware, not just provider-diverse).
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,221 @@
1
+ # OpenPrism
2
+
3
+ > Many diverse model voices, split and recombined into one judged answer.
4
+
5
+ ![CI](https://github.com/ba1lly/OpenPrism/actions/workflows/ci.yml/badge.svg)
6
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
7
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)
8
+
9
+ OpenPrism sends your prompt to a **panel** of different models in parallel, then a
10
+ **judge** model reconciles their answers, an explicit consensus / contradiction /
11
+ gaps / blind-spots pass, then a single response better than any individual model
12
+ produced. It's the [OpenRouter "Fusion"](https://openrouter.ai/blog) pattern as a
13
+ small, self-hostable tool.
14
+
15
+ **Works with:** Claude Code · opencode · Cursor · Windsurf · Gemini CLI · Codex —
16
+ anything that speaks MCP. It runs on models you already have: **any model
17
+ configured in [opencode](https://opencode.ai)** (75+ providers, recommended — zero
18
+ extra keys), or your own OpenAI-compatible provider keys.
19
+
20
+ ---
21
+
22
+ ## Why it works
23
+
24
+ - **Diversity beats raw strength.** Independent models from different houses catch
25
+ different things; a judge that reconciles them outperforms any single model on
26
+ open-ended problems.
27
+ - **Parallel panel.** Wall-clock ≈ the slowest panelist, not the sum — so a 4th
28
+ model is nearly free in latency.
29
+ - **Use it deliberately.** A call costs ~slowest-model + judge. Point it at *hard*
30
+ questions (research, architecture, ambiguous trade-offs), not routine work.
31
+ - **Diversity is measured in model *families*, and never silently faked.** OpenPrism
32
+ picks default panels by family (Anthropic / OpenAI / Google / Qwen / GLM / Kimi /
33
+ MiniMax / …), so two models from the same maker don't count as two voices. Each
34
+ run is graded: **`full`** (all panelists answered, ≥ `OPENPRISM_MIN_FAMILIES`
35
+ distinct families), **`degraded`** (a panelist failed but the family floor still
36
+ holds), or **`stop`** (fewer than the floor answered — effectively a single-family
37
+ monoculture). The grade always prints; it's never silently passed off as
38
+ confident, and `OPENPRISM_STRICT=1` refuses a `stop` outright.
39
+
40
+ ## How it works
41
+
42
+ ```
43
+ ┌──────────── panel (parallel) ────────────┐
44
+ prompt ───▶ │ model A model B model C model D │ ──▶ judge ──▶ answer
45
+ └───────────────────────────────────────────┘ (reconcile +
46
+ (different providers / houses) synthesize)
47
+ ```
48
+
49
+ Two **backends** decide where panel calls go (set with `OPENPRISM_BACKEND`):
50
+
51
+ | Backend | Models from | Use |
52
+ |---|---|---|
53
+ | `opencode` **(default, recommended)** | **every provider/model authed in opencode** — discovered live, nothing hardcoded, no keys in OpenPrism | easiest; just `opencode auth login` your providers |
54
+ | `direct` | your own OpenAI-compatible provider keys (`.env` / `providers.json`) | when you don't run opencode |
55
+
56
+ Two **modes**:
57
+
58
+ - **research** (default) — Fusion-style synthesis. This is where the lift is proven.
59
+ - **code** — code is verifiable, so blending prose is wrong: each model gives a
60
+ candidate, the judge does **best-of-N selection + repair** and emits a *Verify by:*
61
+ command. (v1 doesn't auto-run tests yet.)
62
+
63
+ ## Install
64
+
65
+ Pick whichever fits — all cross-platform (Linux / macOS / Windows / WSL).
66
+
67
+ **A. Claude Code marketplace** (easiest):
68
+ ```
69
+ /plugin marketplace add ba1lly/OpenPrism
70
+ /plugin install openprism@openprism
71
+ ```
72
+
73
+ **B. Any MCP host via `uvx`** (needs [`uv`](https://docs.astral.sh/uv/); no clone, no venv).
74
+ Generate the config for your host with the built-in wizard:
75
+ ```bash
76
+ uvx --from git+https://github.com/ba1lly/OpenPrism openprism init --host opencode
77
+ # ...or --host cursor | windsurf | gemini | codex | claude-code
78
+ ```
79
+ It prints (or `--write`s) the exact MCP config block, with the server launching
80
+ via `uvx --from git+…` — works straight from GitHub, nothing to maintain. (After a
81
+ PyPI release this shortens to `uvx openprism …`; pass `--pypi` to emit that form.)
82
+
83
+ **C. From source** (for development):
84
+ ```bash
85
+ git clone https://github.com/ba1lly/OpenPrism && cd OpenPrism
86
+ ./install.sh # Linux / macOS / WSL (Windows: ./install.ps1)
87
+ ```
88
+ The installer creates a venv, installs OpenPrism, seeds `.env`, and prints host
89
+ config. Use `openprism init --host <h> --local` to point a host at this checkout.
90
+
91
+ Then add a provider key to `.env` (`direct` backend) **or** set
92
+ `OPENPRISM_BACKEND=opencode` to use opencode's providers with no keys here.
93
+
94
+ ### Judge options
95
+
96
+ | `OPENPRISM_JUDGE_BACKEND` | `OPENPRISM_JUDGE_MODEL` | Notes |
97
+ |---|---|---|
98
+ | `claude-code` (default) | alias, e.g. `opus` | `claude -p` headless via a Claude Max plan |
99
+ | `anthropic-api` | `opus` or a model id | metered Anthropic API key |
100
+ | `opencode` | `provider/model` ref | routes the judge through opencode too |
101
+
102
+ ## Usage (CLI)
103
+
104
+ ```bash
105
+ openprism "Best architecture for X, and the trade-offs?" # research
106
+ openprism "Implement a token-bucket rate limiter" --mode code
107
+ openprism "..." --panel "google/gemini-2.5-flash,anthropic/claude-haiku-4-5"
108
+ openprism --bakeoff modelA modelB "a hard prompt" # which model wins?
109
+ openprism --list # presets + known models
110
+ openprism doctor # check your setup
111
+ ```
112
+
113
+ With `OPENPRISM_BACKEND=opencode`, panels are `provider/model` refs; with no
114
+ `--panel`, OpenPrism auto-picks a diverse default across your connected providers.
115
+
116
+ ## Use as a plugin
117
+
118
+ OpenPrism is one MCP server with four tools — `research`, `code`, `bakeoff`,
119
+ `models` — so every host calls the same core. `openprism init --host <host>`
120
+ generates the right config for each:
121
+
122
+ | Host | Config file | Tools surface as |
123
+ |---|---|---|
124
+ | Claude Code | marketplace, or `.mcp.json` | `mcp__openprism__research`, …; commands `/openprism*` |
125
+ | opencode | `~/.config/opencode/opencode.json` | `openprism_research`, …; commands `/openprism*` |
126
+ | Cursor | `~/.cursor/mcp.json` | `openprism` MCP tools |
127
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | `openprism` MCP tools |
128
+ | Gemini CLI | `~/.gemini/settings.json` | `openprism` MCP tools |
129
+ | Codex | `~/.codex/config.toml` | `openprism` MCP tools |
130
+
131
+ ```bash
132
+ openprism init --host cursor # print the block
133
+ openprism init --host opencode --write # merge it into the config file (makes a .bak)
134
+ ```
135
+
136
+ For opencode, also copy the command files for slash commands:
137
+ ```bash
138
+ cp integrations/opencode/commands/*.md ~/.config/opencode/commands/
139
+ ```
140
+ Use `/openprism-models` (or the `models` tool) to see which `provider/model` refs
141
+ are connected.
142
+
143
+ ### Panelist tools (opencode backend)
144
+
145
+ Like Fusion, panelists can use **live web** tools. On the opencode backend the
146
+ panel runs with `OPENPRISM_PANEL_TOOLS`:
147
+
148
+ | Value | Panelists get |
149
+ |---|---|
150
+ | `research` (default, research mode) | `webfetch` + `websearch` + read-only; **all mutation tools denied** (`write`/`edit`/`apply_patch`/`bash`/`task`/`todowrite`) |
151
+ | `none` | pure completions (model knowledge only) — used for `code`/`bakeoff` |
152
+ | `all` | every opencode tool incl. `bash`/`edit`/`write` — **opt-in, runs on your machine** |
153
+
154
+ The `direct` backend is plain completions and ignores tools.
155
+
156
+ ## Adding models & providers
157
+
158
+ **opencode backend (recommended) — you don't add anything to OpenPrism.** Add the
159
+ provider *in opencode* and it appears automatically:
160
+ ```bash
161
+ opencode auth login # pick a provider, paste its key / OAuth
162
+ ```
163
+ Then in any host run `/openprism-models` (or the `models` tool) to see every
164
+ `provider/model` ref now available, and use them:
165
+ ```bash
166
+ openprism "…" --panel "anthropic/claude-opus-4-7,google/gemini-3-pro,openai/gpt-5.2"
167
+ ```
168
+ No `--panel`? OpenPrism auto-picks a diverse default across your connected providers.
169
+
170
+ **direct backend — add providers to OpenPrism.** The default provider is set in
171
+ `.env` (`ALIBABA_API_KEY` + `ALIBABA_BASE_URL`). Add more OpenAI-compatible
172
+ providers in `providers.json` (copy [`providers.json.example`](providers.json.example)):
173
+ ```json
174
+ { "openrouter": { "base_url": "https://openrouter.ai/api/v1",
175
+ "api_key": "env:OPENROUTER_API_KEY",
176
+ "models": ["anthropic/claude-opus-4", "google/gemini-2.5-pro"] } }
177
+ ```
178
+ Reference them as `provider/model` in `--panel`.
179
+
180
+ **Judge model** is independent of the panel — set `OPENPRISM_JUDGE_BACKEND` +
181
+ `OPENPRISM_JUDGE_MODEL` (see the [judge table](#judge-options)).
182
+
183
+ ## Health check & uninstall
184
+
185
+ ```bash
186
+ openprism doctor # checks backend, opencode server, judge, keys — prints no secrets
187
+ ./uninstall.sh # or ./uninstall.ps1 — removes venv/build; leaves your .env
188
+ ```
189
+
190
+ ## Configuration
191
+
192
+ All via env vars (`.env` in the repo root — see [`.env.example`](.env.example)):
193
+ `OPENPRISM_BACKEND`, `OPENPRISM_JUDGE_BACKEND`, `OPENPRISM_JUDGE_MODEL`,
194
+ `ANTHROPIC_API_KEY` (for the `anthropic-api` judge), `OPENPRISM_DEFAULT_PROVIDER`
195
+ (which `direct` provider a bare model id uses), `OPENPRISM_PANEL_TOOLS`
196
+ (research/none/all), `OPENPRISM_MIN_FAMILIES` (diversity floor, default 2),
197
+ `OPENPRISM_STRICT` (refuse below the floor), `OPENPRISM_MAX_PANEL` (size cap),
198
+ `OPENPRISM_PANEL_TIMEOUT`, `OPENPRISM_MAX_TOKENS`, and the opencode-backend
199
+ `OPENPRISM_OPENCODE_*` settings. Extra `direct`-backend providers go in
200
+ `providers.json` (see [`providers.json.example`](providers.json.example)).
201
+
202
+ ## Security notes
203
+
204
+ - OpenPrism never logs provider keys. The opencode backend queries only the
205
+ keyless `/provider` endpoint (never `/config/providers`, which would return keys),
206
+ and the judge always runs tool-less. Still, bind the opencode server to localhost
207
+ and set `OPENCODE_SERVER_PASSWORD` if you expose it. See [SECURITY.md](SECURITY.md).
208
+ - Keep `.env` and `providers.json` out of git (already in `.gitignore`).
209
+
210
+ ## Roadmap
211
+
212
+ - Publish to PyPI so the launch shortens from `uvx --from git+… openprism-mcp` to
213
+ `uvx --from openprism openprism-mcp` (and the CLI to `uvx openprism …`).
214
+ - `code` mode: optional sandboxed test execution (`--verify "<cmd>"`).
215
+ - Panelist tools on the `direct` backend (a tool-call loop, so non-opencode panels
216
+ can browse too).
217
+ - Smarter default-panel selection (capability-aware, not just provider-diverse).
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,9 @@
1
+ """OpenPrism — many diverse model voices, split and recombined into one judged answer.
2
+
3
+ A panel of models (your own provider keys, or any model in opencode) runs in
4
+ parallel; a judge model reconciles them. Two modes:
5
+ - research: Fusion-style synthesis (consensus / contradictions / gaps -> grounded answer)
6
+ - code: best-of-N selection + repair into one final solution
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,10 @@
1
+ """Model backends — where Prism's panel calls actually go.
2
+
3
+ - DirectBackend: Prism's own OpenAI-compatible providers (keys in .env / providers.json).
4
+ Used by Claude Code and the standalone CLI, which have no provider registry to borrow.
5
+ - OpencodeBackend: piggybacks on a running opencode server — every provider/model the
6
+ user has configured/authed in opencode, with zero hardcoding. Used in opencode.
7
+ """
8
+ from .base import Backend, ModelInfo, get_backend
9
+
10
+ __all__ = ["Backend", "ModelInfo", "get_backend"]
@@ -0,0 +1,74 @@
1
+ """Backend interface + factory.
2
+
3
+ A backend turns a model reference into a completion. Model references are always
4
+ `provider/model`; because some model ids themselves contain slashes
5
+ (e.g. opencode's `requesty/xai/grok-4`), we split on the FIRST slash only.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class ModelInfo:
15
+ ref: str # full reference, e.g. "google/gemini-2.5-flash"
16
+ provider: str # provider id
17
+ model: str # model id (may contain slashes)
18
+ name: str = "" # human display name, if known
19
+ connected: bool = True
20
+
21
+
22
+ def split_ref(ref: str, default_provider: str = "") -> tuple[str, str]:
23
+ """`provider/model` -> (provider, model). Bare ref -> (default_provider, ref)."""
24
+ if "/" in ref:
25
+ provider, model = ref.split("/", 1)
26
+ return provider, model
27
+ return default_provider, ref
28
+
29
+
30
+ class Backend(ABC):
31
+ """Async backend. Implementations must be safe to call concurrently."""
32
+
33
+ name = "base"
34
+
35
+ @abstractmethod
36
+ async def list_models(self) -> list[ModelInfo]:
37
+ ...
38
+
39
+ @abstractmethod
40
+ async def complete(
41
+ self, model_ref: str, prompt: str, system: str | None, max_tokens: int,
42
+ tools: dict | None = None,
43
+ ) -> tuple[str, int]:
44
+ """Return (text, total_tokens). Raise on failure — the panel layer
45
+ catches per-model so one failure never sinks the run. `tools` is a
46
+ name->bool map for backends that support tool use (opencode); backends
47
+ that can't (direct raw completions) ignore it."""
48
+ ...
49
+
50
+ async def default_panel(self, mode: str) -> list[str]:
51
+ """Models to use when the caller gives no explicit panel. Default: the
52
+ configured preset for `mode`. opencode overrides this dynamically."""
53
+ from .. import config
54
+
55
+ return config.resolve_panel(mode if mode in config.PANELS else None)
56
+
57
+ async def aclose(self) -> None:
58
+ pass
59
+
60
+
61
+ def get_backend(name: str | None = None) -> Backend:
62
+ """Factory. `name` overrides config.BACKEND."""
63
+ from .. import config
64
+
65
+ backend = (name or config.BACKEND).lower()
66
+ if backend == "opencode":
67
+ from .opencode import OpencodeBackend
68
+
69
+ return OpencodeBackend()
70
+ if backend == "direct":
71
+ from .direct import DirectBackend
72
+
73
+ return DirectBackend()
74
+ raise config.PrismError(f"Unknown OPENPRISM_BACKEND: {backend!r} (use 'direct' or 'opencode').")
@@ -0,0 +1,77 @@
1
+ """DirectBackend — Prism's own OpenAI-compatible providers.
2
+
3
+ Providers come from config.load_providers() (the Alibaba Coding Plan from .env by
4
+ default, plus anything in providers.json). Used by Claude Code and the CLI, which
5
+ have no host provider registry to borrow. A bare model id (no slash) resolves to
6
+ the default provider, preserving the original single-provider behaviour.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from openai import AsyncOpenAI
11
+
12
+ from .. import config
13
+ from .base import Backend, ModelInfo, split_ref
14
+
15
+
16
+ class DirectBackend(Backend):
17
+ name = "direct"
18
+
19
+ def __init__(self) -> None:
20
+ self.providers = config.load_providers()
21
+ if not self.providers:
22
+ raise config.PrismError(
23
+ "No providers configured. Set ALIBABA_API_KEY in .env (or add "
24
+ "providers.json), or use OPENPRISM_BACKEND=opencode."
25
+ )
26
+ self.default_provider = config.DEFAULT_PROVIDER or next(iter(self.providers))
27
+ if self.default_provider not in self.providers:
28
+ import sys
29
+
30
+ fallback = next(iter(self.providers))
31
+ print(f"openprism: OPENPRISM_DEFAULT_PROVIDER={self.default_provider!r} is not "
32
+ f"configured; using {fallback!r} for bare model ids", file=sys.stderr)
33
+ self.default_provider = fallback
34
+ self._clients: dict[str, AsyncOpenAI] = {}
35
+
36
+ def _client(self, provider_id: str) -> AsyncOpenAI:
37
+ if provider_id not in self.providers:
38
+ raise config.PrismError(
39
+ f"Provider {provider_id!r} not configured. Known: {list(self.providers)}"
40
+ )
41
+ if provider_id not in self._clients:
42
+ p = self.providers[provider_id]
43
+ self._clients[provider_id] = AsyncOpenAI(api_key=p.api_key, base_url=p.base_url)
44
+ return self._clients[provider_id]
45
+
46
+ async def list_models(self) -> list[ModelInfo]:
47
+ out: list[ModelInfo] = []
48
+ for pid, p in self.providers.items():
49
+ for m in p.models:
50
+ ref = m if pid == self.default_provider else f"{pid}/{m}"
51
+ out.append(ModelInfo(ref=ref, provider=pid, model=m))
52
+ return out
53
+
54
+ async def complete(
55
+ self, model_ref: str, prompt: str, system: str | None, max_tokens: int,
56
+ tools: dict | None = None,
57
+ ) -> tuple[str, int]:
58
+ # `tools` is ignored: direct providers are plain completions. Panelist tool
59
+ # use (web/fetch) requires the opencode backend.
60
+ provider_id, model = split_ref(model_ref, self.default_provider)
61
+ client = self._client(provider_id)
62
+ messages = ([{"role": "system", "content": system}] if system else []) + [
63
+ {"role": "user", "content": prompt}
64
+ ]
65
+ resp = await client.chat.completions.create(
66
+ model=model, messages=messages, max_tokens=max_tokens
67
+ )
68
+ if not getattr(resp, "choices", None):
69
+ raise RuntimeError(f"{model}: provider returned no choices")
70
+ text = resp.choices[0].message.content or ""
71
+ usage = getattr(resp, "usage", None)
72
+ tokens = getattr(usage, "total_tokens", 0) if usage else 0
73
+ return text, tokens
74
+
75
+ async def aclose(self) -> None:
76
+ for client in self._clients.values():
77
+ await client.close()