API reference
Start with the support queue tutorial for complete working code. Use the field catalog for definition fields, defaults, and constraints. This reference is generated from supported root APIs and public extension modules. Planner and executor implementation types such as JobRunner, JobSpec, and TrialSpec are intentionally omitted.
Model constructors are described by their field contracts rather than duplicating long generated signatures. Methods below are defined on the listed class; ordinary inherited Pydantic methods are not repeated.
Client provider adapters are the application inference API. Public Job native execution is a separate OpenAI-compatible chat-completions path.
plural.Client
Unified client for routing, tracing, and (via other modules) environments.
Args: api_key: Hosted plural gateway API key. Mutually exclusive with building providers solely from ``providers`` / env vars when you want direct mode. providers: Mapping of provider slug to API key string or Provider instance. base_url: Gateway base URL when using ``api_key``. catalog: Optional model catalog. policy: Optional routing policy. sink: Trace sink. Defaults to ``.plural/traces.jsonl``. redactor: Optional redactor applied before persistence. sampler: Optional sampler. capture_content: Whether to keep full prompt/response content in traces. max_retries: Retries per route. max_cost_usd: Optional per-request budget. default_headers: Extra headers for the gateway client. tags: Default tags applied to every trace. project: Project id required when ``api_key`` is account-scoped. Project-scoped keys already know the project. Also reads ``PLURAL_PROJECT``. Examples: Hosted gateway (reads ``PLURAL_API_KEY``):: client = Client() Bring-your-own upstream keys:: client = Client(providers={"openai": "sk-..."}) Account key (project required):: client = Client(api_key="plural_...", project="<project_id>") env = Environment(name="refund-support", version="0.1.0") client.create(env)plural.Client(*, api_key: 'str | None' = None, providers: 'Mapping[str, str | Provider] | None' = None, base_url: 'str | None' = None, catalog: 'ModelCatalog | None' = None, policy: 'RoutingPolicy | None' = None, sink: 'Sink | None' = None, redactor: 'Redactor | None' = None, sampler: 'Sampler | None' = None, capture_content: 'bool | None' = None, max_retries: 'int' = 2, max_cost_usd: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, tags: 'dict[str, str] | None' = None, trace_dir: 'str | Path | None' = None, project: 'str | None' = None) -> 'None'plural.Client.is_authenticated
is_authenticated(self) -> 'bool'Return whether configured credentials are accepted.
Sends a lightweight authenticated request to each HTTP provider.In-process providers (no HTTP client) are treated as authenticated. Returns: ``True`` when every provider accepts the credentials or has no HTTP client to probe. Raises: PluralTimeoutError: If a provider cannot be reached. PluralError: If a transport error occurs while probing.plural.Client.push
push(self, obj: 'Any', **kwargs: 'Any') -> 'dict[str, Any]'Push one immutable revision, creating its parent by slug.
The revision is usable as soon as it is saved and stays private tothe project. Pushing identical content again returns the samerevision. To push a project directory with its dependencies, use``plural <kind> push`` or :func:`plural.project.sync.push`. Args: obj: Environment, Task, Verifier, Agent, Harness, Benchmark, or Trace. **kwargs: Hosted revision ids for graph edges, such as a Task's ``environment_revision_id`` and ``verifier_revision_ids``. Returns: The hosted revision record.plural.Client.chat
chat(self, *, model: 'str', messages: 'Sequence[Message | dict[str, Any]]', models: 'list[str] | None' = None, tools: 'Sequence[Tool | dict[str, Any]] | None' = None, temperature: 'float | None' = None, max_tokens: 'int | None' = None, metadata: 'dict[str, Any] | None' = None, tags: 'dict[str, str] | None' = None, write_trace: 'bool' = True, trace_context: 'TraceContext | None' = None, **kwargs: 'Any') -> 'ChatResponse'Create a chat completion.
Args: model: Primary model id (``author/slug``). messages: Conversation messages. models: Optional fallback model chain. tools: Optional tools. temperature: Sampling temperature. max_tokens: Max tokens to generate. metadata: Request metadata stored on the trace. tags: Extra trace tags. write_trace: Whether to persist this model-call trace. trace_context: Optional parent and episode lineage. **kwargs: Additional :class:`~plural.types.ChatRequest` fields. Returns: Normalized :class:`~plural.types.ChatResponse`.plural.Client.achat
achat(self, *, model: 'str', messages: 'Sequence[Message | dict[str, Any]]', models: 'list[str] | None' = None, tools: 'Sequence[Tool | dict[str, Any]] | None' = None, temperature: 'float | None' = None, max_tokens: 'int | None' = None, metadata: 'dict[str, Any] | None' = None, tags: 'dict[str, str] | None' = None, write_trace: 'bool' = True, trace_context: 'TraceContext | None' = None, **kwargs: 'Any') -> 'ChatResponse'Async chat completion.
Args: model: Primary model id. messages: Conversation messages. models: Optional fallback chain. tools: Optional tools. temperature: Sampling temperature. max_tokens: Max tokens. metadata: Request metadata. tags: Extra trace tags. write_trace: Whether to persist this model-call trace. trace_context: Optional parent and episode lineage. **kwargs: Additional request fields. Returns: Normalized chat response.plural.Client.stream
stream(self, *, model: 'str', messages: 'Sequence[Message | dict[str, Any]]', models: 'list[str] | None' = None, tags: 'dict[str, str] | None' = None, write_trace: 'bool' = True, trace_context: 'TraceContext | None' = None, **kwargs: 'Any') -> 'Iterator[StreamChunk]'Stream a chat completion and record a trace on completion.
Args: model: Primary model id. messages: Conversation messages. models: Optional fallback chain. tags: Extra trace tags. write_trace: Whether to persist this model-call trace. trace_context: Optional parent and episode lineage. **kwargs: Additional request fields. Yields: Stream chunks.plural.Client.astream
astream(self, *, model: 'str', messages: 'Sequence[Message | dict[str, Any]]', models: 'list[str] | None' = None, tags: 'dict[str, str] | None' = None, write_trace: 'bool' = True, trace_context: 'TraceContext | None' = None, **kwargs: 'Any') -> 'AsyncIterator[StreamChunk]'Async streaming chat completion.
Args: model: Primary model id. messages: Conversation messages. models: Optional fallback chain. tags: Extra trace tags. write_trace: Whether to persist this model-call trace. trace_context: Optional parent and episode lineage. **kwargs: Additional request fields. Yields: Stream chunks.plural.Client.label
label(self, trace_id: 'str', *, scores: 'dict[str, float] | None' = None, reward: 'float | None' = None, labels: 'dict[str, Any] | None' = None, feedback: 'str | None' = None) -> 'None'Attach a late outcome label to a trace.
Args: trace_id: Trace id returned via ``response.raw['plural_trace_id']``. scores: Named scores. reward: Scalar reward. labels: Discrete labels. feedback: Free-form feedback.plural.Client.flush
flush(self) -> 'None'Flush pending traces.
plural.Client.close
close(self) -> 'None'Flush traces and close providers.
plural.Client.aclose
aclose(self) -> 'None'Async close.
plural.types.AudioContent
An audio content part.
Attributes: type: Discriminator; always ``"input_audio"``. data: Base64-encoded audio bytes. format: Audio format such as ``"wav"`` or ``"mp3"``.plural.types.ChatRequest
A normalized chat completion request.
Attributes: model: Primary model id in ``author/slug`` form. messages: Conversation messages. models: Optional fallback model chain (tried in order after ``model``). temperature: Sampling temperature. top_p: Nucleus sampling parameter. max_tokens: Maximum tokens to generate. stop: Stop sequence(s). tools: Tools available to the model. tool_choice: Tool selection strategy or a forced tool. response_format: Structured output request. stream: Whether to stream the response. seed: Deterministic seed when supported. user: End-user identifier for abuse detection / analytics. provider: Provider routing preferences. metadata: Arbitrary request metadata propagated into traces. extra: Provider-specific passthrough fields. Examples: >>> ChatRequest( ... model="anthropic/claude-sonnet-4", ... messages=[Message(role="user", content="Hi")], ... ).temperature is None Trueplural.types.ChatResponse
A normalized chat completion response.
Attributes: id: Provider-assigned completion id. model: Model that produced the response (may differ from the request). choices: Completion choices. usage: Token usage and optional cost. provider: Provider slug that served the request. region: Region of the host that served it. The same provider charges different rates per region, so billing needs both. created: Unix timestamp when the completion was created. raw: Original provider payload for debugging. latency_ms: End-to-end latency in milliseconds. attempts: Number of attempts (including retries/fallbacks) used. Examples: >>> resp = ChatResponse( ... id="chatcmpl-1", ... model="openai/gpt-4o-mini", ... choices=[Choice(message=Message(role="assistant", content="Hi"))], ... ) >>> resp.text 'Hi'plural.types.Choice
A single completion choice.
Attributes: index: Choice index. message: The assistant message. finish_reason: Why generation stopped.plural.types.FinishReason
Why generation stopped.
plural.types.FinishReason(*values)plural.types.FunctionCall
A function invocation requested by the model.
Attributes: name: Function name to call. arguments: JSON-encoded argument object as a string.plural.types.FunctionDefinition
JSON-schema function definition for tool calling.
Attributes: name: Function name exposed to the model. description: Human-readable description of the function. parameters: JSON Schema object describing the parameters. strict: Whether to request strict schema adherence when supported.plural.types.ImageContent
An image content part (URL or base64 data URL).
Attributes: type: Discriminator; always ``"image_url"``. url: HTTP(S) URL or ``data:`` URL for the image. detail: Optional detail hint for vision models.plural.types.Message
A single chat message.
Attributes: role: Message role. content: Text content, a list of content parts, or ``None`` when the message only carries tool calls. name: Optional participant name. tool_calls: Tool calls requested by the assistant, if any. tool_call_id: Id of the tool call this message responds to (tool role). reasoning: Reasoning ("thinking") text the model produced before its answer. Separate from ``content`` because callers usually render it differently, and some hosts bill it as output tokens. reasoning_signature: Host-issued attestation for ``reasoning``. Anthropic and Bedrock reject a thinking block replayed without it, so it has to survive a round trip. Examples: >>> Message(role="user", content="Hello").role 'user'plural.types.ProviderPreferences
OpenRouter-compatible provider routing preferences.
Attributes: order: Provider slugs to try in order. allow_fallbacks: Whether to allow backup providers. require_parameters: Only use providers that support all request params. data_collection: Whether to allow providers that may store data. only: Allow-list of provider slugs. ignore: Deny-list of provider slugs. sort: Sort key such as ``"price"``, ``"latency"``, or ``"throughput"``. max_price: Maximum price constraints for prompt/completion tokens.plural.types.ResponseFormat
Structured output / response format request.
Attributes: type: Format type such as ``"text"``, ``"json_object"``, or ``"json_schema"``. json_schema: Schema payload when ``type`` is ``"json_schema"``.plural.types.Role
Chat message role.
plural.types.Role(*values)plural.types.StreamChunk
A single streaming chunk.
Attributes: id: Completion id. model: Model id. delta: Incremental content. finish_reason: Present on the final chunk when known. usage: Usage, typically only on the final chunk. provider: Provider slug. region: Region of the host serving the stream. raw: Original provider chunk payload.plural.types.StreamChunk.to_openai
to_openai(self) -> 'dict[str, Any]'Serialize this chunk as an OpenAI Chat Completions SSE object.
Every adapter yields :class:`StreamChunk`. Hosted gateways and HTTPclients that speak the OpenAI wire format must call this rather thanforwarding :attr:`raw`, which is the provider-native event and is nota stable client contract. Returns: A ``chat.completion.chunk`` object. ``usage`` is present only when the host reported it, typically on the last chunk.plural.types.StreamDelta
A partial delta within a stream chunk.
Attributes: role: Role, typically present only on the first delta. content: Incremental text content. reasoning: Incremental reasoning ("thinking") text. Thinking models emit this for seconds before any ``content`` appears, so a client that ignores it looks frozen. reasoning_started: ``True`` when a thinking block opens. Some hosts encrypt the tokens and send empty ``thinking_delta`` events; this flag is how a UI learns the model is thinking anyway. reasoning_finished: ``True`` when that thinking block closes. reasoning_signature: Incremental attestation for the reasoning block. tool_calls: Incremental tool call fragments, in OpenAI's streaming shape (``index``, ``id``, ``function.name``, ``function.arguments``).plural.types.TextContent
A text content part.
Attributes: type: Discriminator; always ``"text"``. text: The text content.plural.types.Tool
A tool the model may call.
Attributes: type: Tool type; currently only ``"function"``. function: The function definition. Examples: >>> tool = Tool( ... function=FunctionDefinition( ... name="lookup_order", ... description="Look up an order by id", ... parameters={ ... "type": "object", ... "properties": {"order_id": {"type": "string"}}, ... "required": ["order_id"], ... }, ... ) ... ) >>> tool.function.name 'lookup_order'plural.types.ToolCall
A tool call in an assistant message.
Attributes: id: Provider-assigned call id used to correlate tool responses. type: Tool type; currently only ``"function"``. function: The function call payload. extra_content: Opaque provider data that must be sent back with the call on the next turn, keyed by provider, such as Gemini's ``{"google": {"thought_signature": ...}}``. Gemini rejects a replayed function call without its signature.plural.types.Usage
Token usage for a completion.
Attributes: prompt_tokens: Input token count. completion_tokens: Output token count. Includes reasoning tokens when the provider bills them as output. total_tokens: Sum of prompt and completion tokens. cost: Estimated USD cost when known. reasoning_tokens: How many completion tokens were reasoning, when the provider reports the split. ``None`` means unreported, not zero. cache_read_tokens: How many prompt tokens were served from the host's prompt cache. Always a subset of ``prompt_tokens``. cache_write_tokens: How many prompt tokens were written to the host's prompt cache. Always a subset of ``prompt_tokens``. cache_write_long_tokens: How many of ``cache_write_tokens`` were written with a long-lived cache (Anthropic's one-hour TTL), which bills higher.plural.types.Usage.from_counts
from_counts(prompt: 'int', completion: 'int', cost: 'float | None' = None, *, reasoning_tokens: 'int | None' = None, cache_read_tokens: 'int' = 0, cache_write_tokens: 'int' = 0, cache_write_long_tokens: 'int' = 0) -> 'Usage'Build a :class:Usage from prompt/completion counts.
Args: prompt: Prompt token count, including any cached tokens. completion: Completion token count. cost: Optional USD cost. reasoning_tokens: Reasoning tokens included in ``completion``, when reported. cache_read_tokens: Prompt tokens read from the host's cache. cache_write_tokens: Prompt tokens written to the host's cache. cache_write_long_tokens: Cache writes with a long-lived TTL. Returns: A populated :class:`Usage` instance. Examples: >>> Usage.from_counts(10, 5).total_tokens 15plural.types.cached_prompt_tokens_of
Cache reads and writes a host reported inside its prompt count.
OpenAI Chat Completions and the OpenAI-compatible hosts report``prompt_tokens_details.cached_tokens``, the Responses API``input_tokens_details.cached_tokens``, DeepSeek ``prompt_cache_hit_tokens``,Moonshot a top-level ``cached_tokens``, and Gemini ``cachedContentTokenCount``.All of them count those tokens inside the prompt total. Hosts that relayAnthropic report writes as ``prompt_tokens_details.cache_write_tokens``. Args: raw: A provider usage object. Returns: A (cache read, cache write) pair of token counts, zero when unreported. Examples: >>> cached_prompt_tokens_of({"prompt_tokens_details": {"cached_tokens": 64}}) (64, 0)plural.types.cached_prompt_tokens_of(raw: 'Mapping[str, Any] | None') -> 'tuple[int, int]'plural.types.reasoning_tokens_of
The reasoning-token count a provider reported.
OpenAI Chat Completions uses ``completion_tokens_details.reasoning_tokens`` andthe Responses API uses ``output_tokens_details.reasoning_tokens``. Gemini reports``thoughtsTokenCount``. A host may also send ``reasoning_tokens`` directly. Args: raw: A provider usage object. Returns: The count, or ``None`` when the provider reported none.plural.types.reasoning_tokens_of(raw: 'Mapping[str, Any] | None') -> 'int | None'plural.types.text_content
Extract plain text from a message.
Args: message: The message to extract text from. Returns: Concatenated text content, or an empty string.plural.types.text_content(message: 'Message') -> 'str'plural.tracing.Trace
A complete record of an LLM interaction or environment rollout.
Production traffic and environment rollouts emit the same shape so thatdatasets, benchmarks, and a future autorouter can consume either source. Attributes: trace_id: Opaque unique id. environment: Environment name when produced by a rollout. environment_version: Environment version string. environment_fingerprint: Hash of the Environment revision. task_id: Task identity for this Trial. model: Policy / model id used for this episode. initial_state: Explicitly safe Environment snapshot at reset. steps: Ordered steps (decisions, LLM calls, tool calls, events). final_state: Explicitly safe Environment snapshot at episode close. outcome: Optional labeled outcome. ``outcome.reward`` is the return. metrics: Episode aggregates (turns, cost, latency, tool counts). terminated: Natural end (``env.done()``). truncated: Cut off by ``max_turns``. tags: Free-form tags for filtering. metadata: Arbitrary metadata. created_at: Creation timestamp (UTC). schema_version: Trace schema version for stability. Examples: >>> t = Trace(trace_id="t1", tags={"env": "prod"}) >>> t.schema_version '3.0.0'plural.tracing.Trace.add_llm
add_llm(self, *, request: 'ChatRequest | dict[str, Any] | None', response: 'ChatResponse | None', attempts: 'list[Attempt] | None' = None, error: 'str | None' = None) -> 'LLMCall'Append an LLM call step.
Args: request: Chat request. response: Chat response if successful. attempts: Attempt records. error: Error message if failed. Returns: The appended :class:`LLMCall`.plural.tracing.Trace.add_action
add_action(self, name: 'str', arguments: 'dict[str, Any]', *, action_id: 'str | None' = None, tool_call_id: 'str | None' = None, source: 'ActionSource' = 'environment_native', observation: 'Any' = None, result: 'Any' = None, error: 'str | None' = None, latency_ms: 'float | None' = None) -> 'ActionStep'Append an action step.
plural.tracing.Trace.add_turn
add_turn(self, *, observation: 'Any' = None, model_context: 'ChatRequest | dict[str, Any] | None' = None, model_output: 'ChatResponse | None' = None, parsed_action: 'list[ParsedAction] | None' = None, actions: 'list[ActionStep] | None' = None, reward_events: 'list[RewardEvent] | None' = None, reasoning: 'list[ReasoningBlock] | None' = None, turn: 'int | None' = None) -> 'Turn'Append a turn step for one model turn.
plural.tracing.Trace.transitions
transitions(self, *, source: "Literal['outcome', 'events', 'both']" = 'outcome') -> 'list[Transition]'Flatten this episode into step-by-step transitions.
Prefers :class:`Turn` steps. Falls back to grouping flat``llm`` / ``action`` steps so production traces still export. Args: source: Reward source, matching :meth:`turn_rewards`. Returns: One :class:`Transition` per turn.plural.tracing.Trace.turns
turns(self) -> 'list[Turn]'Return turn steps in order.
plural.tracing.Trace.credit
credit(self, value: 'float', *, name: 'str' = 'late', reason: 'str | None' = None, turn_index: 'int | None' = None) -> 'RewardEvent | TraceOutcome'Inject reward after the fact — episode-wide or onto one turn.
Use this when the signal arrives late (likes, a human review, adownstream KPI). Credit assignment across earlier actions is:meth:`returns`, not the environment's job. Args: value: Reward to add. name: Reward source (``"likes"``, ``"review"``, …). reason: Optional human-readable reason. turn_index: Which turn to attribute to. ``None`` updates ``outcome.reward`` (and ``outcome.scores[name]``). Negative indices count from the end. Returns: The new :class:`RewardEvent` or the updated :class:`TraceOutcome`. Raises: IndexError: If ``turn_index`` is out of range.plural.tracing.Trace.turn_rewards
turn_rewards(self, *, source: "Literal['outcome', 'events', 'both']" = 'outcome') -> 'list[float]'Per-turn reward used as `r_t` before discounting.
plural.tracing.Trace.returns
returns(self, gamma: 'float' = 1.0, *, source: "Literal['outcome', 'events', 'both']" = 'outcome') -> 'list[float]'Discounted return `G_t = r_t + γ G_{t+1}` for each decision.
This is how a trainer gives the *research* action credit for a laterpost's likes: the environment does not rewrite history; the trainerwalks the episode backward. Args: gamma: Discount factor in ``[0, 1]``. source: See :meth:`turn_rewards`. Returns: One return per turn, same order as :meth:`turns`. Examples: >>> t = Trace(outcome=TraceOutcome(reward=1.0)) >>> t.add_turn(parsed_action=[ParsedAction(name="search")]) >>> t.add_turn(parsed_action=[ParsedAction(name="answer")]) >>> t.returns(gamma=0.9) [0.9, 1.0]plural.tracing.Trace.label
label(self, *, scores: 'dict[str, float] | None' = None, reward: 'float | None' = None, labels: 'dict[str, Any] | None' = None, feedback: 'str | None' = None) -> 'TraceOutcome'Attach or update the outcome.
Args: scores: Named Verifier outputs. reward: Scalar reward. labels: Discrete labels. feedback: Free-form feedback. Returns: The updated :class:`TraceOutcome`.plural.tracing.TraceContext
Lineage supplied by a caller when creating a child trace.
plural.tracing.resources.trace_json_schema
Load the canonical trace JSON Schema from the installed package.
Returns: The JSON Schema as a mutable dictionary. Raises: ValueError: If the packaged resource is not a JSON object.plural.tracing.resources.trace_json_schema() -> 'dict[str, Any]'plural.Job
Simple public runner for a Task or Benchmark and catalog-backed Agents.
``credentials`` gives each Agent's credential values by Agent name, such as``{"sales-bot": {"SALESFORCE_TOKEN": token}}``. A credential an Agentdeclares and is not given here is read from the exported variable of thesame name. For a Task whose Environment has a party, ``party`` lists the Agents thatshare each episode, in seat order; list one Agent more than once forself-play. Left out, every Agent in ``agents`` plays, once each. ``episode_server`` says where the Job serves an external Agent's episodes;by default it listens on a free loopback port.plural.Job(source: 'Task | Benchmark', agents: 'Sequence[Agent]', *, party: 'Sequence[Agent] | None' = None, mode: 'JobMode' = <JobMode.EVAL: 'eval'>, attempts: 'int' = 1, concurrency: "int | Literal['auto']" = 1, per_runtime_concurrency: 'int | None' = None, priority: 'int' = 0, retry: 'RetryPolicy | None' = None, provider: 'Any' = None, providers: 'Mapping[str, Any] | None' = None, registry: 'Any' = None, store: 'Any' = None, environ: 'Mapping[str, str] | None' = None, resource_resolvers: 'Mapping[str, Any] | None' = None, progress: 'Any' = None, project_policy: 'Any' = None, catalog: 'ModelCatalog | None' = None, client: 'Any' = None, api_key: 'str | None' = None, credentials: 'Mapping[str, Mapping[str, str]] | None' = None, episode_server: 'Any' = None) -> 'None'plural.Job.run_async
run_async(self, *, resume: 'bool' = False) -> 'JobResult'Execute this Job without blocking the caller's event loop.
Returns: The collected Trial outcomes.plural.Job.run
run(self, *, resume: 'bool' = False) -> 'JobResult'Execute synchronously when no event loop is already running.
Returns: The collected Trial outcomes.plural.Benchmark
A semantic version that pins an ordered set of Tasks.
Each version is a release. Beyond its Tasks it may declare categories,scoring rules, and evaluation tracks; see :class:`BenchmarkRelease`.plural.Benchmark.diff
diff(self, other: 'Benchmark') -> 'BenchmarkDiff'Return the structured pin and configuration changes from this version.
plural.Benchmark.dependency_graph
dependency_graph(self) -> 'dict[str, Any]'Export the complete pinned graph in deterministic dependency order.
Returns: Benchmark metadata and all pinned Task dependencies.plural.Benchmark.export
export(self) -> 'dict[str, Any]'Return the deterministic complete dependency graph.
plural.project.resources.LocalResource
One validated local resource and the SDK object it defines.
plural.project.resources.LocalResource(ref: 'ResourceRef', directory: 'Path', value: 'Any', dependencies: 'tuple[ResourceRef, ...]') -> Noneplural.project.manifests.LockEntry
One resolved resource: its local identity and, once pushed, its revision.
plural.project.manifests.LockFile
plural.lock`: resolved dependency revisions and content hashes.
Hosted identifiers belong to ``project_id``. Entries recorded for anotherhosted project are never used as dependency pins.plural.project.layout.Project
A project directory and its identity.
plural.project.layout.Project(root: 'Path', manifest: 'ProjectManifest') -> Noneplural.project.layout.Project.find
find(start: 'Path | None' = None) -> 'Project'Load the project containing `start` (default: the working directory).
Returns: The project. Raises: ProjectError: When no enclosing directory holds ``project.yaml``.plural.project.layout.Project.at
at(root: 'Path') -> 'Project'Load the project whose `project.yaml is directly in root`.
Returns: The project. Raises: ProjectError: When ``project.yaml`` is missing or invalid.plural.project.layout.Project.resource_dir
resource_dir(self, ref: 'ResourceRef') -> 'Path'Directory that holds one resource.
Returns: The resource directory, whether or not it exists. Raises: ValueError: When the name is not one folder name, such as one a hosted record supplied with ``/`` or ``..``.plural.project.layout.Project.manifest_path
manifest_path(self, ref: 'ResourceRef') -> 'Path'Canonical manifest path for one resource.
Returns: The manifest path, whether or not it exists.plural.project.layout.Project.has
has(self, ref: 'ResourceRef') -> 'bool'Whether the resource directory exists locally.
Returns: ``True`` when the directory exists.plural.project.layout.Project.names
names(self, kind: 'ResourceKind') -> 'list[str]'Local resource names of one kind, sorted.
Returns: The names.plural.project.layout.Project.resource_at
resource_at(self, path: 'Path') -> 'ResourceRef | None'The resource whose directory contains `path`, if any.
Returns: The resource, or ``None`` outside any resource directory.plural.project.layout.Project.read_lock
read_lock(self) -> 'LockFile'Return `plural.lock`, or an empty lock when none exists yet.
Raises: ProjectError: When the lock exists but is invalid.plural.project.layout.Project.write_lock
write_lock(self, lock: 'LockFile') -> 'None'Write `plural.lock` with resources in a stable order.
plural.project.layout.Project.read_binding
read_binding(self) -> 'ProjectBinding | None'Return the hosted project this checkout is bound to, if any.
Raises: ProjectError: When the binding file is unreadable.plural.project.layout.Project.write_binding
write_binding(self, binding: 'ProjectBinding') -> 'None'Bind this checkout to a hosted project.
plural.project.manifests.ProjectBinding
.plural/project.json`: the hosted project this checkout pushes to.
plural.project.layout.ProjectError
A project, resource, or command precondition that the user must fix.
``problems`` lists every independent issue so one run reports all of them.plural.project.layout.ProjectError(message: 'str', problems: 'list[str] | None' = None) -> 'None'plural.project.manifests.ProjectManifest
project.yaml`: project identity and the layout version it follows.
plural.project.layout.ResourceKind
One resource kind: its CLI noun, directory, manifest, and hosted collection.
plural.project.layout.ResourceKind(name: 'str', cli: 'str', directory: 'str', manifest: 'str', label: 'str') -> Noneplural.project.layout.ResourceRef
A resource named within one project, such as `task/refund`.
plural.project.layout.ResourceRef(kind: 'str', name: 'str') -> Noneplural.project.layout.ResourceRef.parse
parse(value: 'str') -> 'ResourceRef'Parse `kind/name`.
Returns: The reference.plural.project.resources.Workspace
Resolve resources by name inside one project, caching each load.
plural.project.resources.Workspace(project: 'Project', *, catalog: 'ModelCatalog | None' = None) -> 'None'plural.project.resources.Workspace.load
load(self, ref: 'ResourceRef') -> 'LocalResource'Load and fully validate one resource and its dependencies.
Returns: The validated resource. Raises: ProjectError: Listing every problem found.plural.project.resources.Workspace.get
get(self, kind: 'str', name: 'str') -> 'Any'Load a resource by kind and name, the way the CLI addresses it.
``kind`` is a kind name or its CLI noun, so ``workspace.get("env","support-queue")`` is the object ``plural env show support-queue``describes. Args: kind: ``environment``/``env``, ``task``, ``verifier``, ``harness``, ``agent``, or ``benchmark``. name: The resource's directory name. Returns: The validated SDK object, such as a ``Task`` or ``Benchmark``. Raises: ProjectError: When the resource is missing or invalid. Examples: >>> from plural.project import Project, Workspace >>> workspace = Workspace(Project.find()) # doctest: +SKIP >>> benchmark = workspace.get("benchmark", "support-triage") # doctest: +SKIPplural.project.resources.Workspace.unfinished
unfinished(self, roots: 'Iterable[ResourceRef]') -> 'list[str]'Every `PLURAL-TODO marker left in roots` and what they depend on.
A resource with markers still loads and runs locally; it cannot bepushed, released, or recorded until they are gone. Returns: One ``path:line: note`` entry per marker, dependencies first.plural.project.resources.Workspace.dependencies
dependencies(self, ref: 'ResourceRef') -> 'tuple[ResourceRef, ...]'Direct dependencies declared in a manifest, without loading code.
Returns: The declared dependencies. Raises: ProjectError: When the manifest cannot be read.plural.project.resources.Workspace.dependency_order
dependency_order(self, roots: 'Iterable[ResourceRef]') -> 'list[ResourceRef]'Return `roots` and everything they depend on, dependencies first.
Unresolved references and cycles are reported before any code isimported or anything is uploaded. Raises: ProjectError: Listing unresolved references or the first cycle.plural.project.layout.check_name
Validate a project or resource name.
A valid name is already a hosted slug, so a local directory and its hostedresource are the same string on every filesystem. Returns: The name, unchanged. Raises: ProjectError: When the name cannot be used as a directory and slug.plural.project.layout.check_name(name: 'str', what: 'str') -> 'str'plural.project.layout.find_project_root
Return the nearest directory at or above `start holding project.yaml`.
plural.project.layout.find_project_root(start: 'Path | None' = None) -> 'Path | None'plural.project.schemas.manifest_schemas
Schemas for every file a project author writes by hand.
Returns: Schemas keyed by file name, such as ``task.yaml``.plural.project.schemas.manifest_schemas() -> 'dict[str, dict[str, Any]]'plural.project.schemas.public_schema
Return a JSON Schema containing only public authoring fields.
Returns: A detached schema safe for generated public references.plural.project.schemas.public_schema(model: 'type[BaseModel]') -> 'dict[str, Any]'plural.project.layout.resource_kind
Return a kind by its name (`environment) or CLI noun (env`).
Raises: ValueError: When the name is not a resource kind.plural.project.layout.resource_kind(name: 'str') -> 'ResourceKind'plural.execution.JobStore
Crash-safe filesystem persistence for local execution.
plural.execution.JobStore(root: 'Path' = PosixPath('<default>'), *, on_event: 'Callable[[ProgressEvent], None] | None' = None) -> 'None'plural.execution.JobStore.job_path
job_path(self, job_id: 'str') -> 'Path'Return a validated job directory.
plural.execution.JobStore.initialize
initialize(self, spec: 'JobSpec', plan: 'JobPlan') -> 'Path'Create or validate a locked job directory.
plural.execution.JobStore.load_spec
load_spec(self, job_id: 'str') -> 'JobSpec'Load a persisted job config.
plural.execution.JobStore.load_lock
load_lock(self, job_id: 'str') -> 'JobLock'Load a persisted reproducibility lock.
plural.execution.JobStore.list_jobs
list_jobs(self) -> 'tuple[dict[str, Any], ...]'List stored jobs without accepting partial files as results.
plural.execution.JobStore.emit
emit(self, job_id: 'str', event_type: 'str', status: 'str', *, trial_id: 'str | None' = None, execution_id: 'int | None' = None, message: 'str' = '', data: 'Mapping[str, Any] | None' = None, secret_values: 'Iterable[str]' = ()) -> 'ProgressEvent'Append one monotonic, sanitized progress event.
plural.execution.JobStore.events
events(self, job_id: 'str', *, after: 'int' = 0, follow: 'bool' = False, poll_interval: 'float' = 0.1) -> 'Iterable[ProgressEvent]'Yield events after a sequence cursor, optionally following updates.
plural.execution.JobStore.request_cancel
request_cancel(self, job_id: 'str') -> 'None'Atomically create a cancellation marker.
plural.execution.JobStore.clear_cancel
clear_cancel(self, job_id: 'str') -> 'None'Clear a prior cancellation marker before explicit resume.
plural.execution.JobStore.cancel_requested
cancel_requested(self, job_id: 'str') -> 'bool'Return whether cancellation was requested.
plural.execution.JobStore.trial_path
trial_path(self, trial: 'TrialSpec | str', job_id: 'str | None' = None) -> 'Path'Return a validated trial directory.
plural.execution.JobStore.successful_result
successful_result(self, trial: 'TrialSpec') -> 'TrialResult | None'Return a successful persisted trial result, if present.
plural.execution.JobStore.next_execution_id
next_execution_id(self, trial: 'TrialSpec') -> 'int'Return the next monotonic execution ID for a trial.
plural.execution.JobStore.trial_results
trial_results(self, job_id: 'str') -> 'tuple[TrialResult, ...]'Load all complete trial results in lexical order.
plural.execution.JobStore.write_trial_execution
write_trial_execution(self, trial: 'TrialSpec', execution_id: 'int', result: 'TrialResult', *, stdout: 'bytes' = b'', stderr: 'bytes' = b'', artifacts: 'Iterable[DownloadedFile]' = (), verifier_stdout: 'bytes' = b'', verifier_stderr: 'bytes' = b'') -> 'Path'Append one immutable execution and update the selected result.
plural.execution.JobStore.write_job_result
write_job_result(self, result: 'JobResult') -> 'Path'Atomically publish the aggregate result.
plural.execution.JobStore.write_review
write_review(self, trial: 'TrialSpec', verifier_name: 'str', submission: 'Mapping[str, Any]', result: 'TrialResult') -> 'Path'Append one immutable human review and publish its resolved Trial result.
plural.execution.JobStore.read_job_result
read_job_result(self, job_id: 'str') -> 'JobResult | None'Load the aggregate result when complete.
plural.execution.JobStore.write_sync_state
write_sync_state(self, job_id: 'str', state: 'Mapping[str, Any]') -> 'Path'Persist replay-safe hosted upload metadata without credentials.
plural.execution.JobStore.read_sync_state
read_sync_state(self, job_id: 'str') -> 'dict[str, Any] | None'Read hosted upload metadata when a prior sync was registered.
plural.Environment
Apps the Agent acts in, with one Runtime, policy, and set of limits.
An Environment owns the episode: ``reset`` and ``step``, the table ofactions every App offers, and the rule that the Agent only ever sees anaction's observation. Most Environments need no subclass:: environment = Environment(apps=[Wordle()], runtime=Runtime.docker()) Subclass it only when Apps affect one another. Override :meth:`on_step`to react after any App's action, or :meth:`terminated` to end the episodefrom the state of several Apps.plural.Environment(*, apps: 'Iterable[App[Any]]' = (), runtime: 'EnvironmentRuntime | Mapping[str, Any] | None' = None, name: 'str | None' = None, version: 'str | None' = None, description: 'str | None' = None, overview: 'str | None' = None, readme: 'str | None' = None, resources: 'Iterable[EnvironmentResource]' = (), guardrails: 'Iterable[Guardrail]' = (), harness_policy: 'HarnessPolicy | None' = None, limits: 'ExecutionLimits | None' = None, party: 'Party | int | Mapping[str, int] | None' = None, metadata: 'dict[str, Any] | None' = None, source: 'str | Path | None' = None) -> 'None'plural.Environment.on_step
on_step(self, call: 'ActionCall', result: 'Any', apps: 'Mapping[str, Any]') -> 'None'React after any App's action, for effects that span Apps.
``apps`` maps each App's name to a handle with ``call(action, **args)``,``snapshot()``, and ``state``. An effect here is not an Agent action andis not shown to the Agent. Apps never call each other directly. Rewards are not given here: they depend on the Task, so they come fromthe ``reward`` hook of a Task's Verifier.plural.Environment.terminated
terminated(self) -> 'bool'Return whether the episode is over. Default: any App says it is.
plural.Environment.truncated
truncated(self) -> 'bool'Return whether the episode was cut short. Default: any App says so.
plural.Environment.reset
reset(self, *, initial_state: 'Mapping[str, Any] | None' = None, info: 'Any' = None, seed: 'int | None' = None, verifiers: 'Sequence[Any]' = ()) -> 'tuple[Any, dict[str, Any]]'Start an episode in this process.
``initial_state`` maps each App's name to its starting state; with oneApp it may be that App's state directly. ``verifiers`` gives each:meth:`step` its reward: Verifiers with a ``reward`` hook, or functionsof one :class:`~plural.Step`. Returns: The opening observation and reset information.plural.Environment.step
step(self, action: 'str | Mapping[str, Any] | None' = None, /, **kwargs: 'Any') -> 'tuple[Any, float, bool, bool, dict[str, Any]]'Apply one Agent action in this process.
``action`` is an exposed action name with keyword arguments, or amapping with ``name`` and the arguments. With exactly one action thename may be omitted. Returns: Observation, reward, terminated, truncated, and step information.plural.Environment.snapshot
snapshot(self) -> 'dict[str, Any]'Return each App's state, as a Verifier receives it after the episode.
plural.Environment.close
close(self) -> 'None'End the in-process episode and release every App's connections.
plural.action
Mark an :class:~plural.apps.App method as an action the Agent can take.
Args: fn: Method to register (decorator usage). name: Optional explicit action name. Defaults to the method name. read_only: The action never changes state. Verifiers may call only read-only actions after the episode. timeout_seconds: How long one call may run. Returns: The original method (decorator) or a decorator.plural.action(fn: 'Callable[..., Any] | None' = None, *, name: 'str | None' = None, read_only: 'bool' = False, timeout_seconds: 'float' = 30) -> 'Any'plural.DeterministicVerifier
Code that verifies an episode: it scores the end, rewards actions, or both.
``check`` scores the finished episode; that score is what Tasks andBenchmarks rank on. It is a function of the :class:`Episode`, a``{"python": "verify.py:check"}`` reference, or a command argv. ``reward`` rewards each action while the episode runs. It is a function ofone :class:`Step`, or a ``{"python": "verify.py:reward"}`` reference, andruns in the Runtime beside the Apps, so it can read every App. What itreturns goes to the Trial's reward ledger. Rewards reach the score onlywhen ``check`` is :func:`total_reward`. ``hindsight`` credits the finished trajectory as a whole: a function of a:class:`~plural.rewards.HindsightTrial` that returns:class:`~plural.rewards.Credit` values, or a ``{"python":"verify.py:efficiency"}`` reference. It runs after the score, and can alsorun later on stored Trials with ``plural verifier apply``.plural.DeterministicVerifier.bind_source
bind_source(self, digest: 'str', directory: 'Path | None' = None) -> 'None'Fingerprint callable hooks by the package that ships them.
plural.DeterministicVerifier.bind_source_digest
bind_source_digest(self, digest: 'str') -> 'None'Fingerprint callable hooks by the package that ships them.
plural.DeterministicVerifier.invoke
invoke(self, episode: 'Episode') -> 'VerifierOutput'Run `check` in-process on the finished Episode.
Returns: A validated VerifierOutput.plural.DeterministicVerifier.model_post_init
model_post_init(self: 'BaseModel', context: 'Any', /) -> 'None'This function is meant to behave like a BaseModel method to initialize private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it. Args: self: The BaseModel instance. context: The context.plural.Step
One action the Agent took, as a Verifier's `reward` hook sees it.
``observation`` is what the Agent saw back; ``error`` is set instead whenthe action rejected its input. ``previous_state`` and ``current_state``are the acting App's snapshots around the action, or ``None`` when it doesnot snapshot on each step. ``apps`` reads any App, by name: ``state`` fora Python App, ``snapshot()``, and ``query(action, **arguments)`` for aread-only action. Nothing here reaches the Agent. When several Agents share the episode, ``seat`` is the one that acted and``seats`` lists them all. A number a ``reward`` hook returns credits theacting seat; a mapping of seat to number credits each seat named, as in azero-sum game.plural.Step(task: 'StepTask', turn: 'int', app: 'str', action: 'str', arguments: 'Mapping[str, Any]', observation: 'Any' = None, error: 'str | None' = None, previous_state: 'Any' = None, current_state: 'Any' = None, apps: 'Mapping[str, Any]' = <factory>, seat: 'str | None' = None, seats: 'tuple[str, ...]' = ()) -> Noneplural.environments.types.Observation
What the agent can observe after an action.
This is the only Environment surface sent to the Agent. Internal State isnever copied here automatically. Subclass this with typed fields. The JSON Schema of the subclass is theobservation contract hosted with the environment. Implement :meth:`render`when the prompt should not be a raw dump of the fields. Attributes: text: Default rendered view. Structured subclasses may ignore this. metadata: Extra visible fields that do not need a typed attribute.plural.environments.types.Observation.render
render(self) -> 'str'Return the string the policy sees in the conversation.
Returns: ``text``, or a subclass-specific prompt string.plural.environments.types.State
Persistent internal Environment state for the episode.
State is never sent to the Agent. Only :class:`Observation` is visible.Verifiers receive the final State on :class:`~plural.verifiers.Episode`. Mark a field with :func:`initial` when a Task should supply it. Unmarkedfields are episode progress and cannot be set as ``initial_state``. Attributes: seed: Optional deterministic seed a Task may set. metadata: Extra internal fields that do not need a typed attribute.plural.environments.types.as_text
Render an observation or fallback value as conversation text.
Args: value: Observation, string, or dumpable object. Returns: Text for a user message.plural.environments.types.as_text(value: 'Any') -> 'str'plural.environments.types.initial
Mark a State field as a value a Task may set before the episode starts.
Episode progress, such as whether the case is solved or which guesses havebeen made, stays a normal field. The Environment owns those values.``State.seed`` is settable. ``State.metadata`` is not. Constraints are requirements on the value a Task saves. For example,``min_length=5``, ``max_length=5``, and ``pattern=r"^[a-z]{5}$"`` requirea five-letter word. They are stored on the field's JSON Schema. Args: default: Value used when the Task leaves the field blank. default_factory: Factory used when the default is mutable. description: Shown next to the field when a Task sets initial state. **constraints: Limits such as ``min_length``, ``max_length``, ``pattern``, ``minimum``, ``maximum``, or ``enum``. Returns: A Pydantic field whose JSON Schema includes ``x-plural-initial``.plural.environments.types.initial(default: 'Any' = Ellipsis, *, default_factory: 'Any' = None, description: 'str | None' = None, **constraints: 'Any') -> 'Any'plural.environments.types.is_empty_observation
Return whether `value` has nothing for the policy to read.
Args: value: Observation or fallback payload. Returns: ``True`` when there is no rendered text.plural.environments.types.is_empty_observation(value: 'Any') -> 'bool'plural.environments.types.serialize_observation
Dump an observation onto a decision (dict if it is a model).
Args: value: Observation or other payload. Returns: A JSON-ready value for ``Turn.observation``.plural.environments.types.serialize_observation(value: 'Any') -> 'Any'plural.Harness
Base class for an Agent interaction loop.
Subclasses implement :meth:`run`. Plural discovers and hashes the Pythonclass directory, invokes ``run`` in the Environment Runtime, and emits thestandard result, trajectory, and log artifacts.plural.Harness.model_post_init
model_post_init(self, context: 'Any', /) -> 'None'Bind the subclass source immediately after validation.
plural.Harness.run
run(self, task: 'HarnessTask', agent: 'HarnessAgent', environment: 'HarnessEnvironment') -> 'HarnessResult | str | dict[str, Any] | Any'Run one Agent against one Task and Environment.
plural.external.config.AgentConfig
The whole of `plural-agent.yaml`.
plural.external.config.AgentConfig.load
load(path: 'str | Path | None' = None, *, environ: 'Mapping[str, str] | None' = None) -> 'AgentConfig'Read the config, then apply `PLURAL_TRACE_*` overrides.
Args: path: The file. Defaults to ``PLURAL_AGENT_CONFIG``, then ``plural-agent.yaml`` in the working directory. environ: Environment to read; defaults to ``os.environ``. Returns: The config. Raises: ConfigurationError: When the file is missing or invalid.plural.external.config.AgentConfig.with_environment
with_environment(self, environ: 'Mapping[str, str]') -> 'AgentConfig'Apply `PLURAL_TRACE_* and PLURAL_AGENT_VERSION` overrides.
Returns: A copy with the overrides applied. Raises: ConfigurationError: When an override has the wrong form.plural.external.config.AgentConfig.studio
studio(self, environ: 'Mapping[str, str] | None' = None) -> 'Studio'The Plural API, with the key in `plural.api_key`.
Returns: The client. Raises: ConfigurationError: When that environment variable is not set.plural.external.config.Capture
Which optional fields a trace holds. Turn off what must never leave the service.
plural.external.client.Episode
One episode, as the agent works it.
Every call goes to the Job that started it, with the episode's own token.Pass ``verify`` (a CA bundle path, or ``False`` for a self-signed testcertificate) when the Job serves HTTPS with a private certificate.plural.external.client.Episode(start: 'EpisodeStart', *, verify: 'str | bool' = True, timeout: 'float' = 120.0) -> 'None'plural.external.client.Episode.tools
tools(self) -> 'list[dict[str, Any]]'Describe the actions for a model.
Returns: The actions as OpenAI-compatible tools.plural.external.client.Episode.act
act(self, action: 'str', /, **arguments: 'Any') -> 'Any'Take one action.
Returns: Its observation.plural.external.client.Episode.read
read(self, path: 'str') -> 'bytes'Read one of :attr:files.
Returns: Its bytes.plural.external.client.Episode.trace
trace(self, trajectory: 'Mapping[str, Any]') -> 'None'Report the agent's own trace, as an ATIF document.
The Trial keeps it as self-reported, next to the actions the Job saw.plural.external.client.Episode.finish
finish(self, response: 'Any') -> 'None'End the episode with the agent's final answer. It can be sent once.
plural.external.protocol.EpisodeAction
One action the agent may take, as a tool.
plural.external.client.EpisodeError
The Job refused a request, such as an action after the episode ended.
plural.external.client.EpisodeErrorplural.external.protocol.EpisodeStart
The signed request that starts one episode.
plural.external.protocol.EpisodeTask
What the agent is asked to do.
plural.external.config.FlushPolicy
When a run's trace is written.
``end`` writes it once, when the run finishes. ``interval`` also writes itevery ``interval_seconds`` while the run is going, and ``steps`` every``every_steps`` steps, so a long run can be watched live. Each writereplaces the last until the run finishes.plural.external.config.FlushPolicy.parse
parse(value: 'FlushPolicy | str | Mapping[str, Any]') -> 'FlushPolicy'Read a policy from `end, interval:5, steps:10`, or a mapping.
Returns: The policy. Raises: ConfigurationError: When the value names no policy.plural.external.client.Ping
A signed check, from `plural agent test`, that the agent is reachable.
plural.external.tracing.Run
One traced run of the agent. Use it as a context manager.
Record what happens with :meth:`user`, :meth:`llm`, :meth:`tool`, and:meth:`message`, then :meth:`finish` with the answer. Leaving the ``with``block finishes the run if you did not, as failed when it raised.plural.external.tracing.Run(tracer: 'Tracer', sink: '_Sink | None', *, trace_id: 'str', kind: "Literal['episode', 'production']", flush: 'FlushPolicy', capture: 'Capture', tags: 'Mapping[str, str] | None' = None, metadata: 'Mapping[str, Any] | None' = None, episode: 'Episode | None' = None) -> 'None'plural.external.tracing.Run.system
system(self, text: 'str') -> 'None'Record the system prompt.
plural.external.tracing.Run.user
user(self, text: 'str') -> 'None'Record a message to the agent, such as the request it is working on.
plural.external.tracing.Run.message
message(self, text: 'str') -> 'None'Record something the agent said without a model call.
plural.external.tracing.Run.llm
llm(self, *, model: 'str | None' = None, messages: 'Sequence[Mapping[str, Any]]' = (), output: 'str | None' = None, tool_calls: 'Sequence[ToolCall | Mapping[str, Any]]' = (), reasoning: 'str | None' = None, usage: 'Mapping[str, Any] | None' = None, latency_ms: 'float | None' = None, error: 'str | None' = None, started_at: 'datetime | None' = None) -> 'None'Record one model call.
Args: model: The model that answered. messages: What it was sent, in the OpenAI chat format. output: The text it returned. tool_calls: The tool calls it asked for. reasoning: Its reasoning, when it reported any. usage: ``prompt_tokens``, ``completion_tokens``, ``cached_tokens``, and ``cost_usd``, any of which may be missing. latency_ms: How long the call took. error: Why it failed, when it did. started_at: When it started; defaults to now.plural.external.tracing.Run.tool
tool(self, name: 'str', arguments: 'Mapping[str, Any] | None' = None, result: 'Any' = None, *, call_id: 'str | None' = None, error: 'str | None' = None, latency_ms: 'float | None' = None, started_at: 'datetime | None' = None) -> 'None'Record one tool call and what it returned.
``call_id`` ties it to the model call that asked for it; without one itis matched to the latest unanswered call of the same tool.plural.external.tracing.Run.metadata
metadata(self, **values: 'Any') -> 'None'Attach values to the trace, such as a customer or request id.
plural.external.tracing.Run.finish
finish(self, answer: 'Any' = None) -> 'None'End the run with the agent's answer, and write its trace.
In a Job this also ends the episode, so call it once, last.plural.external.tracing.Run.fail
fail(self, error: 'str') -> 'None'End the run as failed. In a Job the episode ends with no answer.
plural.external.tracing.Run.snapshot
snapshot(self) -> 'tuple[list[_Record], dict[str, Any]]'Read the run so far.
Returns: Its records in time order, and its metadata.plural.external.tracing.Run.atif
atif(self) -> 'dict[str, Any]'Render the trace as a Job keeps it.
Returns: The ATIF document.plural.external.tracing.Run.trace
trace(self) -> 'dict[str, Any]'Render the trace as the Traces page keeps it.
Returns: The Plural ``Trace`` payload.plural.external.protocol.SignatureError
A request whose signature, timestamp, or id does not check out.
plural.external.protocol.SignatureErrorplural.external.tracing.ToolCall
A tool call a model asked for.
plural.external.tracing.ToolCall(id: 'str', name: 'str', arguments: 'Mapping[str, Any]' = <factory>) -> Noneplural.external.tracing.Tracer
Traces an agent's runs, as `plural-agent.yaml` says.
Args: config: The agent and tracing config. environ: Where the API key and overrides are read; defaults to ``os.environ``. studio: The Plural API client, for tests. Built from the API key when omitted. Raises: ConfigurationError: When production tracing is on and the API key's environment variable is not set.plural.external.tracing.Tracer(config: 'AgentConfig', *, environ: 'Mapping[str, str] | None' = None, studio: 'Any' = None) -> 'None'plural.external.tracing.Tracer.from_config
from_config(path: 'str | None' = None, *, environ: 'Mapping[str, str] | None' = None) -> 'Tracer'A tracer for `plural-agent.yaml, or the file PLURAL_AGENT_CONFIG` names.
Returns: The tracer.plural.external.tracing.Tracer.for_agent
for_agent(name: 'str', version: 'str', *, model: 'str' = '', environ: 'Mapping[str, str] | None' = None) -> 'Tracer'A tracer with default settings and no config file.
Returns: The tracer.plural.external.tracing.Tracer.run
run(self, *, input: 'str | None' = None, trace_id: 'str | None' = None, flush: 'FlushPolicy | str | Mapping[str, Any] | None' = None, capture: 'Capture | Mapping[str, bool] | None' = None, tags: 'Mapping[str, str] | None' = None, metadata: 'Mapping[str, Any] | None' = None) -> 'Run'Start a run. Inside a traced episode it is that episode's run.
Args: input: The request the agent is working on, recorded as a user message. trace_id: The trace's id; a new one when omitted. flush: When to write the trace, overriding the config for this run. capture: Which optional fields to keep, overriding the config. tags: Labels for this run, added to the config's. metadata: Values to attach to the trace. Returns: The run; use it as a context manager.plural.external.tracing.Tracer.episode
episode(self, episode: 'Episode', *, flush: 'FlushPolicy | str | Mapping[str, Any] | None' = None, capture: 'Capture | Mapping[str, bool] | None' = None) -> 'Run'The run for one episode of a Plural Job. Its trace goes to that Job.
The Task's instructions are recorded as the first user message, and:meth:`Run.finish` also finishes the episode. Returns: The run; use it as a context manager.plural.external.tracing.Tracer.flush
flush(self, timeout: 'float' = 10.0) -> 'bool'Wait until every pending trace is written.
Returns: Whether everything was written within ``timeout`` seconds.plural.external.tracing.current_run
The run being traced in this context.
Returns: The run, or ``None`` outside one.plural.external.tracing.current_run() -> 'Run | None'plural.external.protocol.new_signing_key
Make a fresh signing key.
Returns: 32 random bytes, as ``whsec_`` and base64.plural.external.protocol.new_signing_key() -> 'str'plural.external.client.ping
Send an agent service a signed ping, as a Job checks it before an episode.
Args: url: The service's episode URL. key: The signing key. ca_bundle: A PEM CA file for a private certificate. timeout: Seconds to wait. Returns: The status the service answered with. Raises: httpx.HTTPError: When the service could not be reached.plural.external.client.ping(url: 'str', key: 'str', *, ca_bundle: 'str | None' = None, timeout: 'float' = 15) -> 'int'plural.external.tracing.set_tracer
Use `value` wherever no tracer is passed, such as in adapters.
plural.external.tracing.set_tracer(value: 'Tracer | None') -> 'None'plural.external.protocol.sign
Standard Webhooks headers for `body`.
Returns: ``webhook-id``, ``webhook-timestamp``, and ``webhook-signature``.plural.external.protocol.sign(key: 'bytes', body: 'bytes', *, message_id: 'str', timestamp: 'int | None' = None) -> 'dict[str, str]'plural.external.protocol.signing_key
The key bytes of a `whsec_`-prefixed base64 secret, or of a raw one.
Returns: The bytes to sign with. Raises: ValueError: When the key is shorter than 32 bytes.plural.external.protocol.signing_key(value: 'str') -> 'bytes'plural.external.tracing.tracer
The process's tracer: the one :func:set_tracer gave, or one for `plural-agent.yaml`.
Returns: The tracer.plural.external.tracing.tracer() -> 'Tracer'plural.external.protocol.verify
Check a signed request and remember its id.
Returns: The request's ``webhook-id``. Raises: SignatureError: When a header is missing, the timestamp is more than five minutes off, the id was already seen, or no signature matches.plural.external.protocol.verify(key: 'bytes', body: 'bytes', headers: 'Mapping[str, str]', *, now: 'float | None' = None, seen: '_Seen | None' = <configured default>) -> 'str'plural.external.client.verify_request
Check that a request came from your Plural Job, and parse it.
Args: body: The request's raw bytes, before any JSON parsing. headers: Its headers. key: The shared signing key. Defaults to ``PLURAL_SIGNING_KEY``. Returns: The episode to start, or a ping to answer with any 2xx. Raises: SignatureError: When the request is not signed with the key, is more than five minutes old, was received before, or is not one Plural sends.plural.external.client.verify_request(body: 'bytes', headers: 'Mapping[str, str]', *, key: 'str | None' = None) -> 'EpisodeStart | Ping'plural.sandbox.SandboxProvider
Async lifecycle provider, separate from environment tool runtimes.
plural.sandbox.SandboxProvider()plural.sandbox.SandboxProvider.capabilities
capabilities(self) -> 'ProviderCapabilities'Declare controls available in the current installation.
plural.sandbox.SandboxProvider.preflight
preflight(self, requirements: 'SandboxRequirements') -> 'EffectiveSandboxPolicy'Fail before launch if any requested control is unavailable.
plural.sandbox.SandboxProvider.doctor
doctor(self) -> 'ProviderDoctor'Return dependency, daemon, or credential health.
plural.sandbox.SandboxProvider.create
create(self, requirements: 'SandboxRequirements') -> 'SandboxHandle'Create a fresh sandbox after successful preflight.
plural.sandbox.SandboxProvider.upload_files
upload_files(self, handle: 'SandboxHandle', files: 'Sequence[FileUpload]', *, root: 'str' = '/workspace') -> 'None'Upload validated files beneath a scoped workspace.
plural.sandbox.SandboxProvider.upload_bundle
upload_bundle(self, handle: 'SandboxHandle', bundle: 'Path', *, root: 'str' = '/workspace') -> 'None'Upload a local directory without following symlinks.
plural.sandbox.SandboxProvider.exec
exec(self, handle: 'SandboxHandle', request: 'ExecRequest') -> 'ExecResult'Execute argv with cwd, env, timeout, stdin, and captured logs.
plural.sandbox.SandboxProvider.download_files
download_files(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace') -> 'tuple[DownloadedFile, ...]'Download exact declared files.
plural.sandbox.SandboxProvider.download_artifacts
download_artifacts(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace', user: 'str | None' = None) -> 'tuple[DownloadedFile, ...]'Download files a Harness declared, which `user` wrote, as regular files only.
plural.sandbox.SandboxProvider.stop_user_processes
stop_user_processes(self, handle: 'SandboxHandle', users: 'Sequence[str | None]') -> 'None'Kill every process the Harnesses' users still run, once their run has ended.
``None`` names the sandbox's default user. A provider that cannot dothis safely does nothing; the App host refuses actions after the Job's``finish`` either way.plural.sandbox.SandboxProvider.isolate_apps
isolate_apps(self, handle: 'SandboxHandle') -> 'AppIsolation'Prepare a separate OS user for the App host, when this sandbox can.
Returns: The users to run the App host and the Harness as; no ``app_user`` when the sandbox cannot separate them.plural.sandbox.SandboxProvider.open_workspace
open_workspace(self, handle: 'SandboxHandle', isolation: 'AppIsolation', paths: 'Sequence[str]') -> 'None'Let `isolation.agent_user` write the Harness's folders, once staging is done.
plural.sandbox.SandboxProvider.isolate_seats
isolate_seats(self, handle: 'SandboxHandle', seats: 'Sequence[str]') -> 'dict[str, str]'Prepare one OS user for each seat's Harness, when this sandbox can.
Each must differ from the App host's user and from every other seat's,and must not be able to become another user, so one seat's Harnesscannot read another's files or process. Returns: Seat to user, or empty when the sandbox cannot separate them.plural.sandbox.SandboxProvider.cancel
cancel(self, handle: 'SandboxHandle') -> 'None'Force cancellation of active work.
plural.sandbox.SandboxProvider.destroy
destroy(self, handle: 'SandboxHandle') -> 'None'Idempotently delete the sandbox and scoped data.
plural.sandbox.SandboxRequirements
Controls required for one sandbox before it may launch.
plural.sandbox.SandboxRequirements.required_capabilities
required_capabilities(self) -> 'frozenset[SandboxCapability]'Return the controls that must be enforceable.
plural.sandbox.ProviderRegistry
Resolve built-ins and installed provider plugins lazily.
plural.sandbox.ProviderRegistry() -> 'None'plural.sandbox.ProviderRegistry.register
register(self, provider: 'SandboxProvider', *, replace: 'bool' = False) -> 'None'Register one concrete provider.
plural.sandbox.ProviderRegistry.get
get(self, name: 'str') -> 'SandboxProvider'Return a registered provider, loading plugins first.
plural.sandbox.ProviderRegistry.doctors
doctors(self, *, include_unavailable: 'bool' = False) -> 'tuple[ProviderDoctor, ...]'Return dynamic provider reports in stable name order.
plural.sandbox.LocalProvider
Run commands in temporary directories without isolation.
This provider intentionally does not advertise network, resource, image, orread-only-root enforcement. It is suitable only for explicitly trusted localdevelopment workloads.plural.sandbox.LocalProvider(*, base_dir: 'Path | None' = None) -> 'None'plural.sandbox.LocalProvider.capabilities
capabilities(self) -> 'ProviderCapabilities'Declare local subprocess controls.
plural.sandbox.LocalProvider.doctor
doctor(self) -> 'ProviderDoctor'Report local execution availability without implying isolation.
plural.sandbox.LocalProvider.create
create(self, requirements: 'SandboxRequirements') -> 'SandboxHandle'Create a scoped temporary workspace.
plural.sandbox.LocalProvider.upload_files
upload_files(self, handle: 'SandboxHandle', files: 'Sequence[FileUpload]', *, root: 'str' = '/workspace') -> 'None'Copy files into the temporary workspace.
plural.sandbox.LocalProvider.exec
exec(self, handle: 'SandboxHandle', request: 'ExecRequest') -> 'ExecResult'Run an argv-only subprocess and kill it at timeout.
plural.sandbox.LocalProvider.download_files
download_files(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace') -> 'tuple[DownloadedFile, ...]'Read exact declared regular files.
plural.sandbox.LocalProvider.cancel
cancel(self, handle: 'SandboxHandle') -> 'None'Kill the active subprocess, if any.
plural.sandbox.LocalProvider.destroy
destroy(self, handle: 'SandboxHandle') -> 'None'Kill active work and remove all scoped files.
plural.sandbox.DockerProvider
Create one locked-down Docker container per execution phase.
plural.sandbox.DockerProvider(*, executable: 'str' = 'docker', default_image: 'str' = 'python:3.12-slim') -> 'None'plural.sandbox.DockerProvider.capabilities
capabilities(self) -> 'ProviderCapabilities'Declare Docker controls only when the daemon is reachable.
plural.sandbox.DockerProvider.preflight
preflight(self, requirements: 'SandboxRequirements') -> 'EffectiveSandboxPolicy'Reject provider-specific image forms before launch.
plural.sandbox.DockerProvider.doctor
doctor(self) -> 'ProviderDoctor'Detect the Docker CLI and daemon.
plural.sandbox.DockerProvider.extend_image
extend_image(self, requirements: 'SandboxRequirements', context: 'Path', key: 'str') -> 'SandboxRequirements'Layer a build context onto the requirements' image, once per base and key.
The context's Dockerfile receives the base image as the ``BASE`` buildargument. The result is tagged by base identity and ``key``, so laterTrials and Jobs reuse it instead of building again. Returns: The requirements, pointing at the extended image.plural.sandbox.DockerProvider.create
create(self, requirements: 'SandboxRequirements') -> 'SandboxHandle'Create and start a hardened, scoped container.
plural.sandbox.DockerProvider.upload_files
upload_files(self, handle: 'SandboxHandle', files: 'Sequence[FileUpload]', *, root: 'str' = '/workspace') -> 'None'Stream exact files into the writable workspace mount.
plural.sandbox.DockerProvider.isolate_apps
isolate_apps(self, handle: 'SandboxHandle') -> 'AppIsolation'Run the App host as its own unprivileged user.
The container runs everything else as ``65532``, with every capabilitydropped, so the Harness cannot read the App host's ``0700`` folder orits process. Returns: The App host's user, and the default user for the Harness.plural.sandbox.DockerProvider.isolate_seats
isolate_seats(self, handle: 'SandboxHandle', seats: 'Sequence[str]') -> 'dict[str, str]'Run each seat's Harness as its own unprivileged user.
Every capability is dropped, so no seat's user can read another's``0700`` folder or process, or the App host's. Returns: Seat to a numeric user, ``65400`` for the first.plural.sandbox.DockerProvider.exec
exec(self, handle: 'SandboxHandle', request: 'ExecRequest') -> 'ExecResult'Execute argv and capture logs, enforcing a host-side timeout.
plural.sandbox.DockerProvider.download_files
download_files(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace') -> 'tuple[DownloadedFile, ...]'Stream exact declared paths out and reject non-regular files.
plural.sandbox.DockerProvider.download_artifacts
download_artifacts(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace', user: 'str | None' = None) -> 'tuple[DownloadedFile, ...]'Read a Harness's files as the user that wrote them, which owns its folder.
No capability is kept, so only a seat's own user can enter its ``0700`` folder.plural.sandbox.DockerProvider.stop_user_processes
stop_user_processes(self, handle: 'SandboxHandle', users: 'Sequence[str | None]') -> 'None'Kill what each Harness user left running, as that user, since no capability is kept.
``None`` is the container's default user, whose ``sleep`` keeps it aliveas PID 1 and is spared.plural.sandbox.DockerProvider.cancel
cancel(self, handle: 'SandboxHandle') -> 'None'Kill the container to force all active processes to stop.
plural.sandbox.DockerProvider.destroy
destroy(self, handle: 'SandboxHandle') -> 'None'Force-remove the container idempotently.
plural.sandbox.DaytonaProvider
Execute in Daytona while keeping its SDK an optional import.
plural.sandbox.DaytonaProvider(*, adapter: 'DaytonaClientAdapter | None' = None, environ: 'Mapping[str, str] | None' = None) -> 'None'plural.sandbox.DaytonaProvider.capabilities
capabilities(self) -> 'ProviderCapabilities'Declare controls implemented by the current Daytona SDK.
plural.sandbox.DaytonaProvider.preflight
preflight(self, requirements: 'SandboxRequirements') -> 'EffectiveSandboxPolicy'Reject Daytona controls whose SDK mapping is not enforceable.
plural.sandbox.DaytonaProvider.doctor
doctor(self) -> 'ProviderDoctor'Detect SDK and credential presence without exposing values.
plural.sandbox.DaytonaProvider.create
create(self, requirements: 'SandboxRequirements') -> 'SandboxHandle'Create a Daytona sandbox from image, snapshot, or defaults.
plural.sandbox.DaytonaProvider.upload_files
upload_files(self, handle: 'SandboxHandle', files: 'Sequence[FileUpload]', *, root: 'str' = '/workspace') -> 'None'Upload exact in-memory files.
plural.sandbox.DaytonaProvider.isolate_apps
isolate_apps(self, handle: 'SandboxHandle') -> 'AppIsolation'Create an App host user and an unprivileged Harness user.
Daytona's default user can ``sudo``, so a Harness running as it couldread anything. Here the Harness runs as ``plural-agent``, which cannot,and the App host as ``plural-apps``. Returns: Both users, or no ``app_user`` when the image has no passwordless ``sudo`` or ``useradd``.plural.sandbox.DaytonaProvider.isolate_seats
isolate_seats(self, handle: 'SandboxHandle', seats: 'Sequence[str]') -> 'dict[str, str]'Create one unprivileged user for each seat's Harness.
Returns: Seat to user, or empty when the image has no passwordless ``sudo`` or ``useradd``.plural.sandbox.DaytonaProvider.open_workspace
open_workspace(self, handle: 'SandboxHandle', isolation: 'AppIsolation', paths: 'Sequence[str]') -> 'None'Let the Harness user write the staged folders, which the default user owns.
Raises: SandboxError: When the folders cannot be opened to it.plural.sandbox.DaytonaProvider.exec
exec(self, handle: 'SandboxHandle', request: 'ExecRequest') -> 'ExecResult'Execute through `sandbox.process.exec`.
A ``user`` other than ``root`` runs through ``sudo -n -E -u``, whichfails rather than falling back to the default user.plural.sandbox.DaytonaProvider.download_files
download_files(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace') -> 'tuple[DownloadedFile, ...]'Download exact declared files.
plural.sandbox.DaytonaProvider.download_artifacts
download_artifacts(self, handle: 'SandboxHandle', paths: 'Sequence[str]', *, root: 'str' = '/workspace', user: 'str | None' = None) -> 'tuple[DownloadedFile, ...]'Download Harness-declared files, refusing any that is not a regular file inside `root`.
Daytona's file API reads as the sandbox's default user, which can readthe App host's folder, so a symlink or a path that resolves outside``root`` is refused first, as the Harness's ``user`` when given, andthe resolved file is what is read. Raises: FileNotFoundError: When a declared path is not a regular file inside ``root``.plural.sandbox.DaytonaProvider.stop_user_processes
stop_user_processes(self, handle: 'SandboxHandle', users: 'Sequence[str | None]') -> 'None'Kill, as root, what the Harnesses' own users left running.
The default user is skipped: it is the one Daytona runs its own tools as.plural.sandbox.DaytonaProvider.cancel
cancel(self, handle: 'SandboxHandle') -> 'None'Delete the sandbox to force cancellation.
plural.sandbox.DaytonaProvider.destroy
destroy(self, handle: 'SandboxHandle') -> 'None'Idempotently delete the remote sandbox.
plural.routing.policies.Explicit
Use the request's model (and optional `models` fallbacks) as-is.
Multi-host models expand to US endpoints first, then the lab host. Examples: >>> from plural.catalog import ModelCatalog >>> from plural.types import ChatRequest, Message >>> policy = Explicit() >>> req = ChatRequest( ... model="openai/gpt-5.6-luna", messages=[Message(role="user", content="x")] ... ) >>> policy.select(req, [req.model], ModelCatalog())[0].model 'openai/gpt-5.6-luna'plural.routing.policies.Explicit()plural.routing.policies.Explicit.select
select(self, request: 'ChatRequest', candidates: 'list[str]', catalog: 'ModelCatalog') -> 'list[ModelRoute]'Return routes in candidate order, expanding multi-host models.
Args: request: The chat request. candidates: Candidate model ids. catalog: Model catalog. Returns: Ordered routes.plural.routing.policies.Fallback
Alias for :class:Explicit emphasizing fallback-chain semantics.
plural.routing.policies.Fallback()plural.routing.policies.LeastCost
Prefer cheaper models among candidates after the primary.
The primary model remains first; remaining candidates are sorted byestimated prompt+completion price (using equal token weights). Eachmodel then expands to its host endpoints (US first).plural.routing.policies.LeastCost()plural.routing.policies.LeastCost.select
select(self, request: 'ChatRequest', candidates: 'list[str]', catalog: 'ModelCatalog') -> 'list[ModelRoute]'Return the primary route first, then cheapest fallbacks.
Args: request: The chat request. candidates: Candidate model ids. catalog: Model catalog. Returns: Ordered routes.plural.routing.policies.LowestLatency
Prefer providers historically associated with low latency.
Uses a static heuristic table for v1; a learned policy can replace thislater via the same :class:`RoutingPolicy` protocol.plural.routing.policies.LowestLatency()plural.routing.policies.LowestLatency.select
select(self, request: 'ChatRequest', candidates: 'list[str]', catalog: 'ModelCatalog') -> 'list[ModelRoute]'Return routes ordered by heuristic provider latency rank.
Args: request: The chat request. candidates: Candidate model ids. catalog: Model catalog. Returns: Ordered routes.plural.routing.policies.ModelRoute
A single candidate route selected by a policy.
Attributes: model: Model id in ``author/slug`` form. provider: Host slug that should serve the request. region: Host region. Carried through so billing can charge the region's own rate rather than whichever one happens to be listed first. upstream_id: Model id the host expects, when different from ``model``. priority: Lower values are tried first.plural.routing.policies.RoutingPolicy
Select an ordered list of model routes for a request.
plural.routing.policies.RoutingPolicy(*args, **kwargs)plural.routing.policies.RoutingPolicy.select
select(self, request: 'ChatRequest', candidates: 'list[str]', catalog: 'ModelCatalog') -> 'list[ModelRoute]'Return ordered routes to try.
Args: request: The chat request. candidates: Candidate model ids (primary + fallbacks). catalog: Model catalog used for cost/latency metadata. Returns: Ordered list of :class:`ModelRoute` values.plural.routing.policies.expand_models
Expand each candidate model into its host routes, keeping model order.
Returns: Every host route for the candidates, ordered by model then endpoint priority.plural.routing.policies.expand_models(model_ids: 'list[str]', catalog: 'ModelCatalog') -> 'list[ModelRoute]'plural.routing.policies.routes_for_model
Expand a catalog model into host routes (US hosts first).
Args: model_id: Canonical ``author/slug`` id. catalog: Model catalog. priority_base: Starting priority for this model's endpoints. Returns: Ordered host routes for the model.plural.routing.policies.routes_for_model(model_id: 'str', catalog: 'ModelCatalog', *, priority_base: 'int' = 0) -> 'list[ModelRoute]'plural.environments.export.hf.to_huggingface_records
Convert a dataset to a list of HF-friendly dictionaries.
Args: dataset: Source dataset. Returns: List of flat-ish records suitable for ``datasets.Dataset.from_list``.plural.environments.export.hf.to_huggingface_records(dataset: 'TraceDataset') -> 'list[dict[str, Any]]'plural.environments.export.verifiers.to_verifiers_trace
Convert an plural trace to a simplified verifiers-compatible dict.
Prefers :class:`~plural.tracing.schema.Turn` steps (observation,actions, observations). Falls back to flat ``llm`` / ``action`` steps. Args: trace: Source trace. Returns: A dictionary with ``messages``, ``reward``, ``task``, and ``extra``.plural.environments.export.verifiers.to_verifiers_trace(trace: 'Trace') -> 'dict[str, Any]'plural.ModelCatalog
In-memory model catalog loaded from a bundled JSON snapshot.
Args: path: Optional path to a catalog JSON file. Defaults to the bundled snapshot. Examples: >>> catalog = ModelCatalog() >>> len(catalog.models()) > 0 Trueplural.ModelCatalog(path: 'str | Path | None' = None, *, entries: 'Iterable[ModelSpec | Mapping[str, Any]]' = (), include_bundled: 'bool' = True) -> 'None'plural.ModelCatalog.add
add(self, *entries: 'ModelSpec | Mapping[str, Any]') -> 'ModelCatalog'Add explicit project entries and return this effective catalog.
Project entries replace bundled entries with the same stable model ID.The catalog is intentionally passed explicitly; no process-globalregistration is performed. Returns: This catalog, for convenient construction.plural.ModelCatalog.with_entries
with_entries(self, *entries: 'ModelSpec | Mapping[str, Any]') -> 'ModelCatalog'Return an independent catalog extended with project entries.
Returns: A copy containing the effective bundled and project entries.plural.ModelCatalog.load_bundled
load_bundled(self) -> 'None'Load the package-bundled catalog snapshot.
plural.ModelCatalog.load_path
load_path(self, path: 'Path') -> 'None'Load a catalog from a filesystem path.
Args: path: Path to a JSON catalog file.plural.ModelCatalog.models
models(self) -> 'list[ModelSpec]'Return all model specs.
Returns: A list of :class:`ModelSpec` entries.plural.ModelCatalog.ids
ids(self) -> 'list[str]'Return registered model IDs in catalog order.
plural.ModelCatalog.suggest
suggest(self, model_id: 'str', *, limit: 'int' = 5) -> 'list[str]'Return nearby catalog IDs for an unknown model.
Args: model_id: The ID the caller tried to use. limit: Maximum suggestions. Returns: Close catalog IDs, or a short prefix of the catalog.plural.ModelCatalog.get
get(self, model_id: 'str') -> 'ModelSpec | None'Look up a model by id.
Args: model_id: Model id in ``author/slug`` form. Returns: The matching :class:`ModelSpec`, or ``None``.plural.ModelCatalog.require
require(self, model_id: 'str') -> 'ModelSpec'Look up a model by id or raise.
Args: model_id: Model id in ``author/slug`` form. Returns: The matching :class:`ModelSpec`. Raises: NotFoundError: If the model is not in the catalog.plural.ModelCatalog.refresh_from_openrouter
refresh_from_openrouter(self, url: 'str' = 'https://openrouter.ai/api/v1/models') -> 'int'Refresh the in-memory catalog from OpenRouter's public models API.
Args: url: Models endpoint URL. Returns: Number of models loaded. Note: This performs a network request and is intended for maintainer tooling, not runtime hot paths.plural.ModelCatalog.write_snapshot
write_snapshot(self, path: 'Path') -> 'None'Write the current catalog to a JSON snapshot file.
Args: path: Destination path.plural.catalog.ModelSpec
A catalog entry for a model.
Attributes: id: Model id in ``author/slug`` form. name: Human-readable display name. context_length: Maximum context window in tokens. pricing: Default per-token pricing (US-first host, then first endpoint). architecture: Modality metadata. supported_parameters: Request parameters the model accepts. endpoints: Inference hosts. Empty means the author is the only host. description: What the model is for, shown on its model card. created: Release date, ``YYYY-MM-DD``. knowledge_cutoff: Training data cutoff, ``YYYY-MM`` or ``YYYY-MM-DD``. max_output_tokens: Largest completion the model produces. open_weights: Whether the weights are published. license: License of published weights, such as ``apache-2.0``. links: Named reference URLs, such as ``homepage`` or ``model_card``. provider: Derived provider slug (author segment of ``id``).plural.catalog.ModelSpec.ordered_endpoints
ordered_endpoints(self) -> 'list[ModelEndpoint]'Return endpoints with US hosts first (Fireworks, then Baseten).
Returns: Ordered endpoints, or a single implicit author host when none are listed.plural.catalog.ModelSpec.host_providers
host_providers(self) -> 'list[str]'Return host slugs that can serve this model.
plural.catalog.ModelSpec.pricing_for
pricing_for(self, provider: 'str | None' = None, region: 'str | None' = None) -> 'ModelPricing | None'Return pricing for a host, or the default US-first price.
Region matters because the same provider charges different rates indifferent regions: Azure EU lists above Azure US for identical models.When a region is given it must match, so a miss falls back to the defaultrather than billing a cheaper region's rate for a pricier one. Args: provider: Host slug. ``None`` uses the default endpoint. region: Host region. ``None`` matches the first endpoint for the provider. Returns: Per-token pricing, or ``None``.plural.catalog.estimate_cost
Estimate USD cost for a usage record against a model spec.
Args: usage: Token usage. spec: Model catalog entry, or ``None``. provider: Optional host slug; bills at that host's pass-through price. region: Optional host region, for providers whose rates vary by region. Returns: Estimated USD cost, or ``None`` if pricing is unavailable. Examples: >>> from plural.types import Usage >>> spec = ModelSpec( ... id="openai/gpt-5.6-luna", ... pricing=ModelPricing(prompt=0.0000002, completion=0.0000012), ... ) >>> round(estimate_cost(Usage.from_counts(1_000_000, 0), spec) or 0, 2) 0.2plural.catalog.estimate_cost(usage: 'Usage', spec: 'ModelSpec | None', *, provider: 'str | None' = None, region: 'str | None' = None) -> 'float | None'plural.catalog.sync.CatalogDiff
What the sync found relative to the bundled catalog.
Attributes: candidates: Upstream models we could carry but do not. Reported only; the catalog is curated, so adding one is an explicit choice. removed: Catalog ids the upstream reference no longer lists. repriced: Rates that moved upstream. unpriced: Catalog ids with no usable price, which stay hidden. unconfirmed: Catalog ids no configured provider currently lists. providers_checked: Provider slugs whose listings were fetched. discounted: Model ids where upstream is running a promotion. Recorded so a reviewer can see we deliberately kept the list price. retiered: Prompt-length rates that differ from the catalog's. new_hosts: Hosts upstream offers for models we carry but we do not list.plural.catalog.sync.CatalogDiff(candidates: 'list[UpstreamModel]' = <factory>, removed: 'list[str]' = <factory>, repriced: 'list[PriceChange]' = <factory>, unpriced: 'list[str]' = <factory>, unconfirmed: 'list[str]' = <factory>, providers_checked: 'list[str]' = <factory>, discounted: 'list[str]' = <factory>, retiered: 'list[TierChange]' = <factory>, new_hosts: 'list[tuple[str, str]]' = <factory>) -> Noneplural.catalog.sync.PriceChange
A per-token price that moved upstream.
Attributes: id: Model id. field_name: Which rate changed (``prompt`` or ``completion``). old: Price currently in the catalog. new: Undiscounted list price reported upstream. provider: Host the rate belongs to, or ``None`` for the model default. region: Host region, or ``None`` for the model default.plural.catalog.sync.PriceChange(id: 'str', field_name: 'str', old: 'float | None', new: 'float | None', provider: 'str | None' = None, region: 'str | None' = None) -> Noneplural.catalog.sync.TierChange
Prompt-length rates that differ from what the catalog records.
Attributes: id: Model id. tiers: Tiers reported upstream, at list prices. provider: Host the tiers belong to, or ``None`` for the model default. region: Host region, or ``None`` for the model default.plural.catalog.sync.TierChange(id: 'str', tiers: 'tuple[UpstreamTier, ...]', provider: 'str | None' = None, region: 'str | None' = None) -> Noneplural.catalog.sync.TierChange.describe
describe(self) -> 'str'Summarize the thresholds and rates for a review body.
Returns: A human-readable description, or a note that tiers were dropped.plural.catalog.sync.UpstreamEndpoint
A single inference host as OpenRouter describes it.
Attributes: provider: Catalog provider slug, or ``None`` when the tag is unmapped. region: Coarse region key. tier: Service tier. Only ``standard`` is used for catalog pricing. upstream_id: Model id the host expects. prompt: Undiscounted USD per prompt token. completion: Undiscounted USD per completion token. discount: Promotional fraction OpenRouter applied, if any. tiers: Prompt-length rates, ordered by threshold. tag: Raw upstream tag, kept for reporting unmapped hosts. cache_read: Undiscounted USD per prompt token read from the cache. cache_write: Undiscounted USD per prompt token written to the cache.plural.catalog.sync.UpstreamEndpoint(provider: 'str | None', region: 'str', tier: 'str', upstream_id: 'str', prompt: 'float | None', completion: 'float | None', discount: 'float | None', tiers: 'tuple[UpstreamTier, ...]', tag: 'str', cache_read: 'float | None' = None, cache_write: 'float | None' = None) -> Noneplural.catalog.sync.UpstreamModel
A model as described by the upstream reference catalog.
Attributes: id: Canonical ``author/slug`` id. name: Display name. context_length: Context window in tokens. prompt: USD per prompt token, or ``None`` when unknown. completion: USD per completion token, or ``None`` when unknown. modality: High-level modality string. input_modalities: Accepted input modalities. output_modalities: Produced output modalities. supported_parameters: Request parameters the model accepts. description: Upstream description, a starting point for the model card. created: Upstream release date, ``YYYY-MM-DD``. max_output_tokens: Largest completion upstream reports.plural.catalog.sync.UpstreamModel(id: 'str', name: 'str | None' = None, context_length: 'int | None' = None, prompt: 'float | None' = None, completion: 'float | None' = None, modality: 'str | None' = None, input_modalities: 'tuple[str, ...]' = ('text',), output_modalities: 'tuple[str, ...]' = ('text',), supported_parameters: 'tuple[str, ...]' = (), description: 'str | None' = None, created: 'str | None' = None, max_output_tokens: 'int | None' = None) -> Noneplural.catalog.sync.UpstreamTier
A long-prompt rate that supersedes the base rate.
Attributes: min_prompt_tokens: Prompt size at which the rate takes over. prompt: Undiscounted USD per prompt token. completion: Undiscounted USD per completion token.plural.catalog.sync.UpstreamTier(min_prompt_tokens: 'int', prompt: 'float', completion: 'float') -> Noneplural.catalog.sync.apply_diff
Produce an updated catalog document.
Price drift is applied automatically because a stale rate bills the wrongamount on every request. Additions and removals are deliberate: the catalogis curated, and dropping a model breaks callers. Args: current: Decoded ``models.json``. upstream: Upstream models keyed by id. changes: Differences from :func:`diff_catalog`. add: Model ids to bring into the catalog. add_hosts: Provider slugs whose regional endpoints should be added to every carried model that upstream offers them for. endpoints_by_model: Per-model host records, required by ``add_hosts``. now: Timestamp to stamp the document with. Returns: A new catalog document ready to write.plural.catalog.sync.apply_diff(current: 'Mapping[str, Any]', upstream: 'Mapping[str, UpstreamModel]', changes: 'CatalogDiff', *, add: 'Iterable[str]' = (), add_hosts: 'Iterable[str]' = (), endpoints_by_model: 'Mapping[str, list[UpstreamEndpoint]] | None' = None, now: 'datetime | None' = None) -> 'dict[str, Any]'plural.catalog.sync.collect_served_slugs
Gather the slugs every configured provider reports.
Args: client: HTTP client to use. env: Environment mapping to read keys from. Defaults to ``os.environ``. Returns: A pair of (normalized slugs, provider slugs that answered).plural.catalog.sync.collect_served_slugs(client: 'httpx.Client', env: 'Mapping[str, str] | None' = None) -> 'tuple[set[str], list[str]]'plural.catalog.sync.diff_catalog
Compare the bundled catalog against upstream.
Args: current: Decoded ``models.json``. upstream: Upstream models keyed by id. served: Normalized slugs configured providers report. Empty means availability could not be confirmed, so nothing is filtered on it. providers_checked: Provider slugs that answered. endpoints_by_model: Per-model host records. Models absent from this mapping are skipped for repricing rather than assumed unchanged, so a failed lookup never looks like a price drop. Returns: The differences a human needs to review.plural.catalog.sync.diff_catalog(current: 'Mapping[str, Any]', upstream: 'Mapping[str, UpstreamModel]', served: 'set[str]', *, providers_checked: 'Iterable[str]' = (), endpoints_by_model: 'Mapping[str, list[UpstreamEndpoint]] | None' = None) -> 'CatalogDiff'plural.catalog.sync.fetch_endpoints
Fetch the hosts OpenRouter lists for one model.
Args: client: HTTP client to use. model_id: Canonical ``author/slug`` id. Returns: Host records, empty when the model is unknown upstream.plural.catalog.sync.fetch_endpoints(client: 'httpx.Client', model_id: 'str') -> 'list[UpstreamEndpoint]'plural.catalog.sync.fetch_openrouter
Fetch the public OpenRouter catalog.
Args: client: HTTP client to use. Returns: Upstream models keyed by canonical id.plural.catalog.sync.fetch_openrouter(client: 'httpx.Client') -> 'dict[str, UpstreamModel]'plural.catalog.sync.fetch_provider_models
List the model slugs a provider currently serves.
Args: client: HTTP client to use. slug: Provider slug. api_key: Provider API key. Returns: Normalized slugs the provider reports, empty when the call fails.plural.catalog.sync.fetch_provider_models(client: 'httpx.Client', slug: 'str', api_key: 'str') -> 'set[str]'plural.catalog.sync.is_region
Decide whether an endpoint tag qualifier names a region.
Args: qualifier: A segment after the provider in an endpoint tag. Returns: ``True`` when the segment looks like a region rather than a variant. Examples: >>> is_region("us-east-1"), is_region("global"), is_region("fp8") (True, True, False)plural.catalog.sync.is_region(qualifier: 'str') -> 'bool'plural.catalog.sync.load_catalog
Read the bundled catalog document.
Args: path: Location of ``models.json``. Returns: The decoded document.plural.catalog.sync.load_catalog(path: 'Path' = PosixPath('<default>')) -> 'dict[str, Any]'plural.catalog.sync.main
Run the catalog sync.
Args: argv: Command-line arguments. Returns: Process exit code. ``1`` under ``--check`` when the catalog is stale.plural.catalog.sync.main(argv: 'list[str] | None' = None) -> 'int'plural.catalog.sync.normalize_model_id
Reduce a vendor model id to a comparable slug.
Vendors name the same weights differently: Fireworks serves``accounts/fireworks/models/llama-v3p1-70b`` where OpenRouter says``meta/llama-3.1-70b``. Comparing bare trailing slugs catches most pairs. Args: model_id: Provider or catalog model id. Returns: A lowercase slug with author and account path segments removed. Examples: >>> normalize_model_id("accounts/fireworks/models/Llama-3.1-70B") 'llama-3.1-70b' >>> normalize_model_id("openai/gpt-4o-mini") 'gpt-4o-mini'plural.catalog.sync.normalize_model_id(model_id: 'str') -> 'str'plural.catalog.sync.order_spec_keys
Put catalog entry keys in a stable order.
A price added to an entry that had none would otherwise land at the end,which makes the review diff harder to read than it needs to be. Args: spec: A single catalog entry. Returns: The same entry with known keys first, in canonical order. Examples: >>> list(order_spec_keys({"endpoints": [], "id": "a/b"})) ['id', 'endpoints']plural.catalog.sync.order_spec_keys(spec: 'Mapping[str, Any]') -> 'dict[str, Any]'plural.catalog.sync.parse_endpoint_tag
Split an OpenRouter endpoint tag into provider, region, and service tier.
A qualifier that is neither a known service tier nor region-shaped is adeployment variant: quantizations such as ``fp8`` and ``fp4``, or programnames such as ``claude-on-aws``. Those are returned as the tier so they areexcluded from pricing, since a 4-bit deployment is not the same product asthe full-precision model and must not set its rate. Args: tag: Endpoint tag such as ``azure/eu`` or ``openai/flex``. Returns: A triple of (provider slug or ``None`` when unmapped, region, tier). Examples: >>> parse_endpoint_tag("amazon-bedrock/us-east-1") ('bedrock', 'us', 'standard') >>> parse_endpoint_tag("openai/flex") ('openai', 'us', 'flex') >>> parse_endpoint_tag("baseten/fp4") ('baseten', 'us', 'fp4')plural.catalog.sync.parse_endpoint_tag(tag: 'str') -> 'tuple[str | None, str, str]'plural.catalog.sync.parse_endpoints
Convert an OpenRouter `/endpoints` payload into host records.
Args: payload: Decoded ``/api/v1/models/{id}/endpoints`` response. Returns: Every host described upstream, with list rather than promotional prices.plural.catalog.sync.parse_endpoints(payload: 'Mapping[str, Any]') -> 'list[UpstreamEndpoint]'plural.catalog.sync.parse_openrouter
Convert an OpenRouter models payload into upstream records.
Args: payload: Decoded ``/api/v1/models`` response. Returns: Upstream models keyed by canonical id.plural.catalog.sync.parse_openrouter(payload: 'Mapping[str, Any]') -> 'dict[str, UpstreamModel]'plural.catalog.sync.parse_tiers
Read prompt-length rates from an endpoint's pricing block.
A tier is only usable if both rates are present, since billing one side at thelong-prompt rate and the other at the base rate matches neither. Args: pricing: The endpoint's ``pricing`` mapping. discount: Promotional fraction to divide back out. Returns: Tiers ordered by threshold.plural.catalog.sync.parse_tiers(pricing: 'Mapping[str, Any]', discount: 'float | None') -> 'tuple[UpstreamTier, ...]'plural.catalog.sync.render_report
Render the diff as markdown for a pull request body.
Args: changes: Differences from :func:`diff_catalog`. Returns: A markdown summary.plural.catalog.sync.render_report(changes: 'CatalogDiff') -> 'str'plural.catalog.sync.standard_endpoints
Index priced standard-tier hosts by provider and region.
Args: endpoints: Host records for one model. Returns: Mapping of (provider, region) to endpoint, cheapest kept on collision.plural.catalog.sync.standard_endpoints(endpoints: 'Iterable[UpstreamEndpoint]') -> 'dict[tuple[str, str], UpstreamEndpoint]'plural.catalog.sync.write_catalog
Write the catalog document back to disk.
Args: document: Catalog document to serialize. path: Location of ``models.json``.plural.catalog.sync.write_catalog(document: 'Mapping[str, Any]', path: 'Path' = PosixPath('<default>')) -> 'None'plural.catalog.edit.ArchitectureDocument
Modalities, as stored.
plural.catalog.edit.EditError
A command that cannot be applied.
plural.catalog.edit.EditErrorplural.catalog.edit.EndpointDocument
One host that serves the model, at its own price.
plural.catalog.edit.Issue
A validation finding.
plural.catalog.edit.ModelCardDocument
One `models.json` entry: everything a model card shows.
Unknown keys are rejected so a misspelled field fails validation instead ofsilently never reaching the card.plural.catalog.edit.PricingDocument
Per-token prices in USD, as stored. Strings keep exact decimals.
plural.catalog.edit.TierDocument
A prompt-length price tier as stored.
plural.catalog.edit.build_parser
The command-line interface.
Returns: The argument parser.plural.catalog.edit.build_parser() -> 'argparse.ArgumentParser'plural.catalog.edit.complete_sentences
Drop a trailing fragment from a description the upstream list cut short.
Returns: The text ending at its last complete sentence. Examples: >>> complete_sentences("Fast model. Good at code, and...") 'Fast model.' >>> complete_sentences("Fast model.") 'Fast model.'plural.catalog.edit.complete_sentences(text: 'str') -> 'str'plural.catalog.edit.main
Run one command and print its JSON result.
Returns: ``0`` on success, ``1`` when the command failed or validation did.plural.catalog.edit.main(argv: 'list[str] | None' = None) -> 'int'plural.catalog.edit.match_upstream
Find a catalog model in the upstream list, which names some authors differently.
OpenRouter lists ``x-ai/grok-4.3`` where the catalog says ``xai/grok-4.3``, andwrites versions with dots where a lab uses dashes. An exact id wins, then an``alias``, then a unique match on the version-insensitive slug. Returns: The upstream record, or ``None`` when there is no unambiguous match. Examples: >>> match_upstream("xai/grok-4.3", {"x-ai/grok-4.3": 1}) 1 >>> match_upstream("anthropic/claude-haiku-4-5", {"anthropic/claude-haiku-4.5": 2}) 2 >>> match_upstream("a/x", {"b/x": 1, "c/x": 2}) is None Trueplural.catalog.edit.match_upstream(model_id: 'str', upstream: 'Mapping[str, Any]', alias: 'str | None' = None) -> 'Any'plural.catalog.edit.parse_rate
Parse a price written per token or per million tokens.
Args: raw: ``0.000003``, ``3e-6``, or ``3/M``. Returns: The per-token price as a plain decimal string. Raises: ValueError: If the value is not a non-negative number. Examples: >>> parse_rate("3/M") '0.000003' >>> parse_rate("0.0000025") '0.0000025' >>> parse_rate("0.15/m") '0.00000015'plural.catalog.edit.parse_rate(raw: 'str') -> 'str'plural.catalog.edit.parse_value
Decode a `key=value` right-hand side for the field it targets.
Prices accept ``/M``; list fields accept ``a,b,c``; anything else is read asJSON when it parses and as a string otherwise. Returns: The value to store. Examples: >>> parse_value("pricing.prompt", "3/M") '0.000003' >>> parse_value("supported_parameters", "tools,temperature") ['tools', 'temperature'] >>> parse_value("context_length", "200000") 200000 >>> parse_value("description", "A small model") 'A small model'plural.catalog.edit.parse_value(path: 'str', raw: 'str') -> 'Any'plural.catalog.edit.run
Execute one parsed command against the catalog at `args.path`.
Returns: The JSON result to print. Raises: EditError: When the command cannot be applied or would leave the catalog invalid.plural.catalog.edit.run(args: 'argparse.Namespace') -> 'dict[str, Any]'plural.catalog.edit.template
A complete, valid-shaped entry to fill in for a new model.
Returns: An entry with placeholder values for every card field.plural.catalog.edit.template(model_id: 'str') -> 'dict[str, Any]'plural.catalog.edit.validate_document
Validate a whole catalog document.
Args: document: Decoded ``models.json``. Returns: Every issue across every entry.plural.catalog.edit.validate_document(document: 'Mapping[str, Any]') -> 'list[Issue]'plural.catalog.edit.validate_entry
Check one parsed entry beyond its shape.
Args: card: A parsed catalog entry. Returns: Errors, which block a write, and warnings, which ``--strict`` promotes.plural.catalog.edit.validate_entry(card: 'ModelCardDocument') -> 'list[Issue]'plural.tracing.TraceWriter
Background writer that applies redaction/sampling then sinks traces.
Args: sink: Destination sink. redactor: Optional redactor applied before write. sampler: Optional sampler; defaults to keep-all. maxsize: Queue size before ``record`` blocks.plural.tracing.TraceWriter(sink: 'Sink', *, redactor: 'Redactor | None' = None, sampler: 'Sampler | None' = None, maxsize: 'int' = 1000) -> 'None'plural.tracing.TraceWriter.record
record(self, trace: 'Trace') -> 'None'Enqueue a trace for persistence.
Args: trace: Trace to record.plural.tracing.TraceWriter.label
label(self, trace_id: 'str', *, scores: 'dict[str, float] | None' = None, reward: 'float | None' = None, labels: 'dict[str, Any] | None' = None, feedback: 'str | None' = None) -> 'None'Attach a late label to a trace id.
If the trace has not been written yet, the label is merged on record.If a SQLite sink is used and the trace already exists, it is updated. Args: trace_id: Trace id to label. scores: Named scores. reward: Scalar reward. labels: Discrete labels. feedback: Free-form feedback.plural.tracing.TraceWriter.flush
flush(self) -> 'None'Block until the queue is empty.
plural.tracing.TraceWriter.close
close(self) -> 'None'Flush, stop the worker, and close the sink.
plural.tracing.sinks.JSONLSink
Append traces as JSON lines to a file.
Args: path: Destination ``.jsonl`` path.plural.tracing.sinks.JSONLSink(path: 'str | Path') -> 'None'plural.tracing.sinks.JSONLSink.write
write(self, trace: 'Trace') -> 'None'Append one JSON line.
Args: trace: Trace to persist.plural.tracing.sinks.JSONLSink.read_all
read_all(self) -> 'list[Trace]'Read all traces from the file.
Returns: List of traces in file order.plural.tracing.sinks.JSONLSink.close
close(self) -> 'None'Close the file handle.
plural.tracing.sinks.MultiSink
Fan out writes to multiple sinks.
Args: sinks: Child sinks.plural.tracing.sinks.MultiSink(sinks: 'list[Sink]') -> 'None'plural.tracing.sinks.MultiSink.write
write(self, trace: 'Trace') -> 'None'Write to every child sink.
Args: trace: Trace to persist.plural.tracing.sinks.MultiSink.close
close(self) -> 'None'Close every child sink.
plural.tracing.sinks.OTelSink
Export traces as OpenTelemetry GenAI-style spans.
The plural :class:`~plural.tracing.schema.Trace` remains canonical. Thissink is an exporter onto the still-Development ``gen_ai.*`` conventions. Args: tracer_name: Tracer instrumentation name. Note: Requires the ``plural[otel]`` extra.plural.tracing.sinks.OTelSink(tracer_name: 'str' = 'plural') -> 'None'plural.tracing.sinks.OTelSink.write
write(self, trace: 'Trace') -> 'None'Emit one span per LLM step plus a parent span for the trace.
Args: trace: Trace to export.plural.tracing.sinks.OTelSink.close
close(self) -> 'None'No-op; tracer provider lifecycle is owned by the host app.
plural.tracing.sinks.SQLiteSink
Persist traces in a SQLite database for querying.
Args: path: Destination ``.sqlite`` path.plural.tracing.sinks.SQLiteSink(path: 'str | Path') -> 'None'plural.tracing.sinks.SQLiteSink.write
write(self, trace: 'Trace') -> 'None'Upsert a trace by `trace_id`.
Args: trace: Trace to persist.plural.tracing.sinks.SQLiteSink.get
get(self, trace_id: 'str') -> 'Trace | None'Fetch a trace by id.
Args: trace_id: Trace id. Returns: The trace, or ``None``.plural.tracing.sinks.SQLiteSink.query
query(self, *, environment: 'str | None' = None, tag: 'str | None' = None, limit: 'int' = 100) -> 'list[Trace]'Query traces with simple filters.
Args: environment: Optional environment name filter. tag: Optional tag key that must be present. limit: Maximum rows. Returns: Matching traces, newest first.plural.tracing.sinks.SQLiteSink.close
close(self) -> 'None'Close the database connection.
plural.tracing.sinks.Sink
Destination for persisted traces.
plural.tracing.sinks.Sink(*args, **kwargs)plural.tracing.sinks.Sink.write
write(self, trace: 'Trace') -> 'None'Persist a single trace.
plural.tracing.sinks.Sink.close
close(self) -> 'None'Flush and release resources.
plural.tracing.sinks.traces_from_jsonl
Load traces from a JSONL file without opening a writable sink.
Args: path: Path to a JSONL file. Returns: Parsed traces.plural.tracing.sinks.traces_from_jsonl(path: 'str | Path') -> 'list[Trace]'plural.tracing.Redactor
Redact sensitive fields and patterns from traces.
Args: fields: Dot-paths to replace with ``[REDACTED]`` (e.g. ``metadata.email``). patterns: Regex patterns applied to all string values. replacement: Replacement string for matches. callables: Custom callables ``(trace) -> trace`` applied last. drop_content: If ``True``, strip message content from LLM request/response.plural.tracing.Redactor(*, fields: 'set[str] | None' = None, patterns: 'list[str] | None' = None, replacement: 'str' = '[REDACTED]', callables: 'list[Callable[[Trace], Trace]] | None' = None, drop_content: 'bool' = False) -> 'None'plural.tracing.Redactor.apply
apply(self, trace: 'Trace') -> 'Trace'Return a redacted copy of `trace`.
Args: trace: The original trace. Returns: A redacted deep copy.plural.providers.anthropic.AnthropicProvider
Adapter for Anthropic's Messages API.
Args: api_key: Anthropic API key. base_url: API base URL. timeout_s: Request timeout in seconds. default_headers: Extra headers. anthropic_version: Value for the ``anthropic-version`` header.plural.providers.anthropic.AnthropicProvider(api_key: 'str', *, base_url: 'str | None' = None, timeout_s: 'float' = 60.0, default_headers: 'dict[str, str] | None' = None, anthropic_version: 'str' = '2023-06-01') -> 'None'plural.providers.anthropic.AnthropicProvider.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming Messages API call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.anthropic.AnthropicProvider.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Execute a streaming Messages API call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.anthropic.AnthropicProvider.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async non-streaming Messages API call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.anthropic.AnthropicProvider.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async streaming Messages API call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.anthropic.AnthropicProvider.close
close(self) -> 'None'Close the sync HTTP client.
plural.providers.anthropic.AnthropicProvider.aclose
aclose(self) -> 'None'Close the async HTTP client.
plural.providers.azure.AzureOpenAIProvider
Azure OpenAI via the v1 chat completions route.
Args: api_key: Azure API key, or an Entra ID token when ``use_bearer_auth`` is set. endpoint: Resource endpoint, such as ``https://acme.openai.azure.com``. deployments: Maps model ids to deployment names. Both the full ``author/slug`` and the bare slug are accepted as keys. api_version: Optional explicit version, for opting into ``preview``. use_bearer_auth: Send the credential as a bearer token for Entra ID. base_url: Overrides ``endpoint`` entirely. region: Serving region, recorded so billing can charge that region's rate. name: Provider slug. timeout_s: Request timeout in seconds. default_headers: Extra headers. http_transport: Transport for synchronous requests. async_http_transport: Transport for asynchronous requests. Raises: ConfigurationError: If neither ``endpoint`` nor ``base_url`` is given. Examples: >>> provider = AzureOpenAIProvider( ... "key", ... endpoint="https://acme.openai.azure.com", ... deployments={"openai/gpt-5.6-sol": "sol-prod"}, ... ) >>> provider._model_id("openai/gpt-5.6-sol") 'sol-prod' >>> provider.close()plural.providers.azure.AzureOpenAIProvider(api_key: 'str', *, endpoint: 'str | None' = None, deployments: 'dict[str, str] | None' = None, api_version: 'str | None' = None, use_bearer_auth: 'bool' = False, base_url: 'str | None' = None, region: 'str' = 'us', name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.BasetenProvider
Baseten Model APIs (OpenAI-compatible).
plural.providers.openai_compatible.BasetenProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.bedrock.BedrockProvider
Amazon Bedrock via the Converse and ConverseStream APIs.
Args: api_key: Bedrock API key sent as a bearer token. Omit when using IAM. region: AWS region such as ``us-east-1``. Exposed as the coarse catalog region via ``region``, since pricing varies by continent not by zone. models: Maps catalog model ids to Bedrock model ids. Both the full ``author/slug`` and the bare slug are accepted as keys. access_key_id: AWS access key id, to sign with SigV4 instead. secret_access_key: AWS secret access key. session_token: Session token for temporary credentials. base_url: Overrides the regional endpoint. name: Provider slug. timeout_s: Request timeout in seconds. default_headers: Extra headers. Raises: ConfigurationError: If no usable credentials are supplied. Examples: >>> provider = BedrockProvider("key", region="us-east-1") >>> provider.name, provider.aws_region, provider.region ('bedrock', 'us-east-1', 'us') >>> provider.close()plural.providers.bedrock.BedrockProvider(api_key: 'str | None' = None, *, region: 'str' = 'us-east-1', models: 'dict[str, str] | None' = None, access_key_id: 'str | None' = None, secret_access_key: 'str | None' = None, session_token: 'str | None' = None, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None) -> 'None'plural.providers.bedrock.BedrockProvider.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming Converse call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.bedrock.BedrockProvider.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Execute a streaming Converse call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.bedrock.BedrockProvider.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async non-streaming Converse call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.bedrock.BedrockProvider.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async streaming Converse call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.bedrock.BedrockProvider.close
close(self) -> 'None'Close the sync HTTP client.
plural.providers.bedrock.BedrockProvider.aclose
aclose(self) -> 'None'Close the async HTTP client.
plural.providers.openai_compatible.DeepSeekProvider
DeepSeek OpenAI-compatible API.
plural.providers.openai_compatible.DeepSeekProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.FireworksProvider
Fireworks AI OpenAI-compatible API.
plural.providers.openai_compatible.FireworksProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.google.GoogleProvider
Adapter for Google Gemini's generateContent API.
Args: api_key: Google AI Studio API key. base_url: API base URL. timeout_s: Request timeout in seconds. default_headers: Extra headers. include_thoughts: Whether to ask Gemini for its thought summaries. Gemini reasons whether or not they are requested, and withholds them unless asked, which makes a thinking model look like a stalled one.plural.providers.google.GoogleProvider(api_key: 'str', *, base_url: 'str | None' = None, timeout_s: 'float' = 60.0, default_headers: 'dict[str, str] | None' = None, include_thoughts: 'bool' = True) -> 'None'plural.providers.google.GoogleProvider.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming generateContent call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.google.GoogleProvider.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Execute a streaming generateContent call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.google.GoogleProvider.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async non-streaming generateContent call.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.google.GoogleProvider.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async streaming generateContent call.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.google.GoogleProvider.close
close(self) -> 'None'Close the sync HTTP client.
plural.providers.google.GoogleProvider.aclose
aclose(self) -> 'None'Close the async HTTP client.
plural.providers.openai_compatible.GroqProvider
Groq OpenAI-compatible API.
plural.providers.openai_compatible.GroqProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.MetaProvider
Meta Model API (OpenAI-compatible).
plural.providers.openai_compatible.MetaProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.MistralProvider
Mistral OpenAI-compatible API.
plural.providers.openai_compatible.MistralProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.MoonshotProvider
Moonshot / Kimi lab API.
plural.providers.openai_compatible.MoonshotProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.OpenAICompatible
Base adapter for OpenAI Chat Completions-compatible APIs.
Args: api_key: Provider API key. base_url: API base URL. name: Provider slug used in traces and errors. timeout_s: Request timeout in seconds. default_headers: Extra headers. organization: Optional OpenAI organization header. strip_model_prefix: Whether to strip ``author/`` from model ids. http_transport: Transport for synchronous requests, such as one that refuses private addresses. ``None`` uses httpx's default. async_http_transport: Transport for asynchronous requests.plural.providers.openai_compatible.OpenAICompatible(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.OpenAICompatible.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming chat completion.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.openai_compatible.OpenAICompatible.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Execute a streaming chat completion.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.openai_compatible.OpenAICompatible.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async non-streaming chat completion.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.openai_compatible.OpenAICompatible.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async streaming chat completion.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.openai_compatible.OpenAICompatible.close
close(self) -> 'None'Close the sync HTTP client.
plural.providers.openai_compatible.OpenAICompatible.aclose
aclose(self) -> 'None'Close the async HTTP client.
plural.providers.openai_compatible.OpenAIProvider
Official OpenAI API, over whichever of its two endpoints fits the request.
OpenAI serves the same models through two incompatible wire formats, and``/responses`` is now the larger one: ``/chat/completions`` rejects functiontools for every current model and never reports reasoning. So requests go to:class:`~plural.providers.openai_responses.OpenAIResponsesProvider` bydefault. The one thing it cannot do is ``stop`` and ``seed``, which itrejects as unknown, so a request using either and needing nothingResponses-only stays on chat completions instead. Every request thereforelands on the endpoint that can serve all of it. Args: api_key: OpenAI API key. transport: ``"auto"`` picks per request as described above. ``"chat"`` and ``"responses"`` pin one endpoint, which is what to reach for when fronting a proxy that speaks only one of them. **kwargs: Forwarded to :class:`OpenAICompatible`. Examples: >>> from plural.types import Message >>> provider = OpenAIProvider("sk-test") >>> hello = [Message(role="user", content="hi")] >>> provider._delegate(ChatRequest(model="openai/gpt-5.6", messages=hello)) is not None Trueplural.providers.openai_compatible.OpenAIProvider(api_key: 'str', *, transport: "Literal['auto', 'chat', 'responses']" = 'auto', **kwargs: 'Any') -> 'None'plural.providers.openai_compatible.OpenAIProvider.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming chat completion on the fitting endpoint.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.openai_compatible.OpenAIProvider.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async variant of :meth:chat.
Args: request: Normalized chat request. Returns: Normalized chat response.plural.providers.openai_compatible.OpenAIProvider.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Stream a chat completion from the fitting endpoint.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.openai_compatible.OpenAIProvider.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async variant of :meth:stream.
Args: request: Normalized chat request. Yields: Normalized stream chunks.plural.providers.openai_compatible.OpenAIProvider.close
close(self) -> 'None'Close both endpoints' HTTP clients.
plural.providers.openai_compatible.OpenAIProvider.aclose
aclose(self) -> 'None'Close both endpoints' async HTTP clients.
plural.providers.openai_responses.OpenAIResponsesProvider
OpenAI Responses API, normalized to the Chat Completions shape.
Args: api_key: OpenAI API key. reasoning_summaries: Whether to ask for streamed reasoning summaries. On by default, because a thinking model that emits nothing for ten seconds is indistinguishable from a hung connection. store: Whether to let OpenAI retain the response. Off by default to match Chat Completions, which retains nothing. **kwargs: Forwarded to :class:`OpenAICompatible`. Examples: >>> provider = OpenAIResponsesProvider("sk-test") >>> provider.endpoint_path '/responses'plural.providers.openai_responses.OpenAIResponsesProvider(api_key: 'str', *, reasoning_summaries: 'bool' = True, store: 'bool' = False, **kwargs: 'Any') -> 'None'plural.providers.openai_responses.OpenAIResponsesProvider.serves_exactly
serves_exactly(request: 'ChatRequest') -> 'bool'Whether this endpoint can honour every part of a request.
Args: request: Normalized chat request. Returns: ``False`` when something would have to be dropped or adjusted, which is what tells the OpenAI dispatcher to prefer Chat Completions. Examples: >>> from plural.types import Message >>> hello = [Message(role="user", content="hi")] >>> req = ChatRequest(model="openai/gpt-5.6", messages=hello, seed=7) >>> OpenAIResponsesProvider.serves_exactly(req) Falseplural.providers.base.Provider
Protocol implemented by all provider adapters.
Adapters must provide sync and async chat/stream methods that acceptnormalized :class:`~plural.types.ChatRequest` objects and returnnormalized responses.plural.providers.base.Provider(*args, **kwargs)plural.providers.base.Provider.chat
chat(self, request: 'ChatRequest') -> 'ChatResponse'Execute a non-streaming chat completion.
plural.providers.base.Provider.stream
stream(self, request: 'ChatRequest') -> 'Iterator[StreamChunk]'Execute a streaming chat completion.
plural.providers.base.Provider.achat
achat(self, request: 'ChatRequest') -> 'ChatResponse'Async non-streaming chat completion.
plural.providers.base.Provider.astream
astream(self, request: 'ChatRequest') -> 'AsyncIterator[StreamChunk]'Async streaming chat completion.
Implementations are typically ``async def`` generators whose returntype is :class:`~typing.AsyncIterator`.plural.providers.base.Provider.close
close(self) -> 'None'Close underlying HTTP resources.
plural.providers.base.Provider.aclose
aclose(self) -> 'None'Close underlying async HTTP resources.
plural.providers.base.ProviderConfig
Configuration for a provider adapter.
Attributes: api_key: Provider API key. base_url: API base URL including version prefix when required. timeout_s: Request timeout in seconds. default_headers: Extra headers sent with every request. organization: Optional organization id (OpenAI-style).plural.providers.openai_compatible.QwenProvider
Qwen via DashScope compatible-mode.
plural.providers.openai_compatible.QwenProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.TogetherProvider
Together AI OpenAI-compatible API.
plural.providers.openai_compatible.TogetherProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.XAIProvider
xAI OpenAI-compatible API.
plural.providers.openai_compatible.XAIProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.providers.openai_compatible.ZhipuProvider
Zhipu / Z.ai lab API.
plural.providers.openai_compatible.ZhipuProvider(api_key: 'str', *, base_url: 'str | None' = None, name: 'str | None' = None, timeout_s: 'float | None' = None, default_headers: 'dict[str, str] | None' = None, organization: 'str | None' = None, strip_model_prefix: 'bool | None' = None, http_transport: 'httpx.BaseTransport | None' = None, async_http_transport: 'httpx.AsyncBaseTransport | None' = None) -> 'None'plural.errors.AuthenticationError
Raised when credentials are missing or rejected (typically HTTP 401/403).
plural.errors.AuthenticationError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.BudgetExceededError
Raised when a request would exceed a configured cost or token budget.
plural.errors.BudgetExceededError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.ConfigurationError
Raised when plural is misconfigured (missing keys, unknown models, etc.).
plural.errors.ConfigurationError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.ConflictError
Raised when a unique slug or name already exists (typically HTTP 409).
plural.errors.ConflictError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.ContentFilterError
Raised when a provider refuses the request due to a content filter.
plural.errors.ContentFilterError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.ContextLengthError
Raised when the request exceeds the model's context window.
plural.errors.ContextLengthError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.InvalidRequestError
Raised when the request is malformed or uses unsupported parameters.
plural.errors.InvalidRequestError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.NotFoundError
Raised when a requested resource (model, trace, dataset) cannot be found.
plural.errors.NotFoundError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.PluralError
Base class for all plural errors.
Args: message: Human-readable description of the failure. provider: Provider slug associated with the failure, if any. status_code: HTTP status code, if the failure came from an HTTP response. body: Raw response body or structured error payload, if available. model: Model id that was being called, if known.plural.errors.PluralError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.PluralTimeoutError
Raised when a provider call exceeds the configured timeout.
Formerly ``plural.TimeoutError``, which shadowed Python's built-in.plural.errors.PluralTimeoutError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.ProviderUnavailable
Raised when a provider is down or returns a transient 5xx error.
plural.errors.ProviderUnavailable(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.RateLimitError
Raised when a provider rate-limits the request (typically HTTP 429).
plural.errors.RateLimitError(message: 'str', *, provider: 'str | None' = None, status_code: 'int | None' = None, body: 'Any' = None, model: 'str | None' = None) -> 'None'plural.errors.is_retryable
Return whether `error` should be retried by the router.
Args: error: The exception raised by a provider call. Returns: ``True`` if the error is a known retryable plural error. Examples: >>> is_retryable(PluralTimeoutError("timed out")) True >>> is_retryable(AuthenticationError("bad key")) Falseplural.errors.is_retryable(error: 'BaseException') -> 'bool'