API Reference
Main Functions
complete
def complete(
multipart_prompt: str,
context: Union[dict, List[dict]] = {},
*,
model: LLM = None,
credentials: Optional[LLMCredentials] = None,
spec: Optional[ModelSpec] = None,
registry: Optional[ModelRegistry] = None,
extra_kwargs=None,
template_path: Optional[Path] = None,
include_paths: Optional[List[Path]] = None,
strict_undefined: bool = False,
strict_params: bool = False,
max_concurrent: Optional[int] = None,
on_complete: Optional[callable] = None,
stop_at: Optional[str] = None,
on_halt: str = "raise",
tools=None,
deps=None,
deps_type=None,
limits=None,
) -> Union[StruckdownResult, List[StruckdownResult]]
Process a struckdown template with one or more contexts.
Parameters:
| Parameter | Type | Description |
|---|---|---|
multipart_prompt | str | Struckdown template string |
context | dict or List[dict] | Variables for template rendering. A list runs the template once per context |
model | LLM | Model configuration |
credentials | LLMCredentials | API credentials – pass explicitly when using struckdown as a library |
spec | ModelSpec | Model and credentials together, instead of model + credentials |
registry | ModelRegistry | Resolve the model by name or alias from a registry |
extra_kwargs | dict | Additional LLM parameters |
template_path | Path | Path for resolving includes |
include_paths | List[Path] | Additional include search paths |
strict_undefined | bool | Raise on undefined template variables |
strict_params | bool | Raise on unsupported LLM parameters instead of warning |
max_concurrent | int | Max concurrent requests (list mode) |
on_complete | callable | Callback after each completion |
stop_at | str | Slot name to stop after; later segments are skipped |
on_halt | str | "raise" (default) or "return" – see Halting |
tools | list | Python functions a use_tools=true slot may call |
deps | Any | Dependency object passed to tools |
deps_type | type | Type of deps, for tool signatures |
limits | UsageLimits | Ceiling on requests, tool calls and output tokens for tool slots |
Returns: StruckdownResult for a single context, List[StruckdownResult] for a list.
Raises: Halted when a [[halt:...]] slot trips and on_halt="raise".
Example:
from struckdown import complete
result = complete("Tell me a joke [[joke]]")
print(result["joke"])
complete_async
async def complete_async(
multipart_prompt: str,
context: Union[dict, List[dict]] = {},
**kwargs
) -> Union[StruckdownResult, List[StruckdownResult]]
Async version of complete(). Same parameters.
Example:
import asyncio
from struckdown import complete_async
async def main():
result = await complete_async("Tell me a joke [[joke]]")
print(result["joke"])
asyncio.run(main())
complete_incremental_async
async def complete_incremental_async(
multipart_prompt: str,
model: LLM = None,
credentials: Optional[LLMCredentials] = None,
context={},
extra_kwargs=None,
template_path: Optional[Path] = None,
include_paths: Optional[List[Path]] = None,
strict_undefined: bool = False,
stream: bool = True,
strict_params: bool = False,
stop_at: Optional[str] = None,
on_halt: str = "raise",
tools=None,
deps=None,
deps_type=None,
limits=None,
*,
spec: Optional[ModelSpec] = None,
registry: Optional[ModelRegistry] = None,
) -> AsyncGenerator[IncrementalEvent, None]
Process a template one slot at a time, yielding events as work completes. Takes a single context, not a list. With stream=True (the default), free-text slots also yield their tokens as they arrive.
Events:
| Event | Fields | When |
|---|---|---|
SlotStreamStart | segment_index, slot_key | A slot begins streaming |
TokenDelta | segment_index, slot_key, delta, accumulated | Each chunk of streamed text |
ThinkingDelta | segment_index, slot_key, delta, accumulated | Each chunk of streamed reasoning |
SlotCompleted | segment_index, slot_key, result, elapsed_ms, was_cached | A slot has its final value |
SlotRetracted | segment_index, slot_key, reason | Streamed text must be withdrawn |
ToolStarted | segment_index, slot_key, tool_name, arguments | A tool call begins |
ToolCompleted | …plus output, ok, error, elapsed_ms, was_cached | A tool call returns |
CheckpointReached | segment_index, segment_name, accumulated_results | A <checkpoint> boundary |
ProcessingComplete | result, early_termination | Final event, with the aggregated StruckdownResult |
ProcessingError | segment_index, slot_key, error_message, partial_results | The run failed |
Every event type is importable from struckdown.incremental, which is the import path to prefer. Most are also re-exported from struckdown itself; ToolStarted and ToolCompleted are not.
Example:
from struckdown import complete_incremental_async, SlotCompleted
from struckdown.incremental import TokenDelta
async for event in complete_incremental_async(prompt, context=ctx,
model=model, credentials=creds):
if isinstance(event, TokenDelta):
print(event.delta, end="", flush=True)
elif isinstance(event, SlotCompleted):
print(f"\n{event.slot_key} done in {event.elapsed_ms:.0f}ms")
Closing the generator cancels the call in flight, so a stop button also ends the spending.
complete_incremental
def complete_incremental(
multipart_prompt: str,
...,
stream: bool = False,
) -> Generator[IncrementalEvent, None, None]
Synchronous version of complete_incremental_async(). Same events and parameters, except that stream defaults to False.
Halting
A [[halt:...]] slot stops a run when its verdict holds. See Halting a Run for the template side.
Halted
from struckdown import Halted
Raised when a guard trips and on_halt="raise" (the default).
Attributes:
| Attribute | Type | Description |
|---|---|---|
slot | str | Name of the halt slot that tripped |
reason | str | The model’s one-sentence justification, written for a log |
results | StruckdownResult | The slots that finished before the run stopped |
when | bool | The slot’s when= setting |
from struckdown import complete, Halted
try:
result = complete(prompt, context=ctx, model=model, credentials=creds)
except Halted as halted:
log.warning("halted at [[halt:%s]]: %s", halted.slot, halted.reason)
return "I can't help with that."
With on_halt="return", complete() returns the partial results instead of raising, and the incremental generators yield ProcessingComplete(early_termination=True) rather than raising.
results holds every slot that finished, which can include one that ran beside the guard and completed before the verdict came back. They are there to log and to bill, not to show. A slot still streaming when the guard trips is withdrawn with a SlotRetracted event.
reason is not for the person being judged: showing it to them describes how the guard works. A guard is an LLM call reading the same untrusted text, so it is a signal to count, not a security boundary.
readonly
from struckdown import readonly
@readonly
def search_handbook(query: str) -> list[dict]:
...
Marks a tool as having no side effects, so a guard running speculatively beside a tool slot need not be settled before the tool runs. Leave it off anything that writes: undeclared means “might write”, which forces the join.
Other Functions
get_embedding
def get_embedding(
texts: List[str],
model: Optional[str] = None,
credentials: Optional[LLMCredentials] = None,
dimensions: Optional[int] = None,
batch_size: int = 100,
max_tokens_per_batch: Optional[int] = None,
progress_callback: Optional[Callable[[int], None]] = None,
cost_callback: Optional[Callable[[float, int, int], None]] = None,
) -> EmbeddingResultList
Get embeddings for texts using API or local models.
Parameters:
| Parameter | Type | Description |
|---|---|---|
texts | List[str] | Texts to embed |
model | str | Model name (e.g., "text-embedding-3-small") |
credentials | LLMCredentials | API credentials |
dimensions | int | Output dimensions (model-specific) |
batch_size | int | Texts per API batch |
max_tokens_per_batch | int | Token ceiling per batch |
progress_callback | Callable | Called with count completed |
cost_callback | Callable | Called with cost and token counts |
Returns: EmbeddingResultList containing EmbeddingResult arrays.
Example:
from struckdown import get_embedding
import numpy as np
results = get_embedding(["hello", "world"])
similarity = np.dot(results[0], results[1])
print(f"Cost: ${results.total_cost}")
get_embedding_async
async def get_embedding_async(
texts: List[str],
**kwargs
) -> EmbeddingResultList
Async version of get_embedding(). Same parameters.
structured_chat
def structured_chat(
prompt: str = None,
messages: List[Dict] = None,
return_type: BaseModel = None,
llm: LLM = None,
credentials: LLMCredentials = None,
max_retries: int = 3,
max_tokens: Optional[int] = None,
extra_kwargs: Optional[dict] = None,
strict_params: bool = False,
) -> Tuple[BaseModel, Box]
Low-level function for structured LLM calls with Pydantic models. No template parsing: this is the call complete() makes for each slot.
Returns: Tuple of (parsed response, completion object).
Result Classes
StruckdownResult
Container for template processing results.
Properties:
| Property | Type | Description |
|---|---|---|
results | Dict[str, SlotResult] | Results by slot name |
response | Any | Last slot’s output |
outputs | Box | All outputs as a Box dict |
thinking | Dict[str, str] | Reasoning text by slot name, where the model returned any |
total_cost | float | Total USD cost. Counts an unknown cost as 0.0 (check has_unknown_costs) and cached slots at their original cost |
prompt_tokens | int | Total input tokens |
completion_tokens | int | Total output tokens |
total_tokens | int | Total tokens |
cached_prompt_tokens | int | Input tokens served from the provider’s cache |
cache_creation_tokens | int | Input tokens written to the provider’s cache |
has_unknown_costs | bool | Any unknown costs |
all_costs_unknown | bool | Every cost unknown |
fresh_call_count | int | Fresh API calls |
cached_call_count | int | Cache hits |
fresh_cost | float | Cost of fresh calls only: what this run spent |
cached_cost | float | Original cost of the slots served from the response cache: what the cache saved |
Methods:
result["slot_name"] # Get slot output
result.keys() # List slot names
len(result) # Number of slots
result.strip_debug_data() # Drop prompts and raw completions
SlotResult
One slot’s outcome, as found in StruckdownResult.results.
Fields:
| Field | Type | Description |
|---|---|---|
name | str | Slot name |
prompt | str | The prompt sent for this slot |
output | Any | The parsed value |
completion | Box | Raw provider response, with usage and cost |
action | Optional[str] | Action or type name, where the slot had one |
options | list | Parsed slot options |
params | dict | Per-slot LLM parameters |
response_schema | dict | Schema sent to the provider |
EmbeddingResult
Numpy array subclass with cost metadata.
Properties:
| Property | Type | Description |
|---|---|---|
cost | Optional[float] | USD cost (None if unknown) |
tokens | int | Token count |
model | str | Model name |
cached | bool | From cache |
Works as a normal numpy array:
import numpy as np
emb = results[0]
similarity = np.dot(emb, other_emb)
EmbeddingResultList
List of EmbeddingResult with aggregate properties.
Properties:
| Property | Type | Description |
|---|---|---|
total_cost | Optional[float] | Total cost (None if any unknown) |
total_tokens | int | Total tokens |
cached_count | int | Cached embeddings |
fresh_count | int | Fresh embeddings |
fresh_cost | Optional[float] | Cost from fresh only |
has_unknown_costs | bool | Any unknown costs |
model | str | Model name |
CostSummary
Aggregate costs across multiple results.
from struckdown import CostSummary
summary = CostSummary.from_results([result1, result2])
print(summary.format_summary())
# Total cost: $0.0012 (1,520 in / 310 out)
# This run: $0.0004 (3 fresh, 2 cached)
format_summary(include_breakdown=True) returns the line the CLI prints; the second line appears only when some calls were cached. When some costs are unknown the total is shown as a lower bound (>=$...), and as unknown when none is known.
Fields:
| Field | Type | Description |
|---|---|---|
total_cost | float | Combined StruckdownResult.total_cost |
fresh_cost | float | Combined fresh_cost |
total_prompt_tokens | int | Combined input tokens |
total_completion_tokens | int | Combined output tokens |
cached_prompt_tokens | int | Input tokens read from the provider’s cache |
cache_creation_tokens | int | Input tokens written to the provider’s cache |
fresh_count | int | Fresh API calls |
cached_count | int | Response-cache hits |
has_unknown_costs | bool | Any result has an unknown cost |
all_costs_unknown | bool | No result has a known cost |
Configuration Classes
LLM
Model configuration. Defaults are read from environment variables for CLI convenience but should be passed explicitly when using struckdown as a library.
from struckdown import LLM, complete
llm = LLM(model_name="openai:gpt-4o")
result = complete("...", model=llm)
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
model_name | str | DEFAULT_LLM env var, or gpt-4.1-mini | Model identifier in provider:model format |
LLMCredentials
API credentials for LLM calls. Defaults are read from environment variables for CLI convenience. When embedding struckdown in a web application, always pass credentials explicitly (e.g. from a database-stored Credential via ModelSpec.as_credentials()).
from struckdown import LLMCredentials, complete
creds = LLMCredentials(
api_key="sk-...",
base_url="https://api.openai.com/v1"
)
result = complete("...", credentials=creds)
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
api_key | str | LLM_API_KEY env var | API key for the provider |
base_url | str | LLM_API_BASE env var | Base URL (set for proxies, leave empty for direct provider access) |
ModelSpec
Portable, self-contained specification for a model endpoint. Combines identity, credentials, pricing, and metadata. Preferred over passing LLM + LLMCredentials separately – complete() takes it directly as spec=.
from struckdown.model_spec import ModelSpec
spec = ModelSpec(
model_name="openai:gpt-4o",
api_key="sk-...",
input_cost_per_mtok=2.50,
output_cost_per_mtok=10.0,
)
result = complete("...", spec=spec)
# or convert to LLM + credentials for structured_chat
llm = spec.as_llm()
credentials = spec.as_credentials()
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
model_name | str | (required) | Model identifier in provider:model format |
model_type | "llm" or "embedding" | "llm" | Model type |
api_key | SecretStr | None | API key (masked in repr) |
base_url | str | None | Base URL for proxies |
data_residency | str | None | Data residency region |
display_name | str | None | Human-readable name |
input_cost_per_mtok | float | None | Input cost per million tokens (USD) |
output_cost_per_mtok | float | None | Output cost per million tokens (USD) |
cache_read_cost_per_mtok | float | None | Price of a prompt token read from the provider’s cache (USD per million); the input rate if unset |
cache_write_cost_per_mtok | float | None | Price of a prompt token written to the provider’s cache (USD per million); the input rate if unset |
model_ref | str | None | Your own identifier for this model, copied onto every usage record |
Computed fields: provider (extracted from model_name), bare_name (model name without provider prefix), provider_display (human-readable provider name).
When both the input and output rates are set, struckdown uses them for cost calculation instead of looking up prices via pydantic-ai or genai-prices. complete(spec=...) sets them, with model_ref, for the calls it makes; as_llm() and as_credentials() do not, so a structured_chat call made with those needs set_model_pricing(...). See Cost Tracking.
ModelRegistry
Collection of ModelSpec instances with alias resolution. Used by pipeline systems (e.g. soaking) to manage multiple models. complete() takes one as registry=.
from struckdown.model_spec import ModelSpec, ModelRegistry
registry = ModelRegistry(
models={
"openai:gpt-4o": ModelSpec(model_name="openai:gpt-4o", api_key="sk-..."),
"openai:gpt-4o-mini": ModelSpec(model_name="openai:gpt-4o-mini", api_key="sk-..."),
},
aliases={"default": "openai:gpt-4o-mini", "best": "openai:gpt-4o"},
default_llm="openai:gpt-4o-mini",
)
spec = registry.resolve("best") # returns the gpt-4o spec
Fields:
| Field | Type | Default | Description |
|---|---|---|---|
models | Dict[str, ModelSpec] | {} | Registered models keyed by model_name |
aliases | Dict[str, str] | {} | Alias to model_name mappings |
default_llm | str | None | Default LLM model name |
default_embedding | str | None | Default embedding model name |
Methods:
| Method | Description |
|---|---|
resolve(name_or_alias) | Resolve a name or alias to a ModelSpec |
resolve_embedding(name) | Resolve an embedding model |
register(spec) | Add a ModelSpec to the registry |
llms() | List all LLM specs |
embeddings() | List all embedding specs |
from_env() | Build a minimal registry from DEFAULT_LLM / LLM_API_KEY / LLM_API_BASE env vars (CLI convenience) |
Utility Functions
clear_cache
from struckdown import clear_cache
clear_cache() # Clear LLM response cache
clear_embedding_cache
from struckdown.embedding_cache import clear_embedding_cache
clear_embedding_cache() # Clear embedding cache
progress_tracking
from struckdown import progress_tracking, complete
def on_call():
print("API call completed!")
with progress_tracking(on_api_call=on_call):
result = complete(prompt)
Context manager that fires a callback after each LLM completion, for progress reporting without changing the complete() signature.
set_model_pricing
from struckdown import set_model_pricing
set_model_pricing(
input_cost_per_mtok=0.40,
output_cost_per_mtok=1.60,
cache_read_cost_per_mtok=0.10, # optional
cache_write_cost_per_mtok=None, # optional
)
set_model_pricing(None, None) # clear
Stored pricing for the calls that follow in this context, whatever their model. set_audio_pricing(cost_per_minute) does the same for transcription, and set_model_ref(ref) names the model on usage records. All three are context variables: set them immediately before the call they belong to. See the Usage Ledger reference.
Usage records
register_usage_handler, usage_tracking, UsageRecord, CostBreakdown and the Django ledger are documented in the Usage Ledger reference.
mark_struckdown_safe
from struckdown import mark_struckdown_safe
safe_content = mark_struckdown_safe("<system>...</system>")
Mark content as safe to bypass auto-escaping.