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:
-
CLI-only path — config/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.
-
--config merge path — config/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 (" → "), 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
Summary
--endpoint-type templatewith--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 theendpoint.templateblock the same way, even though the code comments say they should.Root cause
There are two converters from CLI flags to
AIPerfConfig:CLI-only path —
config/flags/_converter_endpoint.py::build_endpoint()calls_endpoint_template_from_extra()(and_endpoint_template_fallback()), which poppayload_template/response_fieldout ofextra_inputs, read the template file viasafe_read_template_path(), and populateendpoint.template = {body, response_field}.→
endpoint.templateis set, validation passes, the run works.--configmerge path —config/flags/resolver.py::build_cli_overrides()→_apply_input_overrides()only doesendpoint["extra"] = dict(cli.extra_inputs). It doesnot call
_endpoint_template_from_extra/_endpoint_template_fallback. Sopayload_templatestays insideendpoint.extra,endpoint.templateis never built, andEndpointConfig._validate_template_required(config/endpoint.py) raises.build_cli_overrides's own docstring states it "produces identical AIPerfConfig shape to theCLI-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.bodyorendpoint.extra["payload_template"]), so the config-timevalidator is stricter than the runtime — it rejects an
extra-only config the runtime couldhave executed.
Steps to reproduce
Works (CLI only):
Fails (same flags + a
--configfile that sets any endpoint field):Expected
--extra-inputs payload_template/response_fieldshould be lifted intoendpoint.templateon both paths (make
build_cli_overrides/_apply_input_overridesreuse_endpoint_template_from_extra+_endpoint_template_fallback), so combining--configwith 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.bodydoc lists a{{prompt}}variable that does not existconfig/endpoint.pydocuments "Variables:{{prompt}},{{max_tokens}},{{model}},{{messages}}, …". But the render context inendpoints/template_endpoint.py::format_payload()exposes noprompt(and nomessages). 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_fielddoc says "dot notation" but extraction is JMESPathconfig/endpoint.pydocuments "Use dot notation for nested fields:choices.0.message.content", butparse_response()compiles the value withjmespath.compile(). JMESPath indexes arrays with brackets —choices[0].message.content—so the documented
choices.0.message.contentdoes not select an array element and yields nomatch.
3.
response_fieldmismatch is now a hard failure (no auto-detect fallback)When
response_fieldis set but the JMESPath finds nothing,parse_response()returnsNone(counted as a failed/zero-length response) instead of falling back to auto-detection.Combined with #2, a doc-following
response_fieldsilently produces zero successfulresponses. (Appears intentional per the in-code comment, but worth documenting given #2.)
4. Jinja
autoescape=Trueon JSON templatesformat_payload()builds the template withjinja2.Environment(autoescape=True). Raw{{ var }}interpolations are HTML-escaped ("→"), which corrupts JSON bodies unlessevery value uses
|tojson(or|safe). Worth calling out in the template docs, since theendpoint expects the rendered body to
orjson.loads()cleanly.Environment