@qadanm/opencode-smart-router 0.0.0-stage → 0.4.0
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.
- package/CHANGELOG.md +118 -0
- package/CONTRIBUTING.md +96 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/SECURITY.md +174 -0
- package/config.example.json +108 -0
- package/dist/allowance.js +487 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/index.js +11111 -0
- package/dist/npm/index.js +40 -0
- package/dist/tui.js +587 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +120 -0
- package/docs/LIMITATIONS.md +213 -0
- package/docs/MODELS.md +124 -0
- package/docs/OPENDESIGN-INTEGRATION.md +635 -0
- package/docs/QA.md +142 -0
- package/docs/SHARED-CORE.md +72 -0
- package/install +75 -0
- package/package.json +92 -4
- package/scripts/install.sh +324 -0
- package/scripts/uninstall.sh +91 -0
- package/src/allowance.ts +135 -0
- package/src/core/classify/classifier.ts +305 -0
- package/src/core/config-writer.ts +167 -0
- package/src/core/config.ts +1030 -0
- package/src/core/context/strategy.ts +188 -0
- package/src/core/context-health.ts +39 -0
- package/src/core/failures.ts +398 -0
- package/src/core/history/opencode.ts +173 -0
- package/src/core/models/protocol.ts +22 -0
- package/src/core/models/registry.ts +203 -0
- package/src/core/models/seed.ts +771 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +481 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +578 -0
- package/src/core/providers/enablement.ts +143 -0
- package/src/core/providers/opendesign-credits.ts +206 -0
- package/src/core/providers/opendesign-models.ts +628 -0
- package/src/core/providers/opendesign.ts +474 -0
- package/src/core/quota/calibration.ts +166 -0
- package/src/core/quota/commandcode.ts +471 -0
- package/src/core/quota/credentials.ts +490 -0
- package/src/core/quota/ledger.ts +1273 -0
- package/src/core/quota/opencode-go.ts +148 -0
- package/src/core/quota/opendesign.ts +1043 -0
- package/src/core/quota/reconcile.ts +155 -0
- package/src/core/quota/report.ts +261 -0
- package/src/core/quota/service.ts +273 -0
- package/src/core/reprompt.ts +48 -0
- package/src/core/routing/engine.ts +919 -0
- package/src/core/types.ts +825 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1670 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +48 -0
- package/src/rpc.ts +204 -0
- package/src/server/controller.ts +3281 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +671 -0
- package/src/tui/index.tsx +168 -0
package/docs/QA.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# QA evidence
|
|
2
|
+
|
|
3
|
+
> **Current state (2026-10-29):** `npx vitest run` reports **330 tests passing**
|
|
4
|
+
> across 26 files, and `npx tsc --noEmit` is clean. The router now models three
|
|
5
|
+
> subscription providers with separate credit pools. Everything below predates
|
|
6
|
+
> that migration and is kept as a historical record of how each defect was
|
|
7
|
+
> found and fixed; wherever a figure here disagrees with the current code, the
|
|
8
|
+
> code is authoritative. See
|
|
9
|
+
> [PRO-MIGRATION-2026-10-29.md](PRO-MIGRATION-2026-10-29.md) for the current
|
|
10
|
+
> behavior.
|
|
11
|
+
|
|
12
|
+
> **Accounting follow-up (2026-10-28):** artifact `8c71c46c5f7616f950c6a3fcf5c42be2198407848cf9ffac15d01504496f8920` / commit `e8fa5ac`; **106 tests at the time**. The Command Code monthly cap figures in the historical evidence below (for example `capUsd: 70.38`) came from the usage summary and are superseded. The authoritative plan grant is **$70**, derived from the provider's `$14` five-hour and `$35` weekly caps; monthly usage is now `$70 − remaining`. See [ACCOUNTING-INVESTIGATION-2026-10-28.md](ACCOUNTING-INVESTIGATION-2026-10-28.md). See [DESKTOP-PROMOTION-2026-10-28.md](DESKTOP-PROMOTION-2026-10-28.md) and the pre-promotion [DESKTOP-DRY-RUN-2026-10-28.md](DESKTOP-DRY-RUN-2026-10-28.md). Statements below about an earlier global installation are historical.
|
|
13
|
+
>
|
|
14
|
+
> **Post-audit update:** the Option C repair is implemented on branch `option-c`. The current verification record is [QA-OPTION-C-2026-10-27.md](QA-OPTION-C-2026-10-27.md). The pre-audit results below are retained for history.
|
|
15
|
+
|
|
16
|
+
All checks below were run on this machine against the installed desktop server, not only against isolated functions. **The live plugin was disabled after the stability incident documented in [STATUS-2026-10-27.md](STATUS-2026-10-27.md). The live routing results below were captured before that rollback.**
|
|
17
|
+
|
|
18
|
+
## Automated checks
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
npm run typecheck → exit 0
|
|
22
|
+
npm test → 7 files, 69 tests passed
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Coverage includes:
|
|
26
|
+
|
|
27
|
+
- task classification, including narrow CSS, broad scope, screenshots, redesign, architecture, security, stack traces, and self-described simplicity;
|
|
28
|
+
- intelligence-floor enforcement and unclassified-model exclusion;
|
|
29
|
+
- premium protection, one-shot approval, and premium locking in Maximum Efficiency;
|
|
30
|
+
- vision hard requirements and text-only exclusion;
|
|
31
|
+
- provider-reported Go and Command Code quota parsing, including reset times and extra credits;
|
|
32
|
+
- credential discovery, owner-only permission checks, and unauthorized-key fallback;
|
|
33
|
+
- rolling five-hour, weekly, monthly, and per-model ledger accounting;
|
|
34
|
+
- cache-loss pricing and large-context affinity;
|
|
35
|
+
- manual override, pause, dry-run, and disabled-routing behavior;
|
|
36
|
+
- failure classification for context overflow, network, rate limits, auth, provider outage, tool errors, and capability failures;
|
|
37
|
+
- subagent premium caps;
|
|
38
|
+
- configuration guardrails that prevent project files from loosening privacy or spending policy;
|
|
39
|
+
- controller integration that invokes the actual prompt hook and verifies the switch occurs before the request.
|
|
40
|
+
|
|
41
|
+
## Live provider-reported quota
|
|
42
|
+
|
|
43
|
+
Captured from the installed desktop plugin after provider probes. Values are redacted and rounded.
|
|
44
|
+
|
|
45
|
+
### OpenCode Go
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"providerID": "opencode-go",
|
|
50
|
+
"label": "OpenCode Go Plus",
|
|
51
|
+
"source": "provider",
|
|
52
|
+
"rolling": { "usedPct": 0, "resetsAt": "2026-10-27T21:53:04.000Z" },
|
|
53
|
+
"weekly": { "usedPct": 40, "resetsAt": "2026-11-02T00:00:00.000Z" },
|
|
54
|
+
"monthly": { "usedPct": 70, "resetsAt": "2026-11-20T11:37:14.000Z" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Command Code
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"providerID": "commandcode",
|
|
63
|
+
"label": "Command Code GOAT",
|
|
64
|
+
"source": "provider",
|
|
65
|
+
"plan": "GOAT",
|
|
66
|
+
"planStatus": "active",
|
|
67
|
+
"rolling": { "usedPct": 0.33, "usedUsd": 0.0322, "capUsd": 14, "resetsAt": 1793156518583 },
|
|
68
|
+
"weekly": { "usedPct": 72.30, "usedUsd": 25.29, "capUsd": 35, "resetsAt": 1793428863795 },
|
|
69
|
+
"monthly": { "usedPct": 36.53, "usedUsd": 25.67, "capUsd": 70.38, "resetsAt": 1795501544000 },
|
|
70
|
+
"monthlyRemainingUsd": 44.71,
|
|
71
|
+
"extraCredits": { "purchasedUsd": 0, "freeUsd": 0, "totalUsd": 0 }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The Command Code monthly grant is the sum of remaining included credits and billing-period usage, both provider-reported. It is not assumed from the plan name. Purchased and free credits remain separate pools.
|
|
76
|
+
|
|
77
|
+
The plugin uses the Command Code CLI-compatible key because the inference key is rejected by the alpha billing routes. No key, token, email, or account identifier is included here.
|
|
78
|
+
|
|
79
|
+
## Live routing decisions
|
|
80
|
+
|
|
81
|
+
Each test used a new desktop session with a deliberately weak starting model. The router changed the session before the request, and the response came from the changed model.
|
|
82
|
+
|
|
83
|
+
| Prompt | Tier / kind | Selected route | Result |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| Narrow CSS change with explicit file and "do not run tests" | 1 / mechanical | `opencode-go/mimo-v2.6-flash` | File changed from `white` to `#111111`; tool calls survived the switch |
|
|
86
|
+
| Ordinary "reply only" prompt | 2 / unknown | `opencode-go/claude-haiku-5-5` | Completed |
|
|
87
|
+
| Redesign with a screenshot, no approval | 3 / design, vision required | `opencode-go/claude-haiku-5-5` standard fallback | Premium approval warning recorded |
|
|
88
|
+
| Same redesign, one-shot approval | 3 / design, vision required | `commandcode/meta/muse-spark-1.3` premium | One-shot consumed; premium route used |
|
|
89
|
+
| Manual switch to `commandcode/deepseek-v4.1-flash` | manual | Kept | `/router-status` reported `manual`; no override |
|
|
90
|
+
| Cross-provider switch from `opencode-go/beam-free` | 2 | `commandcode/deepseek/deepseek-v4.1-flash` | Request ran on Command Code |
|
|
91
|
+
|
|
92
|
+
A real completed tool task changed a CSS file correctly on the economy route. After the classifier was tightened, the same kind of task selected `opencode-go/mimo-v2.6-flash` (economy) and the file changed from `white` to `#111111`. This verifies that switching before admission preserves tool calls.
|
|
93
|
+
|
|
94
|
+
A live pause test set the router to paused, sent a prompt with the starting model `opencode-go/beam-free`, and confirmed the session stayed on `beam-free` and completed normally. Routing was then resumed.
|
|
95
|
+
|
|
96
|
+
## Cross-provider proof
|
|
97
|
+
|
|
98
|
+
The prompt admission hook was tested with a server that logged the pre-switch model, the post-switch model, and the model observed by the request-context hook:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{"at":"prompt.before","model":{"providerID":"opencode-go","id":"iceman-contributor-free"}}
|
|
102
|
+
{"at":"prompt.after","model":{"providerID":"commandcode","id":"deepseek/deepseek-v4.1-flash"}}
|
|
103
|
+
{"at":"context","model":{"providerID":"commandcode","id":"deepseek/deepseek-v4.1-flash"}}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This proves the switch happened before the intended request, not after it.
|
|
107
|
+
|
|
108
|
+
## Installed desktop verification
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
GET /api/plugin
|
|
112
|
+
{
|
|
113
|
+
"id": "opencode-smart-router",
|
|
114
|
+
"source": { "type": "local", "path": "~/.config/opencode/plugins/opencode-smart-router/index.ts" },
|
|
115
|
+
"features": { "server": true },
|
|
116
|
+
"state": { "status": "active" }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The native commands are present in the desktop server's command list, including `router-status`, `router-explain`, `router-report`, `router-approve`, `router-auto`, `router-pin`, and `router-mode`.
|
|
121
|
+
|
|
122
|
+
## What is provider-reported vs estimated
|
|
123
|
+
|
|
124
|
+
| Value | Source |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Go rolling/weekly/monthly percentages and reset times | Provider-reported |
|
|
127
|
+
| Command Code included credits remaining, extras, five-hour/weekly windows, resets, plan, billing period, spend | Provider-reported |
|
|
128
|
+
| Command Code per-model usage and remaining model allowance | Local estimate from observed step costs |
|
|
129
|
+
| Go per-model dollar usage | Local estimate; the provider reports percentages |
|
|
130
|
+
| Expected task cost, success probability, retry cost | Local model using catalog prices and task signals |
|
|
131
|
+
| Premium budget and spend | Local accounting |
|
|
132
|
+
|
|
133
|
+
## Cleanup
|
|
134
|
+
|
|
135
|
+
All test sessions created through the desktop API were deleted after verification. `/router-reset-metrics --confirm` was used to clear test metrics and re-bootstrap the remaining real sessions. No user conversation was deleted.
|
|
136
|
+
|
|
137
|
+
## Not verified
|
|
138
|
+
|
|
139
|
+
- GUI panel rendering: the installed desktop 2.1.4 has no GUI plugin SDK. See [LIMITATIONS.md](LIMITATIONS.md).
|
|
140
|
+
- A full desktop application process restart was not forced because it would close the user's active windows. The plugin is installed in the global config, the server reports it active, and a clean standalone server loaded it from the same global entry, which is the restart path. The persistent ledger and runtime overrides are written through OpenCode's durable plugin storage; hydration is unit-tested.
|
|
141
|
+
- A live 600K-context switch test on a forked session did not execute a prompt because the source session was busy. Large-context affinity is covered by the routing unit tests and by the cache-loss pricing tests. The live plugin now estimates a large context for forks and long-lived sessions before routing, then refines it in the background.
|
|
142
|
+
- Provider-side billing enforcement cannot be proven from a client. The router fails conservatively and never claims a guaranteed cap.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Shared-core architecture
|
|
2
|
+
|
|
3
|
+
## Verified SDK process model
|
|
4
|
+
|
|
5
|
+
The installed OpenCode 2.1.4 server was probed in an isolated HOME with 20 project locations. A probe plugin recorded `process.pid`, a `globalThis` core id, and a per-location setup counter. Results:
|
|
6
|
+
|
|
7
|
+
- 20 locations produced one PID and one `globalThis` core.
|
|
8
|
+
- `setups` increased per location while `core` and `pid` stayed constant.
|
|
9
|
+
- A second probe wrote a plugin-storage value in location 1 and read it in location 2. Location 2 read the value written by location 1, confirming OpenCode's plugin storage is process-global across locations.
|
|
10
|
+
|
|
11
|
+
This is the basis for one shared core. The probe is reproducible in the lab and does not rely on an unverified assumption for this installed version.
|
|
12
|
+
|
|
13
|
+
## Ownership
|
|
14
|
+
|
|
15
|
+
| Layer | Owns | Instances |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `SharedCore` | ledger, runtime overrides, quota cache, quota probes, session bootstrap, persistence timers, session ownership, diagnostics | exactly one per process |
|
|
18
|
+
| `RouterController` bridge | project registry, project settings, prompt/retry/header hooks, commands, RPC registration, event stream, title queue | one per loaded project location |
|
|
19
|
+
|
|
20
|
+
The shared core is stored under a process-global symbol. Each bridge calls `sharedCore.init`; the core loads storage once, creates one `QuotaService`, schedules one bootstrap, and schedules one quota initialization. Bridges never start their own timers or pollers.
|
|
21
|
+
|
|
22
|
+
## Load path
|
|
23
|
+
|
|
24
|
+
On plugin load the bridge:
|
|
25
|
+
|
|
26
|
+
1. awaits only shared storage hydration (no network, no session enumeration);
|
|
27
|
+
2. builds a seed registry immediately;
|
|
28
|
+
3. registers commands, RPC, hooks, and the event stream;
|
|
29
|
+
4. adds itself to the core;
|
|
30
|
+
5. refreshes the live model catalog in the background.
|
|
31
|
+
|
|
32
|
+
Session bootstrap is delayed one second; quota probing is delayed 1.5 seconds and runs once for the process. There is no provider request on the critical load path.
|
|
33
|
+
|
|
34
|
+
## Lifecycle
|
|
35
|
+
|
|
36
|
+
- `addBridge` registers a project and cancels any pending shutdown grace.
|
|
37
|
+
- `removeBridge` removes it; when the last bridge closes, the core waits 10 seconds before persisting and shutting down.
|
|
38
|
+
- The core object stays bound to the process after shutdown. A later setup revives it through `init` instead of creating a second core that would race on the same storage.
|
|
39
|
+
- Bridge cleanup disposes only that bridge's commands, RPC, hooks, and event stream.
|
|
40
|
+
- Shared state has bounded session ownership, ledger events, decisions, errors, and per-bridge session maps.
|
|
41
|
+
|
|
42
|
+
## Project isolation
|
|
43
|
+
|
|
44
|
+
Session and step events are process-global in this SDK. A session is claimed by the bridge that admitted its prompt. Event handlers ignore events for sessions owned by another bridge. `retry`, `model.request`, and model-selection handling are guarded the same way, so one project cannot suppress another project's retries, set its privacy headers, or mark it manual.
|
|
45
|
+
|
|
46
|
+
The prompt hook checks `projectID` before claiming a session. Title writes are filtered by project. Commands and RPC remain location-scoped, so each project exposes its own 16 router commands.
|
|
47
|
+
|
|
48
|
+
## Fail-closed policy
|
|
49
|
+
|
|
50
|
+
`routing.failClosed` defaults to `true`. When no eligible route exists and the current model is unqualified, the router:
|
|
51
|
+
|
|
52
|
+
1. writes a `BLOCKED` session title;
|
|
53
|
+
2. sends a nonintrusive notification where supported;
|
|
54
|
+
3. records a blocked decision with the reason;
|
|
55
|
+
4. throws `RouterBlockedError` from the prompt hook.
|
|
56
|
+
|
|
57
|
+
The installed server was probed with a throwing prompt hook: the prompt returned HTTP 500 and was not admitted; the session had no model and no request was sent. OpenCode does not expose a typed rejection API. The HTTP error is generic, so the title and notification are the user-facing explanation. This is a documented limitation, not a silent execution path. Manual selections and paused routing are outside the strict block.
|
|
58
|
+
|
|
59
|
+
Every unexpected routing exception is now converted into a blocked decision, a `BLOCKED` title, and a `RouterBlockedError` before admission. A malformed model definition or an engine bug can no longer fall through to an unqualified model. The opt-out is explicit: `routing.failClosed: false`.
|
|
60
|
+
|
|
61
|
+
## Quota and persistence
|
|
62
|
+
|
|
63
|
+
- One `QuotaService` uses the first bridge's global integration credentials; quota snapshots are shared.
|
|
64
|
+
- One ledger is shared by all projects. Provider-reported Go and Command Code observations remain distinct from local per-model estimates.
|
|
65
|
+
- Persistence is debounced; a write takes a snapshot, marks clean, then re-schedules if a mutation arrived during the write.
|
|
66
|
+
- No extra credits, billing changes, or unapproved provider routes are introduced.
|
|
67
|
+
|
|
68
|
+
## Limits
|
|
69
|
+
|
|
70
|
+
- The first loaded location's settings provide the shared quota/bootstrap configuration. Per-project routing thresholds and model approvals still come from each bridge.
|
|
71
|
+
- The core is process-wide only because the installed server is one process with shared `globalThis`. If a future host isolates locations in separate processes, the core must use an external coordinator or accept one core per process with process-scoped storage keys.
|
|
72
|
+
- Prompt-hook rejection is a 500 at the HTTP boundary; it is safe but not a polished typed error.
|
package/install
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# OpenCode Smart Router — zero-to-working in one command.
|
|
4
|
+
#
|
|
5
|
+
# FOR A PERSON:
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/qadanm/opencode-smart-router/main/install | bash
|
|
7
|
+
# ...or, from a clone: ./install
|
|
8
|
+
#
|
|
9
|
+
# FOR AN LLM / CI:
|
|
10
|
+
# The same command, plus --yes. It never prompts, prints one line per step, and exits
|
|
11
|
+
# non-zero with an actionable message on any failure. Every check happens before
|
|
12
|
+
# anything is modified, so a failure leaves the machine untouched.
|
|
13
|
+
#
|
|
14
|
+
# WHAT IT DOES:
|
|
15
|
+
# 1. checks node/npm/git
|
|
16
|
+
# 2. clones the repo to ~/.opencode-smart-router (or updates it)
|
|
17
|
+
# 3. runs scripts/install.sh, which builds, installs, wires the config, and verifies
|
|
18
|
+
#
|
|
19
|
+
# Re-running is safe and is the update path: it pulls the latest and reinstalls.
|
|
20
|
+
#
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
REPO_URL="${SMART_ROUTER_REPO:-https://github.com/qadanm/opencode-smart-router.git}"
|
|
24
|
+
TARGET="${SMART_ROUTER_DIR:-$HOME/.opencode-smart-router}"
|
|
25
|
+
ASSUME_YES=false
|
|
26
|
+
PASSTHRU=()
|
|
27
|
+
|
|
28
|
+
for a in "$@"; do
|
|
29
|
+
case "$a" in
|
|
30
|
+
--yes|-y) ASSUME_YES=true; PASSTHRU+=("--yes") ;;
|
|
31
|
+
*) PASSTHRU+=("$a") ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
|
36
|
+
die() { printf '\033[31m x %s\033[0m\n' "$*" >&2; exit 1; }
|
|
37
|
+
|
|
38
|
+
step "OpenCode Smart Router — bootstrap"
|
|
39
|
+
|
|
40
|
+
# Running from inside a checkout? Then that checkout IS the source. Cloning it
|
|
41
|
+
# again would either duplicate it or, on a private repo, fail with a credential
|
|
42
|
+
# prompt. `./install` is documented as the from-a-clone path, so it must work
|
|
43
|
+
# without network access to the forge.
|
|
44
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || true)"
|
|
45
|
+
if [[ -n "${SELF_DIR:-}" && -f "$SELF_DIR/scripts/install.sh" && -d "$SELF_DIR/.git" ]]; then
|
|
46
|
+
step "Installing from this checkout"
|
|
47
|
+
echo " source: $SELF_DIR"
|
|
48
|
+
exec "$SELF_DIR/scripts/install.sh" "${PASSTHRU[@]}"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
command -v git >/dev/null || die "git is required. Install it first."
|
|
52
|
+
command -v node >/dev/null || die "Node 20.12+ is required: https://nodejs.org"
|
|
53
|
+
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
|
|
54
|
+
NODE_MINOR="$(node -p 'process.versions.node.split(".")[1]')"
|
|
55
|
+
(( NODE_MAJOR > 20 || (NODE_MAJOR == 20 && NODE_MINOR >= 12) )) \
|
|
56
|
+
|| die "Node $(node -v) is too old; the router needs >= 20.12."
|
|
57
|
+
command -v npm >/dev/null || die "npm is required (it ships with Node)."
|
|
58
|
+
echo " node $(node -v), git $(git --version | awk '{print $3}')"
|
|
59
|
+
|
|
60
|
+
if [[ -d "$TARGET/.git" ]]; then
|
|
61
|
+
step "Updating existing clone at $TARGET"
|
|
62
|
+
git -C "$TARGET" fetch --quiet origin || die "could not reach $REPO_URL"
|
|
63
|
+
# Install from whatever branch the clone is on, so feature branches work too.
|
|
64
|
+
BRANCH="$(git -C "$TARGET" rev-parse --abbrev-ref HEAD)"
|
|
65
|
+
git -C "$TARGET" pull --ff-only --quiet origin "$BRANCH" \
|
|
66
|
+
|| echo " (local commits present — installing from the working tree as-is)"
|
|
67
|
+
else
|
|
68
|
+
step "Cloning into $TARGET"
|
|
69
|
+
mkdir -p "$(dirname "$TARGET")"
|
|
70
|
+
git clone --quiet "$REPO_URL" "$TARGET" \
|
|
71
|
+
|| die "clone failed. If the repo is private, set SMART_ROUTER_REPO to a URL you can reach."
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
step "Running the installer"
|
|
75
|
+
exec "$TARGET/scripts/install.sh" "${PASSTHRU[@]}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qadanm/opencode-smart-router",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Subscription-aware model routing for OpenCode: nested usage windows, multi-pool credit accounting, context policy, and self-healing routing that repairs prompts and fails over deterministic rejections.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"opencode-plugin",
|
|
8
|
+
"plugin",
|
|
9
|
+
"router",
|
|
10
|
+
"llm",
|
|
11
|
+
"quota",
|
|
12
|
+
"ai",
|
|
13
|
+
"coding-agent",
|
|
14
|
+
"anthropic",
|
|
15
|
+
"model-routing"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/qadanm/opencode-smart-router#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/qadanm/opencode-smart-router/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/qadanm/opencode-smart-router.git"
|
|
24
|
+
},
|
|
25
|
+
"author": "Moe Qadan",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.12"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./dist/npm/index.js",
|
|
33
|
+
"./source": "./src/index.ts",
|
|
34
|
+
"./rpc": "./src/rpc.ts",
|
|
35
|
+
"./allowance": "./src/allowance.ts",
|
|
36
|
+
"./tui": "./dist/tui.js",
|
|
37
|
+
"./gui-experimental": "./dist/gui-experimental.cjs"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src",
|
|
42
|
+
"scripts/install.sh",
|
|
43
|
+
"scripts/uninstall.sh",
|
|
44
|
+
"install",
|
|
45
|
+
"config.example.json",
|
|
46
|
+
"README.md",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"SECURITY.md",
|
|
50
|
+
"CONTRIBUTING.md",
|
|
51
|
+
"docs/ARCHITECTURE.md",
|
|
52
|
+
"docs/INSTALL.md",
|
|
53
|
+
"docs/LIMITATIONS.md",
|
|
54
|
+
"docs/MODELS.md",
|
|
55
|
+
"docs/OPENDESIGN-INTEGRATION.md",
|
|
56
|
+
"docs/SHARED-CORE.md",
|
|
57
|
+
"docs/QA.md",
|
|
58
|
+
"docs/DEBUGGING-GUI.md"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest",
|
|
64
|
+
"check": "npm run typecheck && npm run test",
|
|
65
|
+
"build:server": "esbuild src/index.ts --bundle --format=esm --platform=node --target=node22 --external:@opencode/plugin --external:@opencode/plugin/* --outfile=dist/index.js --legal-comments=none",
|
|
66
|
+
"build:allowance": "esbuild src/allowance.ts --bundle --format=esm --platform=neutral --target=node20 --outfile=dist/allowance.js --legal-comments=none",
|
|
67
|
+
"build:gui": "vite build --config vite.gui.config.ts",
|
|
68
|
+
"build:tui": "esbuild src/tui/index.tsx --bundle --format=esm --platform=node --target=node22 --external:@opencode/plugin --external:@opencode/plugin/* --external:@opentui/core --external:@opentui/solid --external:solid-js --outfile=dist/tui.js --legal-comments=none",
|
|
69
|
+
"build:all": "npm run build:server && npm run build:allowance && npm run build:gui && npm run build:tui",
|
|
70
|
+
"mutate": "python3 scripts/mutate.py",
|
|
71
|
+
"verify": "bash scripts/cleanroom-verify.sh",
|
|
72
|
+
"prepublishOnly": "npm run check && npm run build:npm",
|
|
73
|
+
"build:npm": "esbuild src/index.ts --bundle --format=esm --platform=node --target=node22 --packages=bundle --outfile=dist/npm/index.js --legal-comments=none --minify"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@opencode/plugin": "2.2.5"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/node": "^24.0.0",
|
|
80
|
+
"solid-js": "^1.9.18",
|
|
81
|
+
"typescript": "^5.9.3",
|
|
82
|
+
"vite": "^7.3.7",
|
|
83
|
+
"vite-plugin-solid": "^2.11.15",
|
|
84
|
+
"vitest": "^3.2.4"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"peerDependenciesMeta": {
|
|
90
|
+
"@opencode/plugin": {
|
|
91
|
+
"optional": true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|