How to Create Custom Provider Plugins for LangExtract: A Complete Developer's Guide
You can create custom provider plugins for LangExtract by implementing a class that inherits from BaseLanguageModel, registering it with the @lx.providers.registry.register decorator, and exposing it via the langextract.providers entry point in your package configuration.
LangExtract's modular architecture allows developers to integrate any language model backend through a standardized plugin system. Whether you need to connect to a proprietary API or a local model server, you can create custom provider plugins for LangExtract using the registry pattern and Python entry points. This article explains the three-layer architecture—registration, discovery, and factory instantiation—and provides a complete implementation based on the google/langextract source code.
Understanding the Plugin Architecture
LangExtract discovers and loads language-model providers through a registry system split into three distinct layers. Understanding these layers is essential before implementing your own provider.
The Three Layers of Provider Integration
| Layer | Responsibility | Core Implementation |
|---|---|---|
| Provider registration | Maps regex patterns for model IDs to a provider class. Registration happens lazily via the @router.register decorator or the router.register_lazy function. |
langextract/providers/router.py |
| Provider discovery | Loads built-in providers, optional providers (e.g., OpenAI), and third-party providers installed as separate packages. Discovery uses Python entry points under the group langextract.providers. |
langextract/plugins.py |
| Factory creation | The factory (factory.create_model) asks the router to resolve a model ID (or a provider name) and returns an instantiated provider. |
langextract/factory.py |
When you add a new provider, you only need to implement a class that inherits from BaseLanguageModel and register it with the router. The registration makes the provider discoverable both for built-in use and external plugin consumption.
Key Concepts
- Patterns: One or more regular-expression strings that decide which
model_idvalues should be handled by the provider. They are stored as compiledre.Patternobjects in the router entry. - Priority: An integer (default 0) that decides which provider wins when multiple patterns match. Higher numbers win.
- Lazy loading: Providers are loaded only when needed, avoiding heavy optional dependencies from being imported at package import time.
- Schema support: If a provider can emit structured JSON, it implements
get_schema_class()andapply_schema()as shown in the built-in Gemini provider.
Step-by-Step Guide to Building a Custom Provider
The repository ships a helper script that scaffolds the boilerplate for you. While all steps below can be done manually, the script saves time and creates a test suite.
Step 1: Scaffold the Plugin Structure
Run the scaffolding script from the LangExtract repository:
python scripts/create_provider_plugin.py MyProvider \
--patterns "^myprovider" "^custom" \
--with-schema # optional – include schema skeleton
This creates the following structure:
langextract-myprovider/
├── langextract_myprovider/
│ ├── __init__.py
│ ├── provider.py
│ └── schema.py # only if --with-schema
├── README.md
├── pyproject.toml
├── test_plugin.py
└── .gitignore
The script writes the files using the templates in scripts/create_provider_plugin.py.
Step 2: Implement the Provider Class
Edit provider.py to implement your provider logic. Here is the minimal implementation pattern:
import os
import langextract as lx
from typing import Sequence
@lx.providers.registry.register(r'^myprovider', r'^custom', priority=10)
class MyProviderLanguageModel(lx.inference.BaseLanguageModel):
"""Minimal LangExtract provider for MyProvider."""
def __init__(self, model_id: str, api_key: str | None = None, **kwargs):
super().__init__()
self.model_id = model_id
# Resolve API key from environment if not supplied.
self.api_key = api_key or os.getenv("MYPROVIDER_API_KEY")
# Initialise the concrete client (replace with your SDK).
# self.client = MyClient(api_key=self.api_key)
def infer(self, batch_prompts: Sequence[str], **kwargs):
"""Yield a list of ScoredOutput objects for each prompt."""
for prompt in batch_prompts:
# Replace the line below with a real API call.
result = f"Mock response for: {prompt[:30]}..."
yield [lx.inference.ScoredOutput(score=1.0, output=result)]
Key implementation points:
- The
@lx.providers.registry.registerdecorator (seerouter.registerat lines 9-35 inlangextract/providers/router.py) registers the class with the given patterns and priority. - The class inherits from
BaseLanguageModel, which supplies helper methods likemerge_kwargs. - You must implement
inferto return an iterator ofScoredOutputobjects. See the Gemini implementation inlangextract/providers/gemini.pyfor a more complex example handling batching and schema constraints.
Step 3: Add Structured Output Support (Optional)
If your provider supports structured JSON output, add a schema.py that defines a subclass of lx.schema.BaseSchema and expose it via get_schema_class. The scaffold already contains a template; the Gemini provider shows a full example in providers/gemini.py (lines 75-83).
import langextract as lx
class MyProviderSchema(lx.schema.BaseSchema):
def __init__(self, schema_dict: dict):
self._schema = schema_dict
@property
def schema_dict(self) -> dict:
return self._schema
@classmethod
def from_examples(cls, examples_data, attribute_suffix="_attributes"):
# Build a JSON schema from example extractions.
return cls({...})
def to_provider_config(self) -> dict:
return {"response_schema": self._schema, "structured_output": True}
Then add the class method to your provider:
@classmethod
def get_schema_class(cls) -> type[lx.schema.BaseSchema] | None:
return MyProviderSchema
Step 4: Configure the Entry Point
Ensure your pyproject.toml exposes the provider correctly. The scaffold writes:
[project.entry-points."langextract.providers"]
myprovider = "langextract_myprovider.provider:MyProviderLanguageModel"
This makes the plugin discoverable by the plugins module (available_providers / get_provider_class in langextract/plugins.py).
Step 5: Test Locally
Install the plugin in editable mode:
cd langextract-myprovider
pip install -e .
Run the bundled test script:
python test_plugin.py
The test script validates registration, pattern matching, inference, and (if present) schema handling. See scripts/create_provider_plugin.py for the autogenerated test logic.
Step 6: Publish (Optional)
Follow standard Python packaging steps:
python -m build
twine upload dist/*
Once published, users can install it with pip install langextract-myprovider and the provider will be automatically available to LangExtract without any additional imports.
Complete Minimal Provider Example
Here is the full implementation for a minimal working provider:
import os
import langextract as lx
from typing import Sequence
@lx.providers.registry.register(r'^myprov', r'^custom', priority=10)
class MyProviderLanguageModel(lx.inference.BaseLanguageModel):
"""Simple example provider."""
def __init__(self, model_id: str, api_key: str | None = None, **kwargs):
super().__init__()
self.model_id = model_id
self.api_key = api_key or os.getenv("MYPROVIDER_API_KEY")
# self.client = MyClient(self.api_key) # ← replace with real client
def infer(self, batch_prompts: Sequence[str], **kwargs):
"""Yield results for each prompt."""
for prompt in batch_prompts:
# Replace with actual request to the LLM service.
result = f"[MyProvider] echo: {prompt}"
yield [lx.inference.ScoredOutput(score=1.0, output=result)]
Registration details: The @lx.providers.registry.register decorator (see router.register at lines 9-35 in langextract/providers/router.py) binds the class to the specified regex patterns.
Factory usage:
import langextract as lx
config = lx.factory.ModelConfig(model_id="myprov-1")
model = lx.factory.create_model(config) # resolves to MyProviderLanguageModel
out = next(model.infer(["Hello world"]))[0].output
print(out) # → "[MyProvider] echo: Hello world"
Important Source Files
| File | Role | Link |
|---|---|---|
langextract/providers/README.md |
High-level guide to provider architecture, pattern registration, and plugin discovery. | README |
scripts/create_provider_plugin.py |
CLI that scaffolds a complete provider package (directory layout, pyproject.toml, tests, README). |
script |
langextract/providers/router.py |
Core registry implementation (register, resolve, resolve_provider). |
router |
langextract/plugins.py |
Discovery of built-in, optional, and third-party providers via entry points. | plugins |
langextract/providers/gemini.py |
Reference implementation of a fully-featured provider (API key handling, batch mode, schema support). | Gemini provider |
langextract/factory.py |
Public API that creates providers from ModelConfig. |
factory |
These files together define the full lifecycle—from registration to discovery to instantiation—that you need to understand when writing a custom provider.
Summary
- Three-layer architecture: Registration happens in
router.py, discovery inplugins.py, and instantiation infactory.py. - Minimal implementation: Inherit from
BaseLanguageModel, implement theinfermethod, and use@lx.providers.registry.registerwith regex patterns. - Entry point requirement: Expose your class under the
langextract.providersgroup inpyproject.tomlto enable automatic discovery. - Scaffolding available: Use
scripts/create_provider_plugin.pyto generate boilerplate, tests, and packaging configuration. - Schema support optional: Implement
BaseSchemaandget_schema_class()to enable structured JSON output.
Frequently Asked Questions
What is the priority parameter in the register decorator?
The priority parameter is an integer (default 0) that resolves conflicts when multiple provider patterns match the same model ID. Higher numbers win. For example, if two providers both match the pattern ^gpt, the one with priority=10 will be selected over one with priority=0. This is implemented in langextract/providers/router.py at lines 9-35.
Can I register multiple patterns for the same provider?
Yes, you can pass multiple regex patterns to the @lx.providers.registry.register decorator. The provider will handle any model ID that matches any of the provided patterns. For example: @lx.providers.registry.register(r'^myprov', r'^custom', priority=10) registers the class to handle both patterns simultaneously.
How does LangExtract discover external provider packages?
LangExtract uses Python's entry point system defined in langextract/plugins.py. When you install a package that exposes an entry point under the group langextract.providers, LangExtract automatically discovers it via the available_providers() function. This happens at runtime without requiring users to manually import your module.
Do I need to implement schema support for my custom provider?
No, schema support is optional. If your provider only returns plain text, you only need to implement the infer method. Schema support (via BaseSchema and get_schema_class()) is only required if you want to enable structured JSON output. You can refer to langextract/providers/gemini.py lines 75-83 for a complete implementation example.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →