Skip to content

LLM

A shared client abstraction over the official openai/anthropic SDKs, used by the Milestone 5 prompting-based linkers to call a local Ollama model in dev or a frontier API in production through the same interface.

client

A small LLM client abstraction over the official openai/anthropic SDKs.

Ollama serves an OpenAI-compatible endpoint, so OpenAiClient covers both local dev (pointed at a local Ollama server) and real OpenAI-style frontier APIs; AnthropicClient covers Claude specifically, whose Messages API has a different shape (a separate system prompt, and its own native JSON-schema output mechanism rather than OpenAI's response_format). create_client picks between them from a single "<provider>/<model>" spec, e.g. "ollama/llama3.1" or "anthropic/claude-opus-4-6".

AnthropicClient

AnthropicClient(
    model: str,
    *,
    api_key: str | None = None,
    max_retries: int = 2,
    timeout: float = 60.0,
    client: Anthropic | None = None,
)

Bases: LlmClient

LlmClient backed by anthropic.Anthropic.

Parameters:

Name Type Description Default
model str

Model name, e.g. "claude-opus-4-6".

required
api_key str | None

Forwarded to anthropic.Anthropic. Defaults to None, which lets the SDK fall back to its own ANTHROPIC_API_KEY env var.

None
max_retries int

Forwarded to anthropic.Anthropic.

2
timeout float

Forwarded to anthropic.Anthropic, in seconds.

60.0
client Anthropic | None

An already-constructed anthropic.Anthropic (or a test double matching its messages.create surface) to use instead of constructing one from the arguments above.

None

Raises:

Type Description
OptionalDependencyError

If anthropic isn't installed and client wasn't given.

Note

The current Anthropic Messages API has no temperature sampling parameter (it was replaced by reasoning-effort controls unrelated to sampling), so complete/complete_structured's temperature argument is accepted for interface parity with OpenAiClient but not forwarded -- verified directly against the installed SDK's messages.create signature, not assumed.

Source code in src/linkingtk/llm/client.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def __init__(
    self,
    model: str,
    *,
    api_key: str | None = None,
    max_retries: int = 2,
    timeout: float = 60.0,
    client: anthropic.Anthropic | None = None,
) -> None:
    self.model = model
    if client is not None:
        self._client = client
    else:
        try:
            import anthropic
        except ImportError as exc:
            raise OptionalDependencyError("AnthropicClient", "llm") from exc
        self._client = anthropic.Anthropic(
            api_key=api_key, max_retries=max_retries, timeout=timeout
        )

LlmClient

Bases: ABC

Common interface over a chat-completion-style LLM provider.

complete returns free text; complete_structured constrains the response to a given JSON schema and returns it already parsed -- prompting linkers (EA/EL/WSD/WSA) need a reliable decision/score back, not text to scrape.

complete abstractmethod

complete(
    messages: list[LlmMessage],
    *,
    max_tokens: int = 1024,
    temperature: float = 0.0,
) -> str

Send messages and return the model's free-text reply.

Source code in src/linkingtk/llm/client.py
46
47
48
49
50
51
52
53
54
@abstractmethod
def complete(
    self,
    messages: list[LlmMessage],
    *,
    max_tokens: int = 1024,
    temperature: float = 0.0,
) -> str:
    """Send `messages` and return the model's free-text reply."""

complete_structured abstractmethod

complete_structured(
    messages: list[LlmMessage],
    *,
    schema: dict[str, Any],
    max_tokens: int = 1024,
    temperature: float = 0.0,
) -> dict[str, Any]

Send messages and return a reply constrained to and parsed from schema.

schema is a JSON Schema object describing the expected response.

Source code in src/linkingtk/llm/client.py
56
57
58
59
60
61
62
63
64
65
66
67
68
@abstractmethod
def complete_structured(
    self,
    messages: list[LlmMessage],
    *,
    schema: dict[str, Any],
    max_tokens: int = 1024,
    temperature: float = 0.0,
) -> dict[str, Any]:
    """Send `messages` and return a reply constrained to and parsed from `schema`.

    `schema` is a JSON Schema object describing the expected response.
    """

LlmMessage dataclass

LlmMessage(role: LlmRole, content: str)

A single turn in a conversation passed to an LlmClient.

OpenAiClient

OpenAiClient(
    model: str,
    *,
    base_url: str | None = None,
    api_key: str | None = None,
    max_retries: int = 2,
    timeout: float = 60.0,
    client: OpenAI | None = None,
)

Bases: LlmClient

LlmClient backed by openai.OpenAI.

Also the client for Ollama (via base_url/api_key, see create_client) since Ollama serves the same OpenAI-compatible Chat Completions wire format.

Parameters:

Name Type Description Default
model str

Model name, e.g. "gpt-4o" or, against an Ollama server, "llama3.1".

required
base_url str | None

Overrides the API base, e.g. a local Ollama server. Defaults to the real OpenAI API.

None
api_key str | None

Forwarded to openai.OpenAI. Defaults to None, which lets the SDK fall back to its own OPENAI_API_KEY env var.

None
max_retries int

Forwarded to openai.OpenAI -- the SDK's own retry handling, not a custom loop.

2
timeout float

Forwarded to openai.OpenAI, in seconds.

60.0
client OpenAI | None

An already-constructed openai.OpenAI (or a test double matching its chat.completions.create surface) to use instead of constructing one from the arguments above.

None

Raises:

Type Description
OptionalDependencyError

If openai isn't installed and client wasn't given.

Source code in src/linkingtk/llm/client.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def __init__(
    self,
    model: str,
    *,
    base_url: str | None = None,
    api_key: str | None = None,
    max_retries: int = 2,
    timeout: float = 60.0,
    client: openai.OpenAI | None = None,
) -> None:
    self.model = model
    if client is not None:
        self._client = client
    else:
        try:
            import openai
        except ImportError as exc:
            raise OptionalDependencyError("OpenAiClient", "llm") from exc
        self._client = openai.OpenAI(
            base_url=base_url, api_key=api_key, max_retries=max_retries, timeout=timeout
        )

create_client

create_client(model: str, **kwargs: Any) -> LlmClient

Build an LlmClient from a "<provider>/<model>" spec.

  • "openai/gpt-4o" -- OpenAiClient against the real OpenAI API.
  • "anthropic/claude-opus-4-6" -- AnthropicClient.
  • "ollama/llama3.1" -- an OpenAiClient pointed at a local Ollama server's OpenAI-compatible endpoint (base_url defaults to "http://localhost:11434/v1", api_key to a dummy value since Ollama doesn't check one -- both overridable via kwargs).

