Skip to content

feat: introduce pluggable service-provider contract, migrate 9router (#7333)#7730

Merged
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/7333-pluggable-service-providers
Jul 19, 2026
Merged

feat: introduce pluggable service-provider contract, migrate 9router (#7333)#7730
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/7333-pluggable-service-providers

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Refs #7333 — Phase 1 of the broader "formalize service providers as pluggable provider backends" architecture proposal.

Introduces a small ServiceProviderPlugin contract (src/lib/services/providerPlugins/) that packages what today is spread across two unrelated files per embedded-service backend:

  • bootstrap.ts's inline SERVICES[] object literal (port, health-check config, spawn args)
  • serviceBackends.ts's SERVICE_BACKEND_MANIFEST_TEMPLATE entry (manifest fields)

Migrates exactly one service — 9router — through the new contract end to end, as living proof the abstraction is sound. cliproxy, mux, and bifrost are completely untouched, still on their pre-existing inline SERVICES[] branches.

What changed

  • src/lib/services/providerPlugins/types.ts (new) — ServiceProviderPlugin interface.
  • src/lib/services/providerPlugins/registry.ts (new) — SERVICE_PROVIDER_PLUGINS (deliberately typed Record<"9router", ...>, not the full ServiceBackendPluginId union, so future backend migrations are explicit reviewable additions) + getServiceProviderPlugin().
  • src/lib/services/bootstrap.ts — 9router's SERVICES[] entry now derives from getServiceProviderPlugin("9router") instead of an inline literal; throws a clear startup error if the plugin is ever missing. cliproxy/mux/bifrost entries and buildSpawnArgsFactory() are unmodified.
  • open-sse/config/providerPluginManifest.ts — additive createServiceBackendManifestEntry(pluginId, template) export. Not wired into generateProviderPluginManifestFromRegistry() or any live request path in this PR.
  • docs/frameworks/EMBEDDED-SERVICES.md — new subsection documenting the contract's current (9router-only) scope and linking to feat(providers): formalize service providers as pluggable provider backends #7333 for the roadmap.
  • changelog.d/features/7333-pluggable-service-providers.md.

How validated (Hard Rule #18 — TDD)

Two new top-level test files under tests/unit/:

  • tests/unit/service-provider-plugin-registry.test.ts
  • tests/unit/service-provider-plugin-manifest.test.ts

Red→green proof: src/lib/services/providerPlugins/ does not exist on origin/release/v3.8.49 (git show origin/release/v3.8.49:src/lib/services/providerPlugins/registry.tsfatal: path ... exists on disk, but not in 'origin/release/v3.8.49'), so running these exact test files against the base branch fails at import resolution. After this PR:

node --import tsx/esm --test tests/unit/service-provider-plugin-registry.test.ts tests/unit/service-provider-plugin-manifest.test.ts
# 9 tests, 9 pass, 0 fail

Coverage includes:

  • 9router plugin values deep-equal the pre-migration literal (port 20130, health path, intervals, needsApiKey).
  • getServiceProviderPlugin("cliproxy"/"cliproxyapi"/"mux"/"bifrost") all return undefined — proves the narrow typing and that nothing else was silently migrated.
  • spawnArgs via the plugin deep-equals a direct call to nineRouterSpawnArgs() — same function, new indirection.
  • createServiceBackendManifestEntry() preserves the template verbatim with models: [].
  • generateProviderPluginManifestFromRegistry() output for openai/anthropic contains no 9router/cliproxyapi entries — proves Step 4 did not leak into the static-registry manifest path.
  • /v1/providers/[provider]/models/route.ts source still dispatches embedded-service backends via isServiceBackendPluginId()/getServiceModels(), and does not reference the new manifest helper — proves this PR left the one real behavior seam untouched.
  • Hard Rule fix(ci): explicit .npmrc auth for npm publish #17 regression: isLocalOnlyPath("/api/services/9router/status") and isLocalOnlyPath("/api/services/cliproxy/status") still return true.

Gates run (all green)

  • node scripts/check/check-test-discovery.mjs
  • npm run typecheck:core
  • npm run typecheck:noimplicit:core (pre-existing unrelated errors in open-sse/services/combo.ts, open-sse/utils/usageTracking.ts, src/shared/services/cliRuntime.ts — confirmed zero diff against base on those files)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>
  • npm run check:complexity-ratchets (cyclomatic 2058 ≤ baseline 2059, cognitive 890 = baseline 890)
  • node scripts/check/check-file-size.mjs (no violations on touched files)
  • npm run check:cycles
  • npm run check:docs-sync
  • node scripts/check/check-fabricated-docs.mjs --strict (3 pre-existing base-red failures, all unrelated to this PR's files — /api/version in docs/INCIDENT_RESPONSE.md/docs/PERF_BUDGETS.md and a migration filename in docs/routing/REASONING_ROUTING.md)

npm run test:coverage intentionally not run locally (heaviest gate, shared devbox) — CI runs it authoritatively.

Explicitly deferred (out of scope for this PR, tracked under #7333)

  • Migrating cliproxyapi through the same plugin registry.
  • Generalizing mux/bifrost into the ServiceBackendPluginId union.
  • Folding the executor-routing special-casing (open-sse/executors/index.ts, open-sse/handlers/chatCore/executorProxy.ts, mode === "cliproxyapi" checks) into the plugin contract.
  • Wiring createServiceBackendManifestEntry() into any live /v1/models or /v1/providers/[provider]/models code path.
  • Any change to the 7 per-service /api/services/9router/* / /api/services/cliproxy/* endpoints or their UI cards.

Live check (not required for this PR, informational)

This PR does not require a VPS live check under Hard Rule #18 — the behavior seam (9router bootstrap spawn args, health-check config, manifest template) is fully covered by the deep-equal regression tests above, and no live request path was touched. If a maintainer wants an end-to-end confirmation anyway: install/enable 9router locally (npm run dev) and confirm it still spawns, passes health checks, and its models still surface at /api/services/9router/models and /v1/providers/9router/models exactly as before.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@diegosouzapw
diegosouzapw merged commit 2ae4061 into release/v3.8.49 Jul 19, 2026
13 of 20 checks passed
@diegosouzapw
diegosouzapw deleted the feat/7333-pluggable-service-providers branch July 19, 2026 21:00
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