You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
refactor(extensions): carve the OpenRouter provider out of core into a peer extension #1337
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.
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.
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-searchcontains an HTTP client and OpenRouter's wire contract, whether or not the consumer ever uses embeddings.Three concrete consequences:
ureqis a required dependency ofgraphforge-searchand 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-apiexports vendor names from its public surface:OpenRouterWireLimitsatlib.rs:338, andOpenRouterProviderSessionwithOpenRouterProviderSessionConfigatlib.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.DocumentEmbeddingProvider,QueryEmbeddingProviderandCandidateRerankerin 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-searchdescribe themselves as provider-neutral in their own doc comments:provider.rsprovider_adapter.rsprovider_batching.rsprovider_execution.rsprovider_publication.rsprovider_reranking.rsprovider_response.rsOnly 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
graphforge-apipublic surface and from the binding surfaces, replacing them with provider-neutral configuration.urequnnecessary ingraphforge-search, so core carries no HTTP client on behalf of a provider.docs/adrcurrently 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
graphforge-apipublic API or in the Python and Node public surfaces.graphforge-searchbuilds and its tests pass with no provider implementation present, andureqis not one of its required dependencies.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.