Skip to content

fix: Unsloth Studio support — adapter, onboarding auth-question skip, and broken auth.json credential store - #33

Merged
hypatomic merged 8 commits into
Hypabolic:mainfrom
hwacookie:fix/model-registry-authstorage-removed
Aug 24, 2026
Merged

fix: Unsloth Studio support — adapter, onboarding auth-question skip, and broken auth.json credential store#33
hypatomic merged 8 commits into
Hypabolic:mainfrom
hwacookie:fix/model-registry-authstorage-removed

Conversation

@hwacookie

Copy link
Copy Markdown
Contributor

Closes #31.

Summary

Three separate problems reported when adding an Unsloth Studio server (see #31 for full repro/root-cause writeups), plus one more found while manually verifying the fix end-to-end against a live instance.

1. New Unsloth Studio adapter (src/adapters/unsloth.ts)

Unsloth Studio has no unauthenticated mode at all — every request, including GET /v1/models, requires a bearer key. fingerprint() identifies it from the Server: unsloth-studio response header, present on every response (401 with no/invalid key, and 200 authenticated alike) — verified against a live instance via curl, not guessed. Deliberately does not claim a bare 200 + data[] on its own (indistinguishable from any other OpenAI-compatible server) — falls through to the generic adapter in that case. Adds IntrospectLoaded support from the real loaded: boolean field each model entry reports.

⚠️ Contract change: adds an optional authRequired?: boolean field to BackendAdapter (src/core/backend-adapter.ts) and a new "unsloth" BackendKind (src/core/capability.ts). Both files are the frozen contract per ARCHITECTURE.md. Bumped CONTRACT_VERSION 2 → 3. The change itself is additive/non-breaking for existing adapters, but the frozen files were touched and should get explicit review.

2. Onboarding dialog now skips "No authentication" for authRequired backends (src/ui/onboarding.ts)

The manual-add flow asked the auth question before ever probing the server, so a user could pick "No authentication" for a backend that can never work without a key and land on a generic "could not identify the server" dead end. Now pre-probes unauthenticated once before asking; if a matching adapter declares authRequired: true, the choice is skipped entirely and the user goes straight to the API-key prompt.

3. Broken credential store on pi-coding-agent >=0.80.8 (src/registry/auth-json-credential-store.ts)

createPiCredentialStore() read ctx.modelRegistry.authStorage, which pi-coding-agent >=0.80.8 removed from the extension-facing SDK entirely ("AuthStorage and its storage backends are no longer exported", per that release's CHANGELOG). Crossbar's package.json pins the devDependency to 0.79.9, but the globally-installed pi binary users actually run can be much newer — this crashed with Cannot read properties of undefined (reading 'set') the moment a keyed server was added. Replaced with a module that reads/writes auth.json directly, in Pi's own flat shape (atomic writes, 0600, read-modify-write so it never touches another provider's entry).

4. process.env bridge for the $ENV sentinel (found during live testing)

Even with the key correctly persisted, pi.setModel() still silently returned false. registerServer() registers providers with apiKey: "$ENVVAR" (never the plaintext key), relying on Pi's own config-value resolver to fill it in from process.env — but nothing ever set that variable. registerServer() and preloadCachedProviders() now bridge the resolved key into process.env[envVarFor(record.id)] immediately before pi.registerProvider(...).

Testing

  • tsc --noEmit: clean.
  • vitest run: 668 passed, 8 skipped, 0 failed.
  • New CLI integration test (tests/integration/cli-preload.test.ts) spawns the real local pi binary against an isolated agent dir with a throwaway dummy key and confirms the model is listed as available via --list-models — the exact chain that was broken (Problem 4).
  • unsloth.fixture.ts uses response data captured verbatim from a live Unsloth Studio instance, not synthesized examples.

Commits

  • f206cc7 fix(generic): read context window/maxTokens fields reported by OpenAI-compatible servers (pre-existing WIP carried along)
  • c4a7c3b feat(adapters): add Unsloth Studio adapter (Problem 1)
  • 1db85eb fix(onboarding): skip the "No authentication" choice for authRequired backends (Problem 2)
  • 88bb28a fix(registry): replace broken authStorage-based credential store (Problem 3)
  • 4b0d8b3 fix(shim): bridge the resolved API key into process.env for the $ENV sentinel (Problem 4)

Hauke Walden added 5 commits August 20, 2026 00:18
…-compatible servers

Pre-existing WIP carried over from before this branch: the generic adapter
previously ignored context_window/max_model_len/context_length/
max_context_length and max_completion_tokens fields entirely, always
falling back to hardcoded defaults (8192/4096) even when the server
reported real values.
Unsloth Studio always requires an API key (no unauthenticated mode at
all), so it needs its own adapter rather than falling through to the
generic OpenAI-compatible fallback.

- fingerprint() identifies the backend from the `Server: unsloth-studio`
  response header, present on every response (401 with no/invalid key,
  and 200 authenticated alike) — verified against a live instance via
  curl. Deliberately does NOT claim a bare 200 + data[] on its own
  (indistinguishable from any other OpenAI-compatible server); falls
  through to the generic adapter in that case, per the existing
  fingerprint-discrimination-must-be-honest rule.
- Adds IntrospectLoaded support from the real `loaded: boolean` field
  each /v1/models entry reports.

BREAKING (contract): adds an optional `authRequired?: boolean` field to
`BackendAdapter` (src/core/backend-adapter.ts) and a new "unsloth"
BackendKind (src/core/capability.ts). Both files are the FROZEN
CONTRACT per ARCHITECTURE.md — this bumps CONTRACT_VERSION 2 -> 3. The
change itself is additive/non-breaking for existing adapters, but the
frozen files were touched and need explicit review.

Fixture/tests use response data captured verbatim from a live Unsloth
Studio instance, not synthesized examples.
… backends (Problem 2 of Hypabolic#31)

The manual-add flow asked the auth question before ever probing the
server, so a user could pick "No authentication" for a backend that
can never work without a key (e.g. Unsloth Studio) and land on the
generic "could not identify the server" dead end once every adapter's
fingerprint 401s.

Now pre-probes unauthenticated, once, before asking. If a matching
adapter declares `authRequired: true`, the auth-choice select is
skipped entirely and the user goes straight to the API-key prompt,
with an explicit notify naming which backend needs it. Falls through
to the original behaviour unchanged when the pre-probe fails or
matches nothing, or when no adapter declares authRequired.
…blem 3 of Hypabolic#31)

`createPiCredentialStore()` built the CredentialStore from
`ctx.modelRegistry.authStorage`, which pi-coding-agent >=0.80.8 removed
from the extension-facing SDK surface entirely (see that release's
CHANGELOG: "AuthStorage and its storage backends are no longer
exported"). Since Crossbar's package.json pins the devDependency to
0.79.9 but the globally-installed `pi` binary users actually run can be
(and was, in the field) much newer, this crashed with "Cannot read
properties of undefined (reading 'set')" the moment a server with
`auth: apiKey` was added.

Replaces it with auth-json-credential-store.ts, which reads/writes
auth.json directly in Pi's own flat shape (atomic writes, 0600,
read-modify-write so it never touches another provider's entry) —
restoring the "secrets live only in auth.json" guarantee without
depending on the removed SDK internals. Its header doc also explains
why built-in providers (anthropic, openai, github-copilot, ...) were
never affected: they run through Pi's internal ModelRuntime, which
kept privileged access all along; only the EXTENSION provider API lost
its bridge.
…sentinel

Found while manually testing the Hypabolic#31 fixes end-to-end against a real
Unsloth Studio instance: adding a keyed server succeeded (key
persisted via the new auth-json-credential-store), but `pi.setModel()`
still silently returned false ("Pi could not select <model>").

registerServer() registers the provider with apiKey: "$ENVVAR" (never
the plaintext key, by design), relying on Pi's own config-value
resolver to fill it in from process.env at request time — but nothing
ever set that env var. The original assumption (encoded in a now-
corrected comment) was that Pi resolved auth.json by provider id on
its own for ANY registered model; empirically true for pi-coding-agent
0.79.9 (verified via the real AuthStorage/ModelRegistry in
provider-shim.test.ts) but not reliably observable for extension-
registered providers under newer Pi versions, whether due to the
extension-facing API changes in >=0.80.8 or an in-memory credential
snapshot that never sees our out-of-band auth.json write.

registerServer() and preloadCachedProviders() now set
process.env[envVarFor(record.id)] to the resolved key immediately
before pi.registerProvider(...), so Pi's live process.env read finds
it regardless. Added an end-to-end CLI integration test that spawns
the real local `pi` binary against an isolated agent dir with a
throwaway dummy key and confirms the model is listed as available via
--list-models — the exact chain that was broken.

Also cleans up test pollution: registerServer's process.env mutation
is real and process-global, so provider-shim.test.ts now resets it in
afterEach to keep the (deliberately isolated) real-ModelRegistry
"keyed availability" tests hermetic.
Hauke Walden added 2 commits August 21, 2026 15:44
…never described

Unsloth Studio emits `context_length` / `max_context_length` /
`native_context_length` ONLY for the model that is currently loaded. Verified
against a live instance:

    {"id":"Qwen3.6-35B-A3B-UD-Q4_K_M","context_length":262144,...,"loaded":true}
    {"id":"Qwen3.8-27B-Q8_0","loaded":false}     <- no context fields at all

The adapter answered that silence with an invented `contextWindow: 8192` /
`maxTokens: 4096`. Those numbers were not merely displayed as "8k ctx" in the
model picker for every unloaded model — including ones with a real 262144-token
context — they were also frozen into `lastKnownModels` in crossbar.json, so the
lie survived restarts.

Adopt the contract the llama.cpp and llama-swap adapters already follow after
review: report only what the backend actually said. `contextWindowFor` now
returns `undefined` instead of a default, `listModels` omits the field entirely
when unknown (so it picks up the real value as soon as the model is loaded), and
the single fallback lives at the Pi-mapping boundary in `toPiModel`, where the
field is mandatory — 128_000 context and `maxTokens: 0` ("no client-side cap,
let the server decide"), identical to the sibling adapters.

Caches already on disk are repaired: `sanitizeLegacyModelCache` now covers
`unsloth` alongside `llamaswap`/`llamacpp`, stripping exactly the fabricated
8192/4096 pair while leaving authoritative values untouched. Since existing
configs are already stamped `modelCacheVersion: 1` and would otherwise skip
migration, the marker is bumped to 2 so the one-time re-sanitize actually runs.

Tests: regression coverage for the unloaded/loaded/max/native cases and both
toPiModel branches, plus an `unsloth` record in the persistence migration
fixture. The conformance fixture declares `maxTokensMayBeUnbounded`.
Detect Unsloth Studio's "Switch model by request" setting via
GET /api/settings/openai-auto-switch (verified against a live instance)
and surface it in Crossbar's model pickers:

- Contract v4: new optional autoLoadsOnDemand() + Capability.AutoLoadStatus
  (non-breaking; only backends that can answer authoritatively implement it)
- Unsloth adapter implements it defensively: non-200 / missing endpoint /
  malformed body all yield undefined (unknown) — old server versions keep
  today's unmarked behaviour
- Model pickers (Use a model in Pi, switch/load actions): not-loaded models
  get a ○ prefix + "no auto-load" badge when the server answered false,
  plus a footnote pointing at the backend's own load UI; selecting one also
  warns that the server cannot serve it until loaded
- Conformance harness gains an autoLoadStatus section; docs updated
  (capability matrix incl. the missing Unsloth row, ARCHITECTURE version)
@hwacookie
hwacookie marked this pull request as draft August 21, 2026 15:01
Selecting a model the server cannot serve only fails on the first
request — so the picker now blocks it instead of warning afterwards:

- selectOverlay gains an optional disabled set: Enter on a disabled
  item is swallowed and the reason is shown as a warning notification,
  the overlay stays open (Esc still cancels)
- performUseModel / performModelAction / add-server flow all pass the
  disabled set; performUseModel skips the picker entirely when no
  model is loaded at all (dead end) and explains why
- the old post-selection warning is gone (unreachable)
- resolveAutoLoadsOnDemand now takes (server, cred) so the add-server
  flow can reuse it before the record exists
- new exported helpers: manualLoadDisabledValues, manualLoadFootnote,
  manualLoadReason (selectOverlay exported for tests)
- tests: 6 new behavioral tests drive the REAL pi-tui SelectList
  headlessly (raw key sequences) verifying Enter is swallowed on ○
  items, navigation onto them still works, and back-compat holds
@hwacookie
hwacookie marked this pull request as ready for review August 21, 2026 19:52
@hwacookie

Copy link
Copy Markdown
Contributor Author

Found (and fixed) another two problems:
a) the context size was not shown correctly
b) Unsloth Studio disables model loading on demand per default. Crossbar will give a warning that selecting other thn the currently loaded models is not possible and must be done via the Web UI if this auto-loadf is disabled.

@hypatomic
hypatomic requested review from hypatomic and a lite review from Copilot August 24, 2026 02:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Crossbar’s compatibility with Unsloth Studio and newer pi-coding-agent runtimes by adding a dedicated adapter, fixing onboarding/auth handling, and replacing the broken credential persistence path with a direct auth.json store (plus an env-var bridge for Pi’s $ENV sentinel). It also enhances the generic OpenAI-compatible adapter to respect server-reported context window / token limits, and adds extensive unit, conformance, and integration coverage.

Changes:

  • Add an Unsloth Studio backend adapter (fingerprinting via Server: unsloth-studio, authRequired: true, loaded-state introspection, and auto-load status support).
  • Update onboarding/model-picking UX to pre-probe unauthenticated once, skip the “No authentication” choice for auth-required backends, and mark/disable models that cannot be served until loaded.
  • Replace the removed Pi authStorage write path with a direct auth.json credential store, and bridge resolved keys into process.env for $ENV-based provider configs (including preload-time).

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/ui/onboarding.test.ts Adds/selects overlay-disabled-item behavior tests; expands model-picker helper coverage.
tests/ui/onboarding-flow.test.ts Verifies authRequired backends skip the auth-choice prompt during manual add.
tests/shim/provider-shim.test.ts Adds assertions around the process.env bridge; ensures env cleanup between tests.
tests/registry/persistence.test.ts Updates expectations for model-cache version bump and legacy cache sanitization.
tests/registry/auth-json-credential-store.test.ts New unit tests for direct auth.json credential store behavior (permissions, atomicity expectations, preservation).
tests/preload.test.ts Ensures preload bridges persisted auth.json keys into process.env when present.
tests/integration/cli-preload.test.ts New end-to-end regression test confirming keyed providers become available via real pi --list-models.
tests/conformance/run-conformance.ts Extends conformance harness to validate autoLoadsOnDemand when capability is present.
tests/conformance/fixtures.ts Adds fixture support for autoLoadStatus expectations.
tests/adapters/unsloth.test.ts New adapter-specific tests covering authRequired and fingerprint discrimination + AutoLoadStatus behavior.
tests/adapters/unsloth.fixture.ts New conformance fixture captured from a live Unsloth Studio instance.
src/ui/onboarding.ts Adds manual-load UI semantics (○ marking, disabled selection handling), selectOverlay enhancements, and authRequired pre-probe logic.
src/shim/provider-shim.ts Bridges resolved API keys into process.env[envVarFor(id)] for $ENV resolution at request time.
src/registry/registry.ts Bumps persisted modelCacheVersion written by the registry.
src/registry/pi-credential-store.ts Removes the legacy AuthStorage-based credential store bridge.
src/registry/persistence.ts Bumps model cache version and expands legacy sanitization for fabricated context/maxTokens values.
src/registry/auth-json-credential-store.ts New direct auth.json-backed credential store implementation.
src/preload.ts Preload now reads persisted keys from auth.json and bridges them into process.env before registration.
src/index.ts Switches runtime credential store creation to the new auth.json store.
src/core/types.ts Updates config schema to allow modelCacheVersion 1 or 2 with rationale.
src/core/index.ts Exposes canAutoLoadStatus helper from the core module index.
src/core/capability.ts Adds AutoLoadStatus capability and new backend kind "unsloth".
src/core/backend-adapter.ts Bumps contract version and adds authRequired? + autoLoadsOnDemand? to adapter contract.
src/adapters/unsloth.ts New Unsloth Studio adapter implementation.
src/adapters/index.ts Registers/exports the new Unsloth adapter in the adapter set.
src/adapters/generic.ts Enhances generic /v1/models parsing to read context window and max token limits when available.
package-lock.json Lockfile updates from dependency resolution changes.
CHANGES.md New changelog entry documenting generic adapter context/maxTokens improvements.
CAPABILITY-MATRIX.md Updates capability matrix to include AutoLoadStatus and Unsloth Studio row.
ARCHITECTURE.md Updates documented CONTRACT_VERSION to match the revised adapter contract.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ui/onboarding.ts
Comment on lines 490 to +494
/**
* Render a single-select overlay (titled SelectList in an accent border) and resolve
* to the chosen item value, or `null` on Esc/cancel. Shared by the model picker and
* the manage menus so they stay visually consistent.
/**
* Items the user may navigate to but NOT confirm (Enter is swallowed and
* `reason` is shown as a notification). Used for models the server cannot serve
Comment on lines +22 to +26
* Writes are read-modify-write + atomic rename (temp file on the same
* filesystem), so a concurrent Pi-side write (e.g. an OAuth login finishing
* around the same time) can only ever lose one side's *own* key entry in the
* unlikely event both writes race — never corrupt the file. Every entry
* belonging to other providers (anthropic, openrouter, github-copilot, ...)
@hypatomic
hypatomic merged commit e1b0dc4 into Hypabolic:main Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple Problems using Unsloth Studio

3 participants