Parameters:

Name Type Description Default
model str

A "<provider>/<model>" spec, e.g. "ollama/llama3.1".

required
**kwargs Any

Forwarded to the chosen client's constructor.

{}

Raises:

Type Description
ValueError

If model doesn't start with a known provider prefix.

OptionalDependencyError

If the chosen provider's SDK isn't installed.

Source code in src/linkingtk/llm/client.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def create_client(model: str, **kwargs: Any) -> LlmClient:
    """Build an [LlmClient][linkingtk.llm.client.LlmClient] from a `"<provider>/<model>"` spec.

    - `"openai/gpt-4o"` -- [OpenAiClient][linkingtk.llm.client.OpenAiClient]
      against the real OpenAI API.
    - `"anthropic/claude-opus-4-6"` --
      [AnthropicClient][linkingtk.llm.client.AnthropicClient].
    - `"ollama/llama3.1"` -- an `OpenAiClient` pointed at a local Ollama
      server's OpenAI-compatible endpoint (`base_url` defaults to
      `"http://localhost:11434/v1"`, `api_key` to a dummy value since Ollama
      doesn't check one -- both overridable via `kwargs`).

    Args:
        model: A `"<provider>/<model>"` spec, e.g. `"ollama/llama3.1"`.
        **kwargs: Forwarded to the chosen client's constructor.

    Raises:
        ValueError: If `model` doesn't start with a known provider prefix.
        OptionalDependencyError: If the chosen provider's SDK isn't installed.
    """
    provider, sep, name = model.partition("/")
    if not sep:
        raise ValueError(
            f"model must be '<provider>/<model>' (e.g. 'ollama/llama3.1'), got: {model!r}"
        )
    if provider == "openai":
        return OpenAiClient(name, **kwargs)
    if provider == "anthropic":
        return AnthropicClient(name, **kwargs)
    if provider == "ollama":
        kwargs.setdefault("base_url", "http://localhost:11434/v1")
        kwargs.setdefault("api_key", "ollama")
        return OpenAiClient(name, **kwargs)
    raise ValueError(
        f"Unknown LLM provider {provider!r} in model spec {model!r}; "
        "expected 'openai/...', 'anthropic/...', or 'ollama/...'"
    )