0.15→0.17 openai-generic wire change: `messages[].content` part array became a plain string — intentional? needs a migration note (breaks byte-stable request consumers)
Summary
Between 0.15.0 (classic syntax, provider: "openai-generic") and 0.17.0 (openai.GenericClient), the chat-completions request body changed shape: messages[].content was a part array [{"type":"text","text":"…"}] on 0.15.0 and is a plain string on 0.17.0. Same function, same args, same endpoint path — captured against a local HTTP sink.
If intentional (it looks like a reasonable normalization for text-only prompts), it deserves a migration-guide line: any consumer that records/byte-compares requests, proxies them, or runs a server that branches on the content type sees every single call change shape when migrating.
Reproduce
Same probe function on each line, VLLM_QWEN_BASE_URL pointed at a local capture sink.
0.15.0 source:
client<llm> Sink {
provider: "openai-generic",
options: {
base_url: env.VLLM_QWEN_BASE_URL,
model: "m",
api_key: env.VLLM_API_KEY,
temperature: 0.0,
max_tokens: 6144,
},
}
class Out { a string, }
function Probe(txt: string, opt: string?) -> Out {
client: Sink
prompt: #"
{{ _.role("user") }}
Text: {{ txt }}
Opt: {{ opt }}
{{ ctx.output_format }}
"#
}0.17.0 source (the migration of the same function):
client Sink = openai.GenericClient.new(
model = "m",
base_url = env.VLLM_QWEN_BASE_URL,
api_key = env.VLLM_API_KEY,
temperature = 0.0,
max_tokens = 6144,
);
class Out { a string, }
function Probe(txt: string, opt: string?) -> Out {
client: Sink
prompt: `${role("user")}
Text: ${txt}
Opt: ${opt}
${ctx.output_format}`
}Captured POST /v1/chat/completions bodies (identical args txt="hallo", opt=null):
0.15.0:
{"model":"m","messages":[{"role":"user","content":[{"type":"text","text":"Text: hallo\nOpt: null\nAnswer in JSON using this schema:\n{\n a: string,\n}"}]}],"temperature":0.0,"max_tokens":6144}0.17.0:
{"model":"m","messages":[{"role":"user","content":"\nText: hallo\nOpt: null\nAnswer in JSON using this schema:\n{\n a: string,\n}"}],"temperature":0.0,"max_tokens":6144}(The leading \n in the 0.17 content is a separate, also-migration-relevant semantics change: classic #"…"# prompt blocks dedented and swallowed the newline after {{ _.role("user") }}; backtick templates are literal — the newline after ${role("user")} is emitted. Both differences together mean a byte-stable migration of a prompt is not achievable by mechanical rewriting alone.)
Why it matters to a real consumer
zenzy-atlas runs a recorded-transcript eval discipline: prompt/toolchain changes are bounded by byte-comparing captured request JSON against a sink, and only lanes whose bytes change pay a live re-record. The 0.15→0.17 migration flips every lane's bytes via this envelope change alone, independent of prompt text — so the cheap "byte-identical ⇒ no re-record" bound is lost for the whole migration, and every recorded lane must be re-verified against the live model. A changelog note (and ideally a documented stability policy for the wire encoding of text-only prompts) would let consumers distinguish "the engine re-encoded" from "my prompt changed".
Asks
- Confirm the part-array → plain-string change is intentional in 0.17, and document it in the migration notes.
- Document the template dedent/role-marker newline semantics change (classic prompt blocks dedent; backtick templates are literal) in the same place — it is the other unavoidable render diff in a mechanical migration.
Environment: Linux x86_64, baml wrapper 0.2.0, 0.15.0 vs 0.17.0 via BAML_VERSION selector; capture sink is a ~20-line local HTTP server, available on request.
Source: BoundaryML/baml