Trimwise exposes a deliberately small public API. Most applications need only Trimmer and the text field of its result. Use TrimConfig when the defaults do not match your tokenizer, omission style, relevance-versus-repetition preference, or Trimwise-managed FastEmbed setup.
from trimwise import TrimConfig, Trimmer
trimmer = Trimmer(
TrimConfig(
omission_marker="[...source omitted...]",
mmr_lambda=0.8,
)
)
result = trimmer.trim(
document,
limit=500,
query="What caused the outage?",
)
prompt_ready_excerpt = result.text
Public imports¶
Import supported objects directly from trimwise:
from trimwise import (
BudgetUnit,
SemanticBackendError,
SourceSpan,
Strategy,
TrimConfig,
TrimResult,
Trimmer,
)
These are the package's seven documented exports:
| Name | Purpose |
|---|---|
Trimmer | Runs synchronous or asynchronous trimming and reuses semantic state |
TrimConfig | Stores immutable reusable configuration |
TrimResult | Reports the excerpt, measurements, resolved strategy, and whether text changed |
SourceSpan | Identifies one retained range in the original input string |
Strategy | Enumerates automatic, structural, lexical, semantic, and hybrid selection |
BudgetUnit | Enumerates token, word, and character measurement |
SemanticBackendError | Reports callback or FastEmbed failures without hiding the original cause |
Internal modules and underscored names are not part of the compatibility promise. Trimwise ships a py.typed marker, so type checkers can use its inline annotations from the installed package.
Trimmer¶
Create a Trimmer once and reuse it across related calls:
Trimmer(
config: TrimConfig | None = None,
*,
embedding_callback: Callable | None = None,
async_embedding_callback: Callable | None = None,
)
| Constructor argument | Default | Meaning |
|---|---|---|
config | TrimConfig() | Reusable measurement, ranking, marker, and FastEmbed settings |
embedding_callback | None | Synchronous query-and-passage embedding backend |
async_embedding_callback | None | Asynchronous query-and-passage embedding backend used through atrim() |
Only one embedding callback may be configured. Non-callable callback values raise TypeError, and supplying both callback forms raises ValueError.
Reusing a Trimmer has little effect on structural and lexical calls. For Trimwise-managed FastEmbed, it matters: the instance lazily caches one initialized model and serializes access to that model. Caller callbacks are not serialized and remain responsible for their own client reuse, caching, rate limits, and thread or task safety.
trim()¶
Use trim() in synchronous code:
trim(
text: str,
limit: int,
*,
unit: BudgetUnit | str = BudgetUnit.TOKENS,
strategy: Strategy | str = Strategy.AUTO,
query: str | None = None,
token_counter: Callable[[str], int] | None = None,
) -> TrimResult
| Argument | Meaning |
|---|---|
text | The complete source string to reduce |
limit | Maximum measured size of the returned text |
unit | tokens, words, or characters |
strategy | auto, structural, lexical, semantic, or hybrid |
query | Optional task or question; required by lexical, semantic, and hybrid strategies |
token_counter | Optional synchronous counter that replaces built-in token measurement for this call |
text must be a Python str. limit must be a non-boolean integer greater than or equal to zero. Enum members or exact lowercase string values are accepted for unit and strategy; unsupported values raise ValueError.
from trimwise import BudgetUnit, Strategy, Trimmer
trimmer = Trimmer()
using_strings = trimmer.trim(
document,
500,
unit="tokens",
strategy="lexical",
query="ORION-774",
)
using_enums = trimmer.trim(
document,
500,
unit=BudgetUnit.TOKENS,
strategy=Strategy.LEXICAL,
query="ORION-774",
)
assert using_strings == using_enums
atrim()¶
atrim() accepts the same arguments and returns the same TrimResult:
async atrim(
text: str,
limit: int,
*,
unit: BudgetUnit | str = BudgetUnit.TOKENS,
strategy: Strategy | str = Strategy.AUTO,
query: str | None = None,
token_counter: Callable[[str], int] | None = None,
) -> TrimResult
import asyncio
from trimwise import Trimmer
async def main() -> None:
"""Trim one source without blocking the event loop."""
result = await Trimmer().atrim(
document,
limit=500,
query="What caused the outage?",
)
print(result.text)
asyncio.run(main())
Without an async embedding callback, atrim() sends the synchronous pipeline to a worker thread. With an async callback, preparation and CPU work use worker threads while the callback is awaited on the calling event loop. Cancellation can propagate into an awaited async callback, but it cannot terminate a worker thread that has already started. See Semantic Models and Async Usage for the full execution model.
Validation and fast paths¶
Every call follows this public order:
- Parse
unitandstrategy. - Normalize and validate the query.
- Validate the text, limit, and custom counter.
- Measure the input.
- Return an early result if the limit is zero or the input already fits.
- Segment, rank, select, compose, and remeasure only when trimming is required.
This order means invalid arguments are rejected even when the source would fit. It also means a valid fitting semantic call returns unchanged without importing FastEmbed or invoking a callback.
| Input condition | Result |
|---|---|
limit < 0 | Raises ValueError |
limit is True, a float, or a string | Raises TypeError |
limit == 0 | Returns empty text after measuring the input |
Input count is at most limit | Returns the original string exactly |
| Empty input with a positive limit | Returns the empty input unchanged |
The unchanged path preserves spaces and line endings exactly. trimmed is calculated from whether result.text != text, not merely from whether the input was over budget.
Strategy and query rules¶
auto resolves before trimming begins:
| Requested strategy and query | Resolved strategy |
|---|---|
auto with no query | structural |
auto with None, an empty string, or whitespace-only text | structural |
auto with a nonblank query | lexical |
Explicit structural | structural; any supplied query is ignored for ranking |
Explicit lexical, semantic, or hybrid | Same strategy; a nonblank query is required |
Leading and trailing whitespace is stripped from a usable query before ranking. result.strategy contains the resolved concrete strategy, never auto.
Automatic mode never chooses semantic or hybrid. This is a deliberate cost boundary: adding a query does not unexpectedly invoke an embedding provider or download a model.
Budget measurement¶
unit controls input_count, output_count, limit, fitting checks, and final enforcement:
| Unit | Exact rule |
|---|---|
tokens | Number of tiktoken IDs from the configured encoding; o200k_base by default |
words | len(text.split()) |
characters | len(text), measured as Python Unicode code points |
Token encoding uses disallowed_special=(), so strings that look like special tokens are measured as ordinary source text. Tiktoken may need network access the first time it places an encoding in its local cache.
Even after individual fragments appear to fit, Trimwise composes the complete source-ordered result with separators, headings, and affordable omission markers, then measures it again. The returned output_count cannot exceed limit.
Custom token counters¶
Use token_counter when the target model's tokenizer differs from the configured tiktoken encoding:
import tiktoken
from trimwise import Trimmer
encoding = tiktoken.get_encoding("cl100k_base")
def count_tokens(text: str) -> int:
"""Measure text with the target model's tokenizer.
Args:
text: Proposed source or composed output.
Returns:
Nonnegative token count.
"""
return len(encoding.encode(text, disallowed_special=()))
result = Trimmer().trim(
document,
limit=500,
unit="tokens",
token_counter=count_tokens,
)
The callback must be synchronous and return a nonnegative, non-boolean integer. It is valid only with unit="tokens". Trimwise uses it for input measurement, candidate fitting, fallback prefix search, and the final result count.
The custom counter replaces public token measurement, not lexical tokenization. Structural and lexical scoring—and the lexical half of hybrid scoring—still use TrimConfig.token_encoding to create consistent subword IDs. Keep that encoding representative of the text even when a custom counter owns the hard budget.
Because Trimwise does not assume a custom counter is monotonic, the rare final-prefix fallback may measure multiple source prefixes to find the longest exact fit. Keep the callback inexpensive or cache work inside it when this path matters.
TrimConfig¶
TrimConfig is a frozen, slotted dataclass:
TrimConfig(
token_encoding: str = "o200k_base",
embedding_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
fastembed_options: Mapping[str, Any] = {},
embedding_batch_size: int = 256,
mmr_lambda: float = 0.7,
omission_marker: str = "[…omitted…]",
)
| Setting | Change it when | Important behavior |
|---|---|---|
token_encoding | Built-in token budgets or lexical subwords should use another tiktoken encoding | Used lazily for token measurement and structural or lexical ranking |
embedding_model | Trimwise-managed FastEmbed should use another supported model | Ignored by caller-provided callbacks |
fastembed_options | TextEmbedding needs provider, cache, or runtime options | Forwarded only during managed model initialization |
embedding_batch_size | Managed passage inference needs a smaller memory footprint or different throughput tradeoff | Passed to passage_embed(); ignored by callbacks |
mmr_lambda | Relevance should receive more or less weight than repetition reduction | 1.0 removes the similarity penalty; lower values strengthen it |
omission_marker | The output needs another visible source-gap marker | Added only where source was omitted and only when it fits |
Most applications should keep the defaults. Configuration changes affect every trimming call made through that Trimmer; per-call choices such as strategy, query, limit, unit, and custom token counter remain method arguments.
Configuration validation¶
| Setting | Accepted boundary |
|---|---|
token_encoding | Nonblank string |
embedding_model | Nonblank string |
omission_marker | Nonblank string |
embedding_batch_size | Positive non-boolean integer |
mmr_lambda | Non-boolean integer or float from 0 through 1 |
fastembed_options | Mapping with string keys and no model_name key |
Invalid text-like settings can raise TypeError or ValueError depending on whether their type or content is wrong. Other invalid configuration boundaries raise the exception documented by their validation rule.
Configuration immutability¶
Trimwise defensively copies fastembed_options and exposes the copied top-level mapping as read-only. The original mapping can be changed later without altering the stored keys or values at that level.
from trimwise import TrimConfig
options = {"threads": 2}
config = TrimConfig(fastembed_options=options)
options["threads"] = 8
assert config.fastembed_options["threads"] == 2
The defensive copy is shallow. Avoid mutating nested objects stored as option values after the configuration is created.
Relevance versus repetition with mmr_lambda¶
Trimwise's MMR selection objective is:
mmr_lambda × relevance − (1 − mmr_lambda) × maximum selected similarity
The default 0.7 weighs relevance at 70% and the similarity penalty at 30%. 1.0 chooses by relevance without an MMR similarity penalty. At 0.0, relevance no longer influences the MMR objective, so candidates are chosen only by least similarity with deterministic source-order ties; that extreme is rarely a useful default.
MMR comes from established diversity-aware ranking research, but no single lambda is optimal for every document and task. Change it only when evaluation on your own prompts shows that the default keeps too much repeated-looking material or suppresses too much relevant evidence. See Choosing a Strategy and the original MMR work for context.
Omission markers¶
The default marker is […omitted…]. Trimwise considers leading, internal, and trailing source gaps in source order. A marker is added only when the complete output still fits; retained source text always takes priority.
from trimwise import TrimConfig, Trimmer
trimmer = Trimmer(TrimConfig(omission_marker="<source omitted>"))
result = trimmer.trim(
"First fact.\n\nRepeated filler.\n\nFinal decision.",
limit=38,
unit="characters",
)
Trimwise may also add minimal newline separators around non-adjacent fragments. The source fragments themselves remain unchanged.
FastEmbed settings¶
These settings affect only Trimwise-managed FastEmbed. They have no effect when embedding_callback or async_embedding_callback supplies vectors.
from trimwise import TrimConfig, Trimmer
config = TrimConfig(
embedding_model="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
embedding_batch_size=64,
fastembed_options={"providers": ["CPUExecutionProvider"]},
)
trimmer = Trimmer(config)
FastEmbed is imported and the model is initialized lazily. fastembed_options is expanded into the TextEmbedding constructor alongside model_name=embedding_model, which is why model_name is rejected inside the options mapping. The batch size is supplied separately during passage inference.
See Semantic Models and Async Usage for callback precedence, vector validation, model reuse, locking, cancellation, and error stages.
TrimResult¶
TrimResult is a frozen, slotted dataclass returned by both public methods:
| Field | Type | Meaning |
|---|---|---|
text | str | Prompt-ready extractive output |
input_count | int | Original source size measured in unit |
output_count | int | Returned text size measured in unit |
limit | int | Caller-supplied maximum output size |
unit | BudgetUnit | Concrete measurement unit |
strategy | Strategy | Concrete strategy after resolving auto |
trimmed | bool | Whether text differs from the input string |
spans | tuple[SourceSpan, ...] | Ordered original-input ranges retained in text |
from trimwise import Trimmer
source = "First fact. Middle detail. Final decision."
result = Trimmer().trim(source, limit=5, unit="words")
assert result.output_count <= result.limit
print(result.text)
print(result.input_count, result.output_count, result.unit.value)
print(result.strategy.value, result.trimmed)
print(result.spans)
Each span uses Python-string offsets: start is inclusive, end is exclusive, and source[span.start:span.end] returns the exact retained source fragment. Spans are ordered and nonoverlapping; adjacent source-backed ranges are merged, while overlapping ranges cannot arise from Trimwise's nonoverlapping candidates. A nonempty unchanged input has one span covering the complete string; an empty result has no spans. Omission markers and minimal separators generated by Trimwise are not included. Keep source identity in your own application when trimming several documents; spans do not contain paths, URLs, or document IDs.
Strategy¶
Strategy is a string enum:
| Member | Value | Query required | Embeddings required |
|---|---|---|---|
Strategy.AUTO | "auto" | No | Never selected automatically |
Strategy.STRUCTURAL | "structural" | No | No |
Strategy.LEXICAL | "lexical" | Yes | No |
Strategy.SEMANTIC | "semantic" | Yes | Yes |
Strategy.HYBRID | "hybrid" | Yes | Yes |
Read Choosing a Strategy for ranking, adaptive evidence selection, research background, runtime cost, and limitations.
BudgetUnit¶
BudgetUnit is a string enum:
| Member | Value |
|---|---|
BudgetUnit.TOKENS | "tokens" |
BudgetUnit.WORDS | "words" |
BudgetUnit.CHARACTERS | "characters" |
Returned results always contain enum instances even when the call used a string value.
SemanticBackendError¶
SemanticBackendError extends RuntimeError and represents optional semantic-backend failures. The message identifies whether a caller callback or FastEmbed failed and includes the relevant stage. The original exception is chained as error.__cause__.
from trimwise import SemanticBackendError, Trimmer
try:
result = Trimmer().trim(
document,
limit=500,
strategy="semantic",
query="What caused the outage?",
)
except SemanticBackendError as error:
print(error)
print("Original backend error:", error.__cause__)
Trimwise does not silently fall back to lexical scoring after a semantic failure. Handle the error, fix the backend, retry according to your application's policy, or deliberately issue a separate call with another strategy.
Exception guide¶
| Exception | Typical cause |
|---|---|
TypeError | Non-string input, non-integer limit, non-callable callback, invalid query type, or required async callback used through trim() |
ValueError | Negative limit, unsupported enum string, missing query, incompatible custom counter, invalid counter result, or invalid configuration |
SemanticBackendError | Callback inference/output failure or FastEmbed import, initialization, inference, or vector failure |
asyncio.CancelledError | The caller cancels an awaited atrim() task |
Argument and configuration errors are programming errors; fix the call rather than retrying it. Backend errors may be transient or permanent depending on their chained cause.
Fixed behavior that is not configuration¶
Trimwise intentionally does not expose every ranking constant. In v1, these remain fixed:
- BM25 uses
k1=1.5andb=0.75. - Primary relevance receives 90% of the relevance score; structural and fact-like signal receives 10%.
- Hybrid normally uses an equal normalized lexical-semantic blend.
- Hybrid's defensive RRF fallback uses
k=60. - Query-aware selection uses a fixed five-candidate recall buffer around its score-drop boundary.
These are implementation choices, not promises that each value is universally optimal. Keeping them private avoids a large tuning surface before Trimwise-shaped benchmarks justify it. Publicly configure only the behavior your application can evaluate directly: budget measurement, omission style, MMR balance, and the managed semantic backend.
Continue exploring¶
- Return to the Trimwise overview.
- Follow the Getting Started guide.
- Compare ranking behavior in Choosing a Strategy.
- Configure embeddings with Semantic Models and Async Usage.