Skip to content

--extra-inputs payload_template is dropped when a --config file is supplied #1135

Description

@Lokiiiiii

Summary

--endpoint-type template with --extra-inputs payload_template:<file> response_field:<path>
works when passed on the CLI alone, but fails config validation when the same flags are
combined with a --config <file>. The two config-resolution paths do not build the
endpoint.template block the same way, even though the code comments say they should.

Root cause

There are two converters from CLI flags to AIPerfConfig:

  1. CLI-only pathconfig/flags/_converter_endpoint.py::build_endpoint() calls
    _endpoint_template_from_extra() (and _endpoint_template_fallback()), which pop
    payload_template / response_field out of extra_inputs, read the template file via
    safe_read_template_path(), and populate endpoint.template = {body, response_field}.
    endpoint.template is set, validation passes, the run works.

  2. --config merge pathconfig/flags/resolver.py::build_cli_overrides()
    _apply_input_overrides() only does endpoint["extra"] = dict(cli.extra_inputs). It does
    not call _endpoint_template_from_extra / _endpoint_template_fallback. So
    payload_template stays inside endpoint.extra, endpoint.template is never built, and
    EndpointConfig._validate_template_required (config/endpoint.py) raises.

build_cli_overrides's own docstring states it "produces identical AIPerfConfig shape to the
CLI-only path for the same inputs" — but for template endpoints it does not.

The runtime endpoint (endpoints/template_endpoint.py) actually supports both sources
(endpoint.template.body or endpoint.extra["payload_template"]), so the config-time
validator is stricter than the runtime — it rejects an extra-only config the runtime could
have executed.

Steps to reproduce

Works (CLI only):

aiperf profile --url http://localhost:8000 --model m --endpoint-type template \
  --extra-inputs payload_template:/path/to/tmpl.jinja response_field:choices[0].message.content \
  --tokenizer <tok> --concurrency 1 --request-count 3

Fails (same flags + a --config file that sets any endpoint field):

╭─ Error Running AIPerf System ─────────────────────────────────────────────╮
│ 1 validation error for AIPerfConfig                                        │
│   Value error, template is required when endpoint type is 'template'       │
╰────────────────────────────────────────────────────────────────────────────╯

Expected

--extra-inputs payload_template / response_field should be lifted into endpoint.template
on both paths (make build_cli_overrides/_apply_input_overrides reuse
_endpoint_template_from_extra + _endpoint_template_fallback), so combining --config
with template CLI flags produces the same valid config as CLI-only.


Additional template documentation / UX bugs found while investigating

These are independent of the merge bug above and would trip up anyone authoring a template
against the field docs.

1. TemplateConfig.body doc lists a {{prompt}} variable that does not exist

config/endpoint.py documents "Variables: {{prompt}}, {{max_tokens}}, {{model}},
{{messages}}, …"
. But the render context in
endpoints/template_endpoint.py::format_payload() exposes no prompt (and no
messages). The actual variables are: text/texts, image/images, audio/audios,
video/videos, query/queries, passage/passages, *_by_name, model,
max_tokens, role, turn, turns, request_info, stream. A template written with
{{prompt}} renders empty.

2. response_field doc says "dot notation" but extraction is JMESPath

config/endpoint.py documents "Use dot notation for nested fields:
choices.0.message.content"
, but parse_response() compiles the value with
jmespath.compile(). JMESPath indexes arrays with brackets — choices[0].message.content
so the documented choices.0.message.content does not select an array element and yields no
match.

3. response_field mismatch is now a hard failure (no auto-detect fallback)

When response_field is set but the JMESPath finds nothing, parse_response() returns
None (counted as a failed/zero-length response) instead of falling back to auto-detection.
Combined with #2, a doc-following response_field silently produces zero successful
responses. (Appears intentional per the in-code comment, but worth documenting given #2.)

4. Jinja autoescape=True on JSON templates

format_payload() builds the template with jinja2.Environment(autoescape=True). Raw
{{ var }} interpolations are HTML-escaped ("&#34;), which corrupts JSON bodies unless
every value uses |tojson (or |safe). Worth calling out in the template docs, since the
endpoint expects the rendered body to orjson.loads() cleanly.

Environment

  • aiperf 0.11.0, Python 3.11

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions