Custom providers¶
Any LLM backend can drive CodeLoop — implement the Provider ABC (pycodeloop.abc.provider.Provider):
from pycodeloop.abc.provider import Provider, ProviderResponse, ToolCall, Usage
class MyProvider(Provider):
def complete(self, system_prompt, messages, tools, on_delta=None) -> ProviderResponse:
response = my_api.chat(
system=system_prompt,
messages=messages, # list[pycodeloop.core.session.Message]
tools=tools, # list[dict] JSON schema per tool
)
if on_delta is not None:
for chunk in response.text_chunks():
on_delta(chunk)
return ProviderResponse(
text=response.text,
tool_calls=[
ToolCall(id=call.id, name=call.name, arguments=call.arguments)
for call in response.tool_calls
],
stop_reason=response.finish_reason,
usage=Usage(input_tokens=response.usage.input, output_tokens=response.usage.output),
)
Streaming and usage tracking are both optional — return ProviderResponse(text=..., tool_calls=...) with defaults for everything else and it still works, just without live text and without token totals.
Using it¶
Pass an instance directly:
from pycodeloop import Config
config = Config(provider=MyProvider(model="my-model"))
Or load it dynamically by dotted path — useful for the CLI, which only takes strings:
pycodeloop run "..." --provider "my_package.my_module:MyProvider" --model my-model
pycodeloop.providers.get_provider(name, **kwargs) treats any name containing : as module.path:ClassName, imports the module, and instantiates the class with **kwargs. The CLI adds the current working directory to sys.path at startup, so a provider class in a plain .py file next to where you run pycodeloop is importable without installing anything.
Validation¶
Config checks isinstance(provider, Provider) at construction time and raises NotProviderInstance if it isn't — a broken custom provider fails immediately, not three tool calls into a run.