Skip to content

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

Closed
hwacookie wants to merge 5 commits into
Hypabolic:mainfrom
hwacookie:main
Closed

fix: Unsloth Studio support — adapter, onboarding auth-question skip, and broken auth.json credential store#32
hwacookie wants to merge 5 commits into
Hypabolic:mainfrom
hwacookie:main

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.
@hwacookie

Copy link
Copy Markdown
Contributor Author

Superseded by #33 (same commits, but from a topic branch instead of my fork's main). Closing this one.

@hwacookie hwacookie closed this Aug 21, 2026
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

1 participant