Skip to content

refactor(extensions): carve the OpenRouter provider out of core into a peer extension #1337

Description

@DecisionNerd

Maintainer scope: before v1.0.0

Backward compatibility is out of scope before the v1.0.0 release. Do not add API aliases, compatibility shims, or deprecation layers to preserve the current vendor-named public surface. Document intentional API breaks instead.

Problem

The only concrete search provider is a named commercial vendor's wire contract, compiled into core unconditionally, with the vendor's name in the public API.

GraphForge is embedded-first and local by design. Its telemetry is disabled by default, it requires no server, and it refuses to send data anywhere. Yet every build of graphforge-search contains an HTTP client and OpenRouter's wire contract, whether or not the consumer ever uses embeddings.

Three concrete consequences:

  • ureq is a required dependency of graphforge-search and is used by exactly one module in that crate, provider_openrouter.rs. Consumers who never call a provider still compile and ship an HTTP client.
  • graphforge-api exports vendor names from its public surface: OpenRouterWireLimits at lib.rs:338, and OpenRouterProviderSession with OpenRouterProviderSessionConfig at lib.rs:437. Binding sources reference the vendor 31 times. The generated TypeScript surface does not carry the name, so the leak stops short of the Node public API. Freezing a vendor name into a public API at v1.0.0 is a commitment nobody intends to make.
  • The provider abstraction has exactly one real implementation. Every other implementation of DocumentEmbeddingProvider, QueryEmbeddingProvider and CandidateReranker in the workspace is a test fake. A trait shape that has never met a second vendor tends to have absorbed the first one's assumptions.

Why this is tractable

The separation is already most of the way done, by deliberate design. Seven of the eight provider modules in graphforge-search describe themselves as provider-neutral in their own doc comments:

Module Lines Self-described role
provider.rs 536 Provider-neutral capability and failure vocabulary
provider_adapter.rs 825 Adapter request and response boundaries
provider_batching.rs 821 Deterministic batch planning
provider_execution.rs 790 Bounded provider-neutral attempt execution
provider_publication.rs 906 Atomic publication of embedding generations
provider_reranking.rs 871 Bounded post-retrieval reranking
provider_response.rs 705 Validation of untrusted provider outputs

Only two modules are vendor-specific, about 2,255 lines in total:

  • graphforge-search/src/provider_openrouter.rs, 1,376 lines, "OpenRouter wire contract over an injectable byte transport".
  • graphforge-api/src/provider_session.rs, 879 lines, whose own doc comment reads "Rust-owned configured OpenRouter workflow shared by public bindings".

The neutral machinery is the extension point. This issue moves the vendor, not the architecture.

Objective

Move the OpenRouter wire contract and its configured session into a peer extension, leave the provider-neutral machinery in core as the published extension point, and remove the vendor from the core dependency graph and public API.

Scope

  • Move both vendor-specific modules into a peer extension crate.
  • Keep the provider traits, batching, execution, response validation, publication and reranking in core. They are the contract an extension implements.
  • Remove the vendor names from the graphforge-api public surface and from the binding surfaces, replacing them with provider-neutral configuration.
  • Make ureq unnecessary in graphforge-search, so core carries no HTTP client on behalf of a provider.
  • Preserve the existing safety controls in the moved code: fixed endpoint paths, HTTPS required unless the host is loopback, redirects disabled, injectable transport, timeouts and body limits.
  • Record the extension boundary in an ADR. docs/adr currently contains no peer-extension boundary ADR, although docs: ADR 0008 — peer-extension boundary (datasets first) #19 was closed naming one; ADR 0008 is heterogeneous lists.

Out of scope

Acceptance criteria

  • No vendor name appears in the graphforge-api public API or in the Python and Node public surfaces.
  • graphforge-search builds and its tests pass with no provider implementation present, and ureq is not one of its required dependencies.
  • The extension supplies the provider through the unchanged core traits, and its tests cover the wire contract that moved.
  • Fixed endpoints, the HTTPS-unless-loopback rule, disabled redirects, the injectable transport, timeouts and body limits are all preserved in the moved code, proven by the tests that move with it.
  • Embedding and reranking behaviour is unchanged for a consumer that supplies a provider, proven by existing suites rather than new ones.
  • An accepted ADR records the peer-extension provider boundary and what core may not depend on.
  • Python and Node parity is preserved for provider-neutral configuration.

BDD Completion Scenarios

Scenario: Core carries no vendor
Given a build of the core crates with no extension present
When the dependency graph and public API are inspected
Then no HTTP client is required by the search crate
And no vendor name appears in any public surface.

Scenario: An extension supplies the provider
Given the peer extension is present and configured
When document embedding, query embedding and reranking run
Then results match the behaviour recorded before the move.

Scenario: The moved code keeps its guards
Given the extension is asked to use a non-loopback plaintext origin, an injected path, or a redirecting endpoint
When a request is attempted
Then it is refused exactly as it is today.

Relationships

Backlog. No code changes now, and this cannot start before the peer-extension platform exists: blocked by #222, and sequenced after #20, which makes datasets the first peer extension. Related to #1200, which is investigating XYG integration. #1336 repairs a defect in the current surface and should land independently rather than wait for this move.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    coreCore source code changesenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions