Skip to content

feat(server): provider config schema v2, layered resolution, hot-apply API - #35

Merged
fengzhi09 merged 1 commit into
mainfrom
feat/provider-config-schema
Sep 26, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
feat/provider-config-schema

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

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.

  • Schema v2 (backward compatible): providers carry protocol (openai|anthropic|gemini), auth (byok|coding-plan + apiKey/baseURL), per-model thinkingLevels/modalities; v1 files load with defaults.
  • Layered resolution: MCODE_WEBUI_MODELS_CONFIG env > cwd models.json > user-level ~/.mcode-webui/providers.json. Same-id providers deep-merge (scalars higher-wins, models union by id with higher-layer collisions winning).
  • Hot-apply API: GET/PUT /api/providers (atomic write → immediate reload → providers.updated SSE frame; no in-process cache — the next /api/models request reflects changes) and POST /api/providers/test (per-protocol minimal probe; format validation strictly pre-fetch; key sent only to the configured baseURL).
  • Security: apiKey is never on the wire — one serialiser (publicView) on every response path with graded masking; /api/models groups expose auth:{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 verify deferred to CI.

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).
@fengzhi09
fengzhi09 merged commit 03368de into main Sep 26, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/provider-config-schema branch September 26, 2026 07:48
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.

1 participant