@hypabolic/crossbar 0.1.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/ARCHITECTURE.md +168 -0
- package/CAPABILITY-MATRIX.md +49 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/RESEARCH.md +343 -0
- package/package.json +53 -0
- package/src/adapters/anthropic.ts +197 -0
- package/src/adapters/generic.ts +164 -0
- package/src/adapters/index.ts +64 -0
- package/src/adapters/llamacpp.ts +217 -0
- package/src/adapters/llamaswap.ts +276 -0
- package/src/adapters/lmstudio.ts +307 -0
- package/src/adapters/ollama.ts +340 -0
- package/src/adapters/openai.ts +195 -0
- package/src/adapters/vllm.ts +197 -0
- package/src/core/backend-adapter.ts +123 -0
- package/src/core/capability.ts +53 -0
- package/src/core/index.ts +36 -0
- package/src/core/types.ts +160 -0
- package/src/discovery/engine.ts +247 -0
- package/src/discovery/probe.ts +144 -0
- package/src/index.ts +158 -0
- package/src/registry/ids.ts +68 -0
- package/src/registry/persistence.ts +111 -0
- package/src/registry/pi-credential-store.ts +27 -0
- package/src/registry/registry.ts +150 -0
- package/src/shim/provider-shim.ts +187 -0
- package/src/ui/loaded-widget.ts +220 -0
- package/src/ui/onboarding.ts +439 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Crossbar — Architecture & Frozen Contract (Phase 1)
|
|
2
|
+
|
|
3
|
+
**Status:** Interface frozen pending sign-off · **Date:** 2026-06-21 · Companion to `RESEARCH.md`,
|
|
4
|
+
`CAPABILITY-MATRIX.md`. The contract under `src/core/` is the **single source of truth**. Phase 2
|
|
5
|
+
fan-out targets it and nothing else. Typechecked against Pi `0.79.9` (`tsc --noEmit` → clean).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
discovery engine ──┐
|
|
9
|
+
├─► server registry ──► provider shim ──► pi.registerProvider(...)
|
|
10
|
+
adapters[kind] ────┘ │ │
|
|
11
|
+
(BackendAdapter) └─► crossbar.json (metadata) └─► models appear in /model & /login
|
|
12
|
+
secrets ─► Pi authStorage (auth.json,0600)
|
|
13
|
+
onboarding overlay ─────────────────────────────────────────────────► /crossbar (alias /local)
|
|
14
|
+
loaded-model widget ─────────────────────────────────────────────────► ctx.ui.setStatus / setWidget
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 1. Module layout (Phase 2 fills these)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
index.ts # extension entry: registers /crossbar, wires session_start (STUB today)
|
|
22
|
+
core/ # FROZEN CONTRACT — do not change without bumping CONTRACT_VERSION
|
|
23
|
+
capability.ts # Capability enum, AuthMode, BackendKind
|
|
24
|
+
types.ts # Probe, DiscoveredServer, ModelDescriptor, LoadedState, ServerRecord, PiModelEntry
|
|
25
|
+
backend-adapter.ts # BackendAdapter interface + capability guards
|
|
26
|
+
index.ts # re-exports
|
|
27
|
+
adapters/ # one file per kind, each `implements BackendAdapter`
|
|
28
|
+
ollama.ts lmstudio.ts llamacpp.ts llamaswap.ts vllm.ts
|
|
29
|
+
openai.ts anthropic.ts generic.ts # generic = openai-generic fallback
|
|
30
|
+
index.ts # ADAPTERS registry (kind → instance), probe-order export
|
|
31
|
+
discovery/
|
|
32
|
+
probe.ts # Probe impl: fetch + timeout + auth-header injection + redaction
|
|
33
|
+
engine.ts # port sweep (localhost default), fingerprint dispatch, dedupe by origin
|
|
34
|
+
registry/
|
|
35
|
+
registry.ts # ServerRecord CRUD, health poll, model cache, in-memory state
|
|
36
|
+
persistence.ts # crossbar.json read/write via getAgentDir(); secrets via authStorage
|
|
37
|
+
ids.ts # stable provider-id generation (kind + host + port)
|
|
38
|
+
shim/
|
|
39
|
+
provider-shim.ts # ServerRecord + ModelDescriptor[] → pi.registerProvider / unregisterProvider
|
|
40
|
+
ui/
|
|
41
|
+
onboarding.ts # ctx.ui.custom overlay: discovered list + manual add + test + pick + save
|
|
42
|
+
loaded-widget.ts # setStatus/setWidget live "currently loaded" indicator
|
|
43
|
+
theme.ts # getSelectListTheme wrapper + token helpers
|
|
44
|
+
tests/
|
|
45
|
+
conformance/ # runs EVERY adapter against the contract using fixtures
|
|
46
|
+
fixtures/<kind>/ # captured HTTP responses per backend (+ edge cases)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 2. BackendAdapter contract (frozen)
|
|
50
|
+
|
|
51
|
+
See `src/core/backend-adapter.ts`. Invariants every adapter MUST honour — the conformance suite checks
|
|
52
|
+
each one:
|
|
53
|
+
|
|
54
|
+
1. **Stateless & I/O-free.** No instance state; never call `fetch` directly — only the injected `Probe`.
|
|
55
|
+
2. **Honest capabilities.** An optional method (`introspectLoaded`/`switchModel`/`loadUnload`/`health`)
|
|
56
|
+
is defined **iff** the matching `Capability` is in `capabilities`. Orchestrator uses the `canX`
|
|
57
|
+
guards, never feature-sniffs.
|
|
58
|
+
3. **`fingerprint` is unauth & cheap.** Uses only public metadata endpoints; returns `null` fast for
|
|
59
|
+
non-matches; sets a calibrated `confidence` (exact unique-path match → ~1.0; generic `/v1/models`
|
|
60
|
+
shape → ~0.3). Cloud adapters return `null`.
|
|
61
|
+
4. **`listModels` filters embeddings** out of chat registration (keeps them only if flagged).
|
|
62
|
+
5. **`toPiModel` owns Pi mapping.** Sets `api`, `compat` quirk flags, `cost` zeros for local, and
|
|
63
|
+
conservative defaults: `contextWindow` from backend or a safe fallback, `input` defaults `["text"]`.
|
|
64
|
+
6. **`switchModel` confirms or throws.** Must surface server-down-mid-switch and model-not-available as
|
|
65
|
+
rejections, not silent success.
|
|
66
|
+
7. **No secret leakage.** Adapters receive `ServerCredential` but must never log/serialize `apiKey`.
|
|
67
|
+
|
|
68
|
+
`CONTRACT_VERSION = 1`. Any breaking change bumps it; registry asserts adapters match.
|
|
69
|
+
|
|
70
|
+
## 3. Provider-registration shim (`shim/provider-shim.ts`)
|
|
71
|
+
|
|
72
|
+
The ONLY bridge to Pi. For each enabled, healthy server:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
register(record):
|
|
76
|
+
adapter = ADAPTERS[record.kind]
|
|
77
|
+
cred = resolveCredential(record) # apiKey from authStorage, or {mode:"none"}
|
|
78
|
+
models = registry.cachedModels(record) # from listModels, refreshed on poll
|
|
79
|
+
pi.registerProvider(record.id, {
|
|
80
|
+
name: record.label,
|
|
81
|
+
baseUrl: adapter.inferenceBaseUrl(server),
|
|
82
|
+
apiKey: cred.mode === "apiKey" ? `$${ENV_FOR(record.id)}` : undefined, # see secret note
|
|
83
|
+
api: adapter.piApi,
|
|
84
|
+
models: models.map(m => adapter.toPiModel(server, m)),
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Re-registration on model-list change = `unregisterProvider(id)` then `registerProvider(id, ...)`.
|
|
89
|
+
- Switching the served model updates the registered `models[]` (and the loaded widget), then
|
|
90
|
+
optionally `pi.setModel(...)` to point the agent at it.
|
|
91
|
+
- **Secret handling:** keys are stored via `authStorage.set(record.id, {type:"api_key", key})`. The shim
|
|
92
|
+
passes the key to Pi using Pi's own indirection (env/`!cmd`/literal) — never inlining the plaintext
|
|
93
|
+
into long-lived structures. Exact mechanism (env handoff vs literal at register time) is the first
|
|
94
|
+
thing Phase 2 verifies against `auth-storage.ts`; default to letting Pi resolve from `auth.json`.
|
|
95
|
+
|
|
96
|
+
## 4. Server registry & persistence
|
|
97
|
+
|
|
98
|
+
- **State:** `ServerRecord[]` in memory; mirror to `getAgentDir()/crossbar.json` (`CrossbarConfigFile`,
|
|
99
|
+
`version:1`). Non-secret only — never write `apiKey` here.
|
|
100
|
+
- **Secrets:** `ctx.modelRegistry.authStorage` (`auth.json`, `0600`), keyed by `record.id`.
|
|
101
|
+
- **IDs:** stable, derived from `kind + host + port` (e.g. `crossbar-ollama-localhost-11434`) so a server
|
|
102
|
+
keeps its id and key across restarts. (`registry/ids.ts`)
|
|
103
|
+
- **Health poll:** interval loop started in `session_start`, stopped in `session_shutdown` (per Pi's
|
|
104
|
+
long-lived-resource rule). Updates health, refreshes model cache, drives the loaded widget. On
|
|
105
|
+
`unreachable`, keep the registration but mark degraded; show last-known models/loaded.
|
|
106
|
+
- **Lifecycle:** `session_start` → load `crossbar.json` → (if enabled) run auto-discovery →
|
|
107
|
+
register all enabled+reachable servers → start poll. `session_shutdown` → stop poll.
|
|
108
|
+
|
|
109
|
+
## 5. Discovery engine (`discovery/`)
|
|
110
|
+
|
|
111
|
+
- **Default scope: localhost only.** Probe ports `[11434,1234,8080,8000,5000,5001,1337]` on `127.0.0.1`.
|
|
112
|
+
LAN host-range probing is opt-in via `CrossbarSettings.lanDiscovery` (no mDNS exists for any backend).
|
|
113
|
+
- **Per origin:** run the probe-order fingerprint chain (CAPABILITY-MATRIX §"probe order"); pick the
|
|
114
|
+
highest-confidence adapter; fall back to `openai-generic` when only `/v1/models` matches.
|
|
115
|
+
- **Short timeouts** (e.g. 600ms) and bounded concurrency; a refused port returns `status:0` fast.
|
|
116
|
+
- **Key-vs-no-auth:** probe public metadata first; a `401` on `/v1/chat/completions` with `200` on
|
|
117
|
+
`/v1/models` ⇒ "running but keyed" → onboarding prompts for a key.
|
|
118
|
+
|
|
119
|
+
## 6. Onboarding flow (`ui/onboarding.ts`) — `/crossbar`
|
|
120
|
+
|
|
121
|
+
Overlay via `ctx.ui.custom<T>(factory, { overlay:true, overlayOptions })` using `SelectList` +
|
|
122
|
+
`Container`/`Text`/`DynamicBorder`, themed with `getSelectListTheme` / `theme.fg`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
/crossbar →
|
|
126
|
+
[Discovered] Ollama (localhost:11434) ✓ healthy
|
|
127
|
+
LM Studio (localhost:1234) ✓ healthy
|
|
128
|
+
[Manual add] + Add server… → input URL → optional API key (no-auth toggle) → Test connection
|
|
129
|
+
→ on select/add: fingerprint → health → listModels → pick default model → save (registry + auth.json)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- Capability-driven: hide "switch model" when `!supports(SwitchModel)`; show load/unload only when
|
|
133
|
+
`LoadUnload`; show a no-auth toggle always, key entry only when needed.
|
|
134
|
+
- Test-connection uses the same `Probe` + adapter `health`/`listModels` as production.
|
|
135
|
+
- Cloud keys (OpenAI/Anthropic) can still be entered via stock `/login`; Crossbar's registered models
|
|
136
|
+
surface in `/model` regardless.
|
|
137
|
+
|
|
138
|
+
## 7. Loaded-model widget (`ui/loaded-widget.ts`)
|
|
139
|
+
|
|
140
|
+
- `ctx.ui.setStatus("crossbar-loaded", theme.fg("accent", "● <server>:<model>"))`, refreshed from the
|
|
141
|
+
health poll's `introspectLoaded` and from the `model_select` event.
|
|
142
|
+
- When `!supports(IntrospectLoaded)` (OpenAI, Anthropic, vLLM, llamafile) → show **last-known**
|
|
143
|
+
selection from `ServerRecord.lastKnownLoaded`; never claim live state we can't read (`source` field).
|
|
144
|
+
|
|
145
|
+
## 8. Conformance suite (`tests/conformance/`)
|
|
146
|
+
|
|
147
|
+
One parameterized suite runs against **every** adapter using a fake `Probe` backed by per-backend
|
|
148
|
+
fixtures. Required cases:
|
|
149
|
+
|
|
150
|
+
- fingerprint: positive, negative (other backend's response), ambiguous-port disambiguation.
|
|
151
|
+
- listModels: normal, empty, embeddings filtered, missing-caps defaults applied.
|
|
152
|
+
- introspectLoaded / switchModel / loadUnload: present ⇔ capability; success + edge cases —
|
|
153
|
+
**auth failure (401), server-down-mid-switch, model-not-loaded, streaming cutoff**.
|
|
154
|
+
- toPiModel: output validates against Pi's `ProviderConfig["models"]` element shape (compile + runtime).
|
|
155
|
+
- capability honesty: every optional method present ⇔ its `Capability` is declared.
|
|
156
|
+
|
|
157
|
+
Discovery validated on **zero / one / many** servers (Phase 3 hardening).
|
|
158
|
+
|
|
159
|
+
## 9. Phase 2 waves (contract-driven fan-out — nothing starts before sign-off)
|
|
160
|
+
|
|
161
|
+
- **Wave A (parallel):** conformance harness + fixtures · discovery engine + probe · registry +
|
|
162
|
+
persistence + id gen.
|
|
163
|
+
- **Wave B (parallel, one adapter per agent):** ollama · lmstudio · llamacpp+llamaswap · vllm ·
|
|
164
|
+
openai+anthropic · generic. Each lands with its fixtures and passes conformance before merge.
|
|
165
|
+
- **Wave C:** provider shim + `/crossbar` onboarding overlay + loaded widget; full integration; green
|
|
166
|
+
conformance across all adapters.
|
|
167
|
+
|
|
168
|
+
After each wave: reconvene, integrate, run conformance against every adapter.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Crossbar — Capability Matrix (draft, Phase 0)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-21 · companion to `RESEARCH.md`. Values: ✅ yes · ◐ partial/conditional · ❌ no.
|
|
4
|
+
Backend endpoints are **[WEB]** — confirm live per adapter. `pi api` = which built-in Pi API type the
|
|
5
|
+
adapter registers under (`oai` = `openai-completions`, `ant` = `anthropic-messages`).
|
|
6
|
+
|
|
7
|
+
| Backend | port | pi api | listModels | introspectLoaded | switchModel | loadUnload | auth | health | perModelCaps | streaming | discovery fingerprint |
|
|
8
|
+
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
9
|
+
| **Ollama** | 11434 | oai | ✅ `/api/tags`,`/v1/models` | ✅ `/api/ps` | ✅ implicit (request id) | ✅ `keep_alive:0` | ◐ none local | ✅ `GET /` text | ✅ `/api/show` caps + ctx | ✅ | `GET /` → `Ollama is running` |
|
|
10
|
+
| **LM Studio** | 1234 | oai | ✅ `/api/v0/models`,`/v1/models` | ✅ `state` field | ✅ JIT + `/api/v1/models/load` | ✅ load/unload + `lms` | ◐ Bearer, none default | ◐ infer 200 | ✅ type+`max_context_length` | ✅ | `/api/v0/models` w/ `state`,`compatibility_type` |
|
|
11
|
+
| **llama-server** | 8080 | oai | ✅ `/v1/models` | ◐ `/props`,`/slots` (single) | ❌ (1/instance) | ❌ classic | ◐ none / `--api-key` | ✅ `/health` | ◐ ctx via `/props`,`meta` | ✅ | `/props` w/ `default_generation_settings`+`build_info` |
|
|
12
|
+
| **llama-swap** | 8080 | oai/ant | ✅ `/v1/models` (all config) | ✅ `/running` | ✅ via `model` → restart upstream | ✅ `/api/models/unload`, ttl | ◐ optional multi-scheme | ✅ `/health`→OK | ◐ via upstream | ✅ | `/` → `/ui/`; `/running`,`/upstream/{model}` |
|
|
13
|
+
| **vLLM** | 8000 | oai | ✅ `/v1/models` | ◐ `/is_sleeping` (dev) | ❌ base · ◐ LoRA | ◐ sleep/wake + LoRA | ◐ none / `--api-key` | ✅ `/health` | ◐ `max_model_len` only | ✅ | `/version` + `/metrics` `vllm:` + `owned_by:"vllm"` |
|
|
14
|
+
| **OpenAI** | cloud | oai | ✅ `/v1/models` | ❌ | ✅ (pick id) | ❌ managed | ✅ Bearer | ❌ (status page) | ❌ (static table needed) | ✅ | n/a (configured, not probed) |
|
|
15
|
+
| **Anthropic** | cloud | ant | ✅ `/v1/models` | ❌ | ✅ (pick id) | ❌ managed | ✅ x-api-key+version | ❌ | ✅ caps + `max_input_tokens` | ✅ | n/a |
|
|
16
|
+
| **TabbyAPI** | 5000 | oai | ✅ `/v1/model/list` | ✅ `/v1/model` | ✅ load | ✅ `/v1/model/{load,unload}` | ✅ x-api-key/x-admin-key | ◐ | ◐ | ✅ | `/v1/model/*` + `x-admin-key` |
|
|
17
|
+
| **KoboldCpp** | 5001 | oai | ✅ `/v1/models` | ✅ `/api/v1/model` | ❌ (1 GGUF) | ❌ | ◐ `--password` | ✅ `/api/extra/version` | ◐ | ✅ | `/api/extra/version`→`{"result":"KoboldCpp"}` |
|
|
18
|
+
| **oobabooga** | 5000 | oai | ✅ `/v1/models` | ✅ `/v1/internal/model/info` | ✅ load | ✅ `/v1/internal/model/{load,unload}` | ◐ `--api-key` | ◐ | ◐ | ✅ | `/v1/internal/*` namespace |
|
|
19
|
+
| **Jan** | 1337 | oai | ✅ `/v1/models` | ◐ | ◐ engine | ◐ engine | ◐ Bearer | ❌ | ◐ | ✅ | weak (log line) |
|
|
20
|
+
| **llamafile** | 8080 | oai | ✅ `/v1/models` | ◐ `/props` | ❌ | ❌ | ◐ `--api-key` | ✅ `/health` | ◐ via `/props` | ✅ | `/props` w/ non-`bNNNN` build_info |
|
|
21
|
+
| **generic OpenAI-compat** | varies | oai | ✅ `/v1/models` | ❌ | ❌ | ❌ | ◐ optional Bearer | ◐ | ◐ | ✅ | anything serving `/v1/models` (fallback) |
|
|
22
|
+
|
|
23
|
+
## Capability-driven UX rules (derived)
|
|
24
|
+
|
|
25
|
+
- **switchModel ❌** (llama-server, vLLM-base, KoboldCpp, llamafile) → hide/disable the "switch model"
|
|
26
|
+
action; offer "restart server with model X" guidance only where applicable. Suggest llama-swap when a
|
|
27
|
+
bare llama-server is detected (it unlocks switching).
|
|
28
|
+
- **introspectLoaded ❌/◐** (OpenAI, Anthropic, vLLM, llamafile) → show **last-known** selected model
|
|
29
|
+
rather than a live "loaded" indicator; never claim live state we can't read.
|
|
30
|
+
- **perModelCaps ❌** (OpenAI) / ◐ (most local) → fall back to a maintained static capability table and
|
|
31
|
+
conservative defaults (`contextWindow` from `/props`/`max_*` when present, else a safe default).
|
|
32
|
+
- **auth ◐** → onboarding offers a no-auth toggle; probe public metadata endpoints first, only require a
|
|
33
|
+
key for inference (a `401` on `/v1/chat/completions` but `200` on `/v1/models` ⇒ "running but keyed").
|
|
34
|
+
- **loadUnload ✅** (Ollama, LM Studio, TabbyAPI, oobabooga, llama-swap) → expose explicit load/unload;
|
|
35
|
+
elsewhere degrade to implicit-on-use (Ollama) or nothing.
|
|
36
|
+
|
|
37
|
+
## Discovery probe order (cheapest/most-specific first)
|
|
38
|
+
|
|
39
|
+
1. `GET /` → `Ollama is running` ⇒ Ollama · redirect `/ui/` ⇒ llama-swap
|
|
40
|
+
2. `GET /api/extra/version` → `{"result":"KoboldCpp"}` ⇒ KoboldCpp
|
|
41
|
+
3. `GET /api/v0/models` 200 w/ `state`/`compatibility_type` ⇒ LM Studio
|
|
42
|
+
4. `GET /props` w/ `default_generation_settings`+`build_info` ⇒ llama-server / llamafile
|
|
43
|
+
5. `GET /version` + `/metrics` `vllm:` ⇒ vLLM
|
|
44
|
+
6. `GET /v1/models` shape: `owned_by:"vllm"`⇒vLLM · `meta.n_ctx_train`⇒llama.cpp ·
|
|
45
|
+
multiple models on :8080⇒llama-swap · `/v1/internal/*`⇒oobabooga · `/v1/model/*`⇒TabbyAPI ·
|
|
46
|
+
else ⇒ generic OpenAI-compat
|
|
47
|
+
|
|
48
|
+
Default probe ports (localhost): `11434, 1234, 8080, 8000, 5000, 5001, 1337`. **No mDNS exists for any
|
|
49
|
+
backend** → LAN discovery, if enabled, is active port-probing across host IPs (opt-in).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hypabolic
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Crossbar
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Hypabolic/Crossbar/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@hypabolic/crossbar)
|
|
5
|
+
|
|
6
|
+
**The local/self-hosted inference connector Pi should have shipped with.**
|
|
7
|
+
|
|
8
|
+
Crossbar is an extension for the [Pi coding agent](https://github.com/earendil-works/pi) that makes
|
|
9
|
+
wiring Pi to *any* local or self-hosted model backend effortless — zero hand-edited JSON, all setup
|
|
10
|
+
inside Pi's TUI, with auto-discovery, multi-server support, live "currently loaded" indicators, and
|
|
11
|
+
in-place model switching.
|
|
12
|
+
|
|
13
|
+
> Built by [Hypabolic](https://github.com/hypabolic).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why Crossbar
|
|
18
|
+
|
|
19
|
+
Existing connectors stop short: manual config files, a single server at a time, Ollama-only discovery,
|
|
20
|
+
or hardcoded model lists. Crossbar beats them on three axes:
|
|
21
|
+
|
|
22
|
+
1. **Widest backend support** — Ollama, LM Studio, llama.cpp, llama-swap, vLLM, OpenAI, Anthropic, plus
|
|
23
|
+
a generic adapter for the OpenAI-compatible long tail (TabbyAPI, KoboldCpp, text-generation-webui,
|
|
24
|
+
Jan, llamafile, …).
|
|
25
|
+
2. **Easiest onboarding** — `/crossbar` auto-discovers running servers on localhost and registers them.
|
|
26
|
+
No JSON. API-key *and* no-auth endpoints, with a connection test before you commit.
|
|
27
|
+
3. **Highest-fidelity UX** — multiple simultaneous servers, a live loaded-model widget, and capability-
|
|
28
|
+
driven model switching that gracefully hides what a backend can't do.
|
|
29
|
+
|
|
30
|
+
## Supported backends
|
|
31
|
+
|
|
32
|
+
| Backend | Discover | Loaded indicator | Switch model | Load/Unload | Auth |
|
|
33
|
+
|---|:--:|:--:|:--:|:--:|---|
|
|
34
|
+
| **Ollama** | ✅ | ✅ live | ✅ implicit | ✅ | none (local) |
|
|
35
|
+
| **LM Studio** | ✅ | ✅ live | ✅ JIT | ✅ | optional key |
|
|
36
|
+
| **llama.cpp** (`llama-server`) | ✅ | ✅ (single) | ❌¹ | ❌ | optional key |
|
|
37
|
+
| **llama-swap** | ✅ | ✅ live | ✅ proxy swap | ✅ | optional key |
|
|
38
|
+
| **vLLM** | ✅ | last-known | ❌¹ | ❌ | optional key |
|
|
39
|
+
| **OpenAI** | configured | — | pick model | — | API key |
|
|
40
|
+
| **Anthropic** | configured | last-known | pick model | — | API key |
|
|
41
|
+
| **Generic OpenAI-compatible** | ✅ (fallback) | last-known | ❌ | ❌ | optional key |
|
|
42
|
+
|
|
43
|
+
¹ Single model per instance. Run **llama-swap** in front of `llama-server`/vLLM to unlock switching —
|
|
44
|
+
Crossbar detects it automatically and prefers it.
|
|
45
|
+
|
|
46
|
+
Full endpoint-level detail is in [`CAPABILITY-MATRIX.md`](./CAPABILITY-MATRIX.md); research notes and
|
|
47
|
+
Pi-integration citations are in [`RESEARCH.md`](./RESEARCH.md) and [`ARCHITECTURE.md`](./ARCHITECTURE.md).
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# from npm
|
|
53
|
+
pi install npm:@hypabolic/crossbar
|
|
54
|
+
|
|
55
|
+
# or from git
|
|
56
|
+
pi install git:github.com/hypabolic/crossbar
|
|
57
|
+
|
|
58
|
+
# or a local checkout
|
|
59
|
+
pi install /path/to/crossbar
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then start Pi and run `/crossbar`.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
- **`/crossbar`** (alias **`/local`**) — open the onboarding overlay: pick from auto-discovered servers
|
|
67
|
+
or add one manually (URL + optional API key / no-auth toggle), test the connection, choose a model,
|
|
68
|
+
and save.
|
|
69
|
+
- **Auto-discovery** runs on session start: reachable no-auth servers on localhost are registered
|
|
70
|
+
automatically; keyed servers are surfaced with a prompt to add them.
|
|
71
|
+
- Registered models appear in Pi's standard **`/model`** picker and `/login` provider list.
|
|
72
|
+
- The **loaded-model widget** shows what's currently resident (`●` live via introspection, `◷` with a
|
|
73
|
+
`(last-known)` suffix where a backend can't report live state).
|
|
74
|
+
|
|
75
|
+
LAN discovery (beyond localhost) is **off by default** — no backend advertises over mDNS, so Crossbar
|
|
76
|
+
never scans your network unless you opt in.
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
- **Discovery** probes localhost ports and fingerprints each server by response shape (e.g. Ollama's
|
|
81
|
+
`GET /` banner, LM Studio's `/api/v0/models` state, vLLM's `/version` + `owned_by`), preferring the
|
|
82
|
+
most specific match. The generic adapter is the low-confidence fallback.
|
|
83
|
+
- **Registration** maps discovered models onto Pi's built-in `openai-completions` / `anthropic-messages`
|
|
84
|
+
providers via `pi.registerProvider` — Crossbar writes no streaming code for OpenAI-compatible servers.
|
|
85
|
+
- **Persistence** keeps non-secret server metadata in `~/.pi/agent/crossbar.json`; **API keys live only
|
|
86
|
+
in Pi's `auth.json`** (mode `0600`), keyed by the server's provider id, exactly like Pi's own creds.
|
|
87
|
+
|
|
88
|
+
## Security
|
|
89
|
+
|
|
90
|
+
- API keys are **never** written to `crossbar.json`, never logged, and never inlined into a provider
|
|
91
|
+
config — Pi resolves them from `auth.json` at request time.
|
|
92
|
+
- Discovery is localhost-only by default.
|
|
93
|
+
- Crossbar adds no telemetry.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install
|
|
99
|
+
npm run check # tsc --noEmit
|
|
100
|
+
npm test # vitest (conformance + unit + live-socket integration)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The `BackendAdapter` contract (`src/core/`) is the frozen boundary every adapter implements; the
|
|
104
|
+
conformance suite (`tests/conformance/`) validates every adapter against it, and
|
|
105
|
+
`tests/integration/` exercises the real discovery path over live sockets.
|
|
106
|
+
|
|
107
|
+
### CI / releasing
|
|
108
|
+
|
|
109
|
+
- **CI** (`.github/workflows/ci.yml`) runs `tsc --noEmit` + the full test suite on every push and PR
|
|
110
|
+
(Node 22 & 24).
|
|
111
|
+
- **Releases** (`.github/workflows/release.yml`) publish to npm via **GitHub→npm OIDC trusted
|
|
112
|
+
publishing** — no tokens or secrets. [Provenance](https://docs.npmjs.com/generating-provenance-statements)
|
|
113
|
+
is attached automatically. Two ways:
|
|
114
|
+
1. **Manual** — GitHub → *Actions → Release → Run workflow* → choose `patch` / `minor` / `major`.
|
|
115
|
+
It bumps `package.json`, commits, tags `vX.Y.Z`, and publishes.
|
|
116
|
+
2. **Tag push** — `npm version patch && git push --follow-tags` locally.
|
|
117
|
+
|
|
118
|
+
**One-time setup:** on npmjs.com, add a **Trusted Publisher** for `@hypabolic/crossbar`
|
|
119
|
+
(*Package settings → Trusted Publisher → GitHub Actions*) pointing at repo **`Hypabolic/Crossbar`**
|
|
120
|
+
and workflow **`release.yml`**. The workflow authenticates through the OIDC `id-token` it already
|
|
121
|
+
requests — no `NPM_TOKEN` needed.
|
|
122
|
+
|
|
123
|
+
<!-- TODO: add an onboarding demo GIF (docs/onboarding.gif) recorded against a live Ollama + LM Studio. -->
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
[MIT](./LICENSE) © Hypabolic
|