htmlp

Rust interface

Give every prompt fragment a source. Keep estimates and reported usage distinct.

Use HTMLP's runtime types without writing a prompt file, or connect them to checked file rendering. Your provider adapter sends the request; HTMLP supplies the types and accounting.

Install

[dependencies]
htmlp = { git = "https://github.com/alexmckenley/htmlp", tag = "v0.2.0-alpha.4", features = ["runtime"] }

runtime enables JSON types without a provider SDK, async runtime, networking, or tokenizer tables. Add tokens for Cl100k. See all library features.

Build a request

use htmlp::runtime::{ContentSource, ModelRequest, PromptFragment, Role};

let request = ModelRequest::builder("my-model")
    .fragment(PromptFragment::text(
        ContentSource::SystemPrompt,
        Role::System,
        "Review the change.",
    ))
    .fragment(PromptFragment::text(
        ContentSource::UserMessage,
        Role::User,
        "Check the parser.",
    ))
    .max_output_tokens(1024)
    .build();

let estimate = request.estimate();
let system_tokens = estimate.tokens_for(&ContentSource::SystemPrompt);

PromptFragment has private fields. Every constructor requires a ContentSource and a Role; ModelRequestBuilder accepts attributed fragments. The type system requires a category, while the caller chooses the correct one. Source and role are independent: repository context can use Role::User.

Attribute context

ContentSourceUse for
SystemPromptBase instructions.
ContextBlock { block_id }Named rules, skills, environment, or retrieved context.
ToolDefinitionsAvailable tool schemas; builder tools are counted here automatically.
UserMessage, SessionMessageUser-authored input and session messages.
AssistantHistoryReplayed assistant output.
ToolOutput { tool_name }Results attributed to a named tool.
SystemReminder { origin }, Hook { hook_name }Harness-injected reminders and hooks.
CompactionSummarySummarized conversation context.

Names are part of the source key, so two skill IDs have separate accounting. These categories belong to the Rust and JSON API. Markup currently has no category or source attribute, and section IDs do not assign a source or role.

Render a checked file

Author a file with budgets and reasons, then run htmlp sign rules.htmlp. Add tokens alongside runtime for this example:

use htmlp::{Bindings, Cl100k, parse_file, render_checked};
use htmlp::runtime::{ContentSource, PromptFragment, Role};

let document = parse_file("rules.htmlp").expect("valid markup");
let counter = Cl100k::new().expect("tokenizer");
let bindings = Bindings::from([("task".into(), "Review the change.".into())]);
let rendered = render_checked(&document, &bindings, &counter)
    .expect("valid signatures, bindings, and budgets");
let fragment = PromptFragment::checked(
    ContentSource::SystemPrompt, Role::System, &rendered,
);

Handle diagnostics in your application; expect keeps these examples short. Rendering substitutes values literally and enforces every file, section, and direct-item budget. Missing bindings, invalid signatures, and exceeded budgets return errors before any checked text is exposed.

PromptFragment::checked copies validated text into a fragment. Runtime estimation still uses its explicit heuristic; it does not reuse the file's exact count. Extracting and changing a string does not retain the original validation guarantee.

Track token usage

TypeMeasurement
TextMeasurementComplete rendered document count from the chosen tokenizer.
RequestEstimateHeuristic request allocation by source, including image reserves.
ReportedUsageProvider-reported totals for one call.
TokenUsageOptional estimate, optional reported usage, and context window.
use htmlp::runtime::{ReportedUsage, TokenUsage};

let usage = TokenUsage {
    estimate: Some(request.estimate()),
    reported: Some(ReportedUsage {
        input_tokens: 120,
        cache_read_tokens: 80,
        output_tokens: 30,
        reasoning_tokens: 10,
        ..Default::default()
    }),
    context_window_tokens: 128_000,
};
assert_eq!(usage.prompt_tokens(), Some(200));

prompt_tokens() prefers reported input plus cache reads and writes, then falls back to the estimate. It returns None when neither exists. An external harness with unavailable attribution should set estimate: None.

ReportedUsage.input_tokens is uncached input; cache lanes are disjoint. Reasoning tokens are a subset of output, not an extra output charge. Provider adapters normalize their own conventions. Merge cumulative snapshots of the same call with merge_snapshot; sum distinct completed calls with add_call.

ByteEstimator defaults to four UTF-8 payload bytes per token, rounded up once per full source key, plus 8,192 tokens per image. Customize its nonzero bytes_per_token and image_tokens fields, then call estimate(&request). sources() exposes each allocation; total_tokens() derives the sum. These estimates exclude provider framing and do not enforce a context-window limit.

Tools, images, and adapters

PromptFragment::new accepts typed ContentBlock values: text, reasoning, images, tool uses, and tool results. Supply tool-call IDs with ToolUseId::from_string. Add ToolDefinition values through builder.tool(...); schemas use JSON. Set output limits and reasoning preferences with max_output_tokens and thinking_level.

Adapters read request.fragments(), request.tools(), and request settings to build provider-specific messages. bytes_by_source() measures raw payload bytes, including image transport data; it is distinct from token estimation.

Request helpers return derived requests: with_suffix_instruction appends an attributed user instruction and removes tools; with_warm_probe keeps tools, appends a reminder, and sets a one-token output limit; without_image_blocks replaces images with a text note while preserving attribution. Estimate the resulting request before sending it. Cache behavior and provider compatibility remain the adapter's responsibility.

Reference

The JSON schemas describe serialized shapes. PromptFragment and ModelRequest are constructed through Rust APIs, not deserialized from JSON. HTMLP does not send model requests or provide native bindings for other languages. Other languages can use the CLI's JSON document output.