LLM
LLM is an abstract facade every provider adapter
(Monad\Clarity\Services\LLMAdapters\{OpenAI,Anthropic,DeepSeek,Gemini}) implements: one
complete() method translating a provider-agnostic request to that provider's wire
format and back. No global registry — construct the adapter you want directly, with its own
credentials and an HttpClient:
use Monad\Clarity\Services\HttpClient;
use Monad\Clarity\Services\LLMAdapters\Anthropic;
use Monad\Clarity\Services\LLM\LLMRequest;
$adapter = new Anthropic(apiKey: $apiKey, httpClient: new HttpClient());
$response = $adapter->complete(new LLMRequest(
model: 'claude-opus-5',
messages: [['role' => 'user', 'content' => 'Say hello in five words.']],
systemInstruction: 'You are terse.',
));
$response->content; // the model's text reply
LLMRequest fields
new LLMRequest(
string $model,
array $messages, // [['role' => 'user'|'assistant', 'content' => string], ...]
?string $systemInstruction = null, // a top-level field, never a message — providers disagree on system-role messages
float $temperature = 1.0, // 0.0–2.0
int $maxOutputTokens = 1024,
int $timeoutSeconds = 30,
?array $responseSchema = null, // a JSON Schema the output must conform to; null = plain text
)
Validated at construction — a malformed request throws before any network round trip.
LLMResponse fields
$response->provider; // e.g. 'anthropic'
$response->model;
$response->content; // string, or array if $responseSchema was set and honoured
$response->usage; // ['inputTokens' => int, 'outputTokens' => int]
$response->providerRequestId; // ?string
$response->raw; // the provider's full decoded response body — an escape hatch
Structured JSON output
Set responseSchema to a JSON Schema and $response->content comes back as a
decoded array instead of a string. Each provider has its own mechanism behind that one
field — OpenAI and Gemini enforce the schema server-side, DeepSeek offers JSON mode without schema
enforcement, and Anthropic has two mechanisms, neither of which reaches every model.
use Monad\Clarity\Services\LLMAdapters\AnthropicStructuredOutput;
$adapter = new Anthropic(
apiKey: $apiKey,
httpClient: new HttpClient(),
structuredOutput: AnthropicStructuredOutput::NativeSchema,
);
Requires Clarity 1.8.0 or newer. ForcedTool is the default and the
behaviour of every earlier release: a synthetic tool whose schema is yours, with the answer read out
of the resulting tool call. It accepts any JSON Schema you can write, and the newest Anthropic models
refuse it. NativeSchema uses Anthropic's own JSON-schema response mode, which those
models require — but it is absent from several older ones and accepts a narrower range of schema:
no recursion, no numeric or string bounds, and additionalProperties: false on every
object. A rejected schema comes back as a precise error naming the offending keyword.
A model id does not say which mechanism it supports, so the choice is yours to make once, per adapter, rather than something the framework can guess per request. The default was left unchanged so existing code keeps the behaviour it already had.
Workspace-scoped Anthropic keys
$adapter = new Anthropic(
apiKey: $apiKey,
httpClient: new HttpClient(),
workspaceId: 'wrkspc_01abc',
);
Requires Clarity 1.8.0 or newer. Only needed if your API key is not itself scoped to a workspace — Anthropic refuses such a key outright unless the request names one. Leave it unset otherwise and no header is sent.
Provider notes
-
OpenAI — the output cap is sent as
max_completion_tokens. Requires Clarity 1.8.0; earlier releases sentmax_tokens, which every current OpenAI chat model rejects, so the adapter could reach only older models. -
Anthropic —
temperatureis sent only when you move it off the default of1.0. Current Anthropic models refuse a non-default value; if you set one, it is sent as asked and the model decides. -
Gemini — the model is part of the URL path and the key is a query parameter,
both Google's own conventions. Assistant turns are translated to Gemini's
modelrole so your message list stays provider-neutral. - DeepSeek — structured output is best-effort: DeepSeek guarantees valid JSON but does not enforce the schema server-side, so the adapter also states the schema in the system instruction.
What's deliberately not here
No agents, tool orchestration, vector databases, memory, prompt pipelines, or automatic cross-provider retries — a thin, provider-agnostic request/response contract, not an agentic framework.
Next steps
- HttpClient — what every adapter sends requests over.