feat: Add ServiceLoader-based ModelProvider SPI for pluggable LLM backends#1363
Open
svetanis wants to merge 1 commit into
Open
feat: Add ServiceLoader-based ModelProvider SPI for pluggable LLM backends#1363svetanis wants to merge 1 commit into
svetanis wants to merge 1 commit into
Conversation
10 tasks
Contributor
|
Hi @svetanis, thank you for your contribution and We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team, we will keep you updated if any additional information is required. thank you. |
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.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
ADK Java has no convention-based way to resolve a model name string to a third-party backend.
Using any non-Gemini model requires per-application wiring: constructing or injecting model
objects into
.model(...)(thecontrib/langchain4jandcontrib/spring-aibridges), orcalling
LlmRegistry.registerLlm(pattern, factory)manually in every application — the onlynative mechanism for string resolution today. Python ADK resolves a model string from config at
runtime via LiteLLM; Java has no equivalent. This PR adds the missing discovery layer for
OpenAI-compatible endpoints; it does not replace the bridges, which remain the path for other
providers.
Additionally, pattern-based registration has a subtle correctness constraint. When an agent is
created with
.model("groq/some-model"), that string is not what reaches the provider:LlmRegistrycalls the registered factory with the requested name, and the"model"field inthe outgoing request is whatever name the returned
BaseLlmwas constructed with(
Basic.javacopies it intoLlmRequest.model;ChatCompletionsRequest.fromLlmRequestsendsit verbatim). Since providers accept only bare model IDs, a correct factory must build a
distinct instance per requested name and strip the routing prefix before construction —
otherwise the endpoint receives
"groq/some-model"and rejects the request.Solution:
A minimal
ServiceLoader-based SPI incom.google.adk.models— 3 classes, no newdependencies:
ModelProviderprefix()+createFromBareModelName(name)→BaseLlm; a defaultcreate(name)strips the routing prefix (groq/<model-id>→<model-id>)ModelProviderRegistryregisterAll()discovers all implementations viaServiceLoaderand registers each with the existingLlmRegistry; a provider that fails to instantiate is logged at WARN and skippedOpenAiCompatibleLlmBaseLlmover the nativeChatCompletionsHttpClientfor anyPOST /v1/chat/completionsendpoint; constructed with the bare model ID; the delegate is held as theChatCompletionsClientinterfaceUsage — one explicit opt-in line; provider JARs on the classpath self-register:
Adding a provider requires no ADK changes: implement
ModelProvider(typically a one-linerreturning
new OpenAiCompatibleLlm(bareModelName, apiUrl, apiKey)) and declare it inMETA-INF/services/com.google.adk.models.ModelProvider.Deliberately out of scope:
automatic
registerAll()insideRunner/AdkWebServer.start()— registration is explicit opt-in;auto-registration can be discussed as a follow-up if there is interest.
Provider distribution: ADK owns only the interface contract — nothing to publish or
coordinate. A provider can live directly in an application's own source tree (one class + a
META-INF/servicesfile; no published artifact needed), or be shipped independently byvendors/community under their own Maven coordinates.
Testing Plan
Unit Tests:
Notes: the registry tests drive the real
java.util.ServiceLoader(via an in-memoryMETA-INF/servicesresource plus a real test-classpath services file) and verify effects onthe real
LlmRegistry— no static mocking.registerAll_resolvedLlmReceivesBareModelNamecovers the full resolution chain:
LlmRegistry.getLlm("spi-test-alpha/some-model-id")returnsa
BaseLlmwhose model name is the baresome-model-id.Manual End-to-End (E2E) Tests:
Verified against this exact branch with external provider JARs and a live endpoint: two
providers (Groq, OpenRouter) on the classpath were discovered via
ServiceLoaderandregistered by
ModelProviderRegistry, an agent was built with.model("groq/llama-3.3-70b-versatile"), and Groq returned a normal completion — confirmingthe full chain (discovery → registration → name-aware factory → prefix strip → native
ChatCompletionsHttpClientround trip):The same design has also been exercised manually against Groq and OpenRouter endpoints, including
streaming and tool calling.
To try it against this branch with no external provider JAR:
Checklist
Additional context
Supersedes the earlier, much larger #1199, which I am closing in favor of this scoped change.