Skip to main content
Use any provider or gateway in three steps: implement the small ChatModel protocol, run the conformance check, and bind it. metalworks talks to LLMs only through this protocol, so once your adapter passes, it drops in anywhere a model is expected.

The protocol

from typing import ClassVar, TypeVar
from pydantic import BaseModel
from metalworks.llm.protocol import ChatCapabilities, TextResult, Usage

T = TypeVar("T", bound=BaseModel)

class MyChatModel:
    protocol_version: ClassVar[str] = "1.0"
    model_id = "myprovider/my-model"
    capabilities = ChatCapabilities(
        native_structured=False,  # set True if your provider enforces JSON schema
        tool_calls=True,
        native_grounding=False,
        thinking=False,
    )

    def complete_text(self, *, system, user, max_tokens=1024, temperature=0.7,
                      thinking_budget=0, timeout_s=120.0) -> TextResult:
        text = ...  # call your provider
        return TextResult(text=text, usage=Usage(input_tokens=0, output_tokens=0))

    def complete_structured(self, *, system, user, output_model, max_tokens=1024,
                            temperature=0.7, thinking_budget=0, timeout_s=120.0):
        ...  # see the ladder below

Structured output

If your provider has no native JSON-schema mode, reuse the shared ladder so you get tool-call extraction with a prompt-embedded fallback and one validation retry:
from metalworks.llm.structured import prompt_embedded_structured

def complete_structured(self, *, system, user, output_model, **kw):
    return prompt_embedded_structured(
        model_id=self.model_id,
        output_model=output_model,
        complete_text=lambda prompt: self.complete_text(system=system, user=prompt).text,
        user=user,
    )
All paths end in output_model.model_validate(...) and raise a typed StructuredOutputError on failure, so callers never see a raw ValidationError.

Verify it

from metalworks.llm import ChatModel

def test_my_model_satisfies_protocol():
    assert isinstance(MyChatModel(), ChatModel)  # runtime_checkable
Bind your model anywhere a ChatModel is expected, including ResearchDeps:
deps = ResearchDeps(chat=MyChatModel(), embeddings=..., corpus=..., reader=...)