feat(server): provider config schema v2, layered resolution, hot-apply API - #35
Merged
Merged
Conversation
Ticket 01 of the model-providers workstream.
Schema (v2, backward-compatible with v1):
{ version: 2, providers: [
{ id, label, preset?, enabled, protocol: openai|anthropic|gemini,
auth: { type: byok|coding-plan, apiKey?, baseURL? },
models: [{ id, label?, contextLimit?, thinkingLevels?, modalities? }] } ] }
Layered resolution (highest wins on per-field basis):
- MCODE_WEBUI_MODELS_CONFIG env (existing) > cwd models.json (existing)
- user-level ~/.mcode-webui/providers.json (new, PUT target)
Same-id provider deep merge, models deduped by id with higher layer winning.
v1 records default to protocol=openai / auth.type=byok on load.
Routes (Hono layer):
- GET /api/providers — masked catalogue + sources + userPath
- PUT /api/providers — validate + atomic-write + SSE broadcast
('event: providers.updated' + state push)
- POST /api/providers/test
— local key-format check FIRST (no network
for malformed inputs), then a per-protocol
minimal probe (openai GET /v1/models,
anthropic POST /v1/messages with
max_tokens:1, gemini GET /v1beta/models).
Security:
- apiKey is masked in EVERY response path (apiKeyMasked field only).
- Probe requests send the key ONLY to the configured baseURL.
- Atomic rename on the user-level write (no half-written config).
/api/models extension:
- Each model carries protocol / thinkingLevels / modalities from config.
- Each provider group carries auth: {hasKey, type} (no apiKey, no
baseURL — the secrets surface lives only on /api/providers).
- Engine-authoritative merge semantics preserved (1 > 2 > 3).
Tests (42 lib unit + 15 route):
- Parser matrix: v1 compat, v2 fields, layer precedence + deep
merge + dedupe, duplicate-id rejection.
- API: GET masking (no plaintext in any response shape, pinned),
PUT round-trip + hot reload effect on /api/models without
restart, test endpoint structured errors + malformed-key
rejection (no network), SSE payload masking.
- Test isolation: MCODE_WEBUI_{DATA_DIR,MODELS_CONFIG} set per
test to /tmp tmpdir; no server.js spawn so the existing
test-isolation-lint doesn't apply, but the same discipline holds.
Live self-check (isolated 18096/18097):
- PUT a fake provider with key
'sk-realkey-FAKE-SECRET-1234567890' → 200 with apiKeyMasked
'sk-r***7890' (plaintext nowhere in any response).
- GET /api/models immediately lists fake_openai/gpt-4o-mini with
protocol/thinkingLevels/modalities (no restart).
- SSE event 'providers.updated' observed with masked payload.
- Test endpoint: short key → 400 INVALID_KEY in <5ms (no network);
unknown protocol → 400 BAD_PROTOCOL; valid key + unreachable
baseURL → 502 PROBE_FAILED (timeoutMs respected).
Gates: typecheck 0 err; test:webui 537 pass + 2 skip + 0 fail
(3 stable runs); test:webapp 213 pass; build OK; check:source OK;
check:docs-alignment OK (all 3 new endpoints registered).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ticket 01 of the provider-configuration feature (user-approved schema): the backend foundation for preset providers, BYOK/coding-plan auth, and per-model thinking-level/modality metadata.
protocol(openai|anthropic|gemini),auth(byok|coding-plan + apiKey/baseURL), per-modelthinkingLevels/modalities; v1 files load with defaults.MCODE_WEBUI_MODELS_CONFIGenv > cwdmodels.json> user-level~/.mcode-webui/providers.json. Same-id providers deep-merge (scalars higher-wins, models union by id with higher-layer collisions winning).GET/PUT /api/providers(atomic write → immediate reload →providers.updatedSSE frame; no in-process cache — the next/api/modelsrequest reflects changes) andPOST /api/providers/test(per-protocol minimal probe; format validation strictly pre-fetch; key sent only to the configured baseURL).apiKeyis never on the wire — one serialiser (publicView) on every response path with graded masking;/api/modelsgroups exposeauth:{hasKey,type}only.Acceptance (independent agent)
PASS — masking hunt CLEAN (43-char known key grepped across GET/PUT/models/SSE live: 0 plaintext hits; probe error details carry no body/URL — gemini's key-in-URL never echoed). Layer precedence + deep-merge verified live (env overrides one field, union keeps user models); v1 compat live; malformed-key probes return 400 in ~3ms with zero packets. Gates: 57/57 ×3 · test:webapp 213/213 · typecheck 0 · build ✓ · check:source ✓ (4579 files).
Notes:
readModelsConfig(v1 reader) remains as documented dead code; probe uses a fixed probe model with documented rationale.Full
pnpm verifydeferred to CI.