Repository navigation
[Engine] API fields accepted and silently not honoured: one list to pick from, most with no known client #4839
Description
Area
[Engine] (speed, kernels, models, the server and API)
Desktop app, Terminal or source?
Source (START-HERE.bat, setup.sh or a build)
A source checkout with the mock engine. No model and no GPU were used.
What happened
The API server accepts these request fields, returns a success, and does not do what the field asks. A few routes at the end have a similar gap. This is one report so that you can pick from it, instead of many small issues or PRs.
Most rows have no known client behind them, and that may be reason enough to leave them. I am not asking for all of this, and I will not open a PR for a row unless you ask. "None of these" is a fair answer.
How to read the table.
- "What happens" was run on
c62d5e3awith the mock engine. The model's replies are scripted, so this shows what the server does with such a reply. It does not show how often a real model writes one. The Logs below are a fresh run of most rows. The rest, such asallowed_toolson/v1/responsesand what the SDKs raise, are from earlier runs on the same commit. - "Known client" notes come from reading client source, unless they say "ran". That is unconfirmed static analysis.
- "No thread found" means my searches of open and closed issues and PRs found none.
Requests and tool calls
| Field | What happens (mock engine) | What the source says | Existing thread | Known client |
|---|---|---|---|---|
parallel_tool_calls: false, chat and /v1/responses |
Two calls come back. /v1/responses also echoes false. |
OpenAI schema: "Whether to enable parallel function calling during tool use." | No thread found. #4630 only uses the term. | None known. No client was run. |
tool_choice.disable_parallel_tool_use, /v1/messages |
Two tool_use blocks come back. |
Anthropic: "Claude calls at most one tool in each response." | No thread found | None known. No client was run. |
tool_choice of type allowed_tools, chat and /v1/responses |
Acts as auto. A tool left out of the list is still in the prompt, and a call to it comes back. Mode required does not force a call. |
OpenAI schema: "Constrains the tools available to the model to a pre-defined set." | No thread found | None known. No client was run. |
strict: true on a function tool |
Arguments that break the schema come back as a normal call. | OpenAI schema: "the model will follow the exact schema defined in the parameters field." | #3758 is about json_schema, not tools. #4222 and #4754 touch nearby code; I found no check of strict in their diffs. |
None known. Grammar work (#4113, #3862) may make this moot. |
Legacy functions and function_call, chat |
The functions never reach the prompt. The reply is stop with no call and no error. A role: function message in history gets a clear 400. |
OpenAI schema: "Deprecated in favor of tools", and still listed. | No thread found | None known. The fields are deprecated. |
Anthropic prefill (a last assistant message) |
The assistant turn is closed and a new one starts. The status is 200. | Anthropic SDK: the response "will continue directly from that last turn". Anthropic's errors page says newer models return a 400 instead. So either is documented, and today's result is neither. | #1950 and #1966, #822, #3356, all closed. The closing note on #1966 says it is not a rejection. | None run. |
Typed and server tools (web_search_20250305, bash_20250124 and others) |
Sent to the model with an empty schema: {"name": "web_search", "description": "", "parameters": {}}. A call comes back as ordinary tool_use. |
Anthropic: server tools run on the server. Client tools carry an input_schema. |
No thread found | Not checked. Claude Code's WebSearch was not run. |
tool_result.is_error: true |
The prompt is identical to a success. | Anthropic: "Set to true if the tool execution resulted in an error." |
No thread found | Not checked which clients send it. It matters most for empty or unclear error text. |
thinking.display: "omitted" |
The thinking text is still returned. | Anthropic: "no thinking text is streamed." | No thread found | None known. No client was run. |
thinking.type: "between_tools" |
The prompt still opens <think>, as with adaptive. |
Anthropic: it "turns off up-front thinking." | No thread found | Not checked whether Claude Code sends this to another base URL. |
top_logprobs and include: ["message.output_text.logprobs"], /v1/responses |
logprobs is [], while chat returns entries from the same engine. |
OpenAI SDK lists both. | #3200 (closed) is chat. #4056 is /v1/completions. |
None known. No client was run. |
jsonschema is not in requirements.txt or packaging/locks/ |
Without it, a json_schema answer of the wrong type gets a 200. An earlier run with jsonschema 4.26 on the path gave the documented 502. |
docs/DETAILS.md:525 says "jsonschema is used when installed" and documents the fallback. It does not say the locks leave it out. |
#1805, closed in the tracker reset, which says that is not a rejection. #3758 is related. | None known. I did not run a packaged release. |
Rows where I looked for a client and did not find one
These were checked more closely. In each case the behaviour is real, but I found no reason to send a PR.
| Field | What happens (mock engine) | What the source says | Existing thread | Why it may not matter |
|---|---|---|---|---|
tool_choice: "none", /v1/responses |
If the model writes a call anyway, a function_call comes back. Chat returns text for the same reply. |
OpenAI schema: "none means the model will not call any tool and instead generates a message." |
No report. #1788, #3075, #3126 are chat or closed. #4622, #4710 are the other direction. | Codex, the Responses client strata connect sets up, always sends auto. The client would only get the raw call as text, not a usable answer. The 0.2.2 notes left this route alone because of Codex's compaction, which is a related but different case. |
/v1/messages/count_tokens with tool_choice: none |
Count 1391, while the real request reads 246. | The docstring promises "the prompt this server would read". Anthropic's reference counts "messages, system prompt, and tools" with no exception. | #278 (closed) | Claude Code's count calls send no tool_choice (from its bundle). Anthropic's text supports today's count, so the docstring could change instead. An over-count cannot cause an overflow. |
n on chat |
n=3 gives one choice. n=0 gives a 200. |
OpenAI schema: "How many chat completion choices to generate", from 1 to 128. | No thread found | Nothing in this repo sends n above 1. SillyTavern can, by an opt-in setting, and from its source one choice still works, so a 400 would break it. It is also one of many unread fields. |
created and owned_by on /v1/models |
Both are missing. | OpenAI schema requires both. | #3500 (detail route only), #4100, #4370 | The Python SDK returns None and does not raise. The #4100 reporter says a patch without them satisfied Visual Studio. async-openai requires both, but that is from its source; it was not run and no app using it was found. If you want them, a comment on #3500 fits better than a new PR. |
input_tokens_details.cache_write_tokens, /v1/responses |
Missing. | OpenAI schema and SDK require it. | No thread on this route. #2290 and #2289 are the Anthropic field. | Only an explicit model_validate fails. Codex and openai-agents default it to 0 (from their source). No strict gateway was tested. |
POST /v1/responses/input_tokens |
404 "this server keeps no responses". The reason is wrong, because the call stores nothing. | OpenAI documents the route. | No thread on this route. #278 (closed) is Anthropic's. | Codex does not call it. agno catches the error, and nobody has reported Haystack's counter against Strata (from their source). |
HEAD |
501 with an HTML page. | RFC 9110 requires it for a general-purpose server. | No thread found | GNU wget --spider falls back to GET (ran). Strata's healthcheck and docs use GET. Claude Code ignores the result of its one HEAD. Only curl -fI fails. No proxy or uptime monitor was tested. The fix touches about 15 write sites. |
DELETE /v1/responses/{id} |
501 with an HTML page. The SDK retries, then raises InternalServerError. |
docs/DETAILS.md says delete is "refused with an error that says so". |
No thread found | The call fails either way, and every Strata response says "store": false. A code search found no use in Codex or the Agents SDKs, though that search is weak. If any row here is worth doing, it is this one, because the docs promise it. |
stream: true on /v1/images/generations |
200 with plain JSON. The OpenAI SDK's stream gives 0 events and no error (earlier run). | OpenAI says stream is only supported for the GPT image models, so nothing requires it here. |
No thread found | The repo's only caller does not send it. Third-party clients were not surveyed. Pictures are opt-in and need a source build. A 400 would break a raw client that gets a usable image today. |
Not in this list. Two notes on the /v1/completions route in #3332, its streamed usage and array prompts, belong on that PR.
What I am asking. Pick any rows you want, and I will send one focused PR per problem. Otherwise, close this.
Smallest steps or command that reproduce it
Needs a Python with the repo's dependencies. It talks only to a local mock server. Save the file in the repo root, then run PYTHONPATH=. python fields_mock.py. For the jsonschema row, also run git grep jsonschema -- requirements.txt packaging/locks, which finds nothing.
fields_mock.py
# usage, from the repo root: PYTHONPATH=. python fields_mock.py (mock engine only; no model, no GPU)
import http.client, json, sys, tempfile
from pathlib import Path
from serve.frontend import ChatTemplate
from serve.server import ByteTokenizer, MockEngine, Service, serve
from serve.test_logprobs import LpEngine
import serve.images as images_api, serve.test_images as TI
ONE = "</think>\n\n<tool_call>\n<function=a>\n<parameter=x>\nnot-int\n</parameter>\n</function>\n</tool_call>"
TWO = ONE + "\n<tool_call>\n<function=b>\n<parameter=y>\n2\n</parameter>\n</function>\n</tool_call>"
P = {"type": "object", "properties": {"x": {"type": "integer"}}, "required": ["x"]}
OAI = [{"type": "function", "function": {"name": n, "parameters": P}} for n in "ab"]
RSP = [{"type": "function", "name": n, "parameters": P} for n in "ab"]
ANT = [{"name": n, "input_schema": P} for n in "ab"]
U = [{"role": "user", "content": "hi"}]
def up(script="</think>\n\nanswer", cls=MockEngine):
tok = ByteTokenizer()
eng = cls(tok, script) if cls is LpEngine else cls(tok, script, max_context=131072)
svc = Service(eng, tok, ChatTemplate(Path("serve/chat_template.jinja")))
return svc, eng, tok, serve(svc, port=0).server_address[1]
def call(port, path, body=None, method="POST"):
c = http.client.HTTPConnection("127.0.0.1", port, timeout=30)
c.request(method, path, None if body is None else json.dumps(dict(model="m", **body)), {"Content-Type": "application/json"})
r = c.getresponse(); raw = r.read().decode(errors="replace")
try: return r.status, r.getheader("Content-Type"), json.loads(raw)
except ValueError: return r.status, r.getheader("Content-Type"), raw
def chat(port, **kw): return call(port, "/v1/chat/completions", dict(messages=U, **kw))[2]["choices"]
def names(c): return [t["function"]["name"] for t in c[0]["message"].get("tool_calls") or []]
def anth(port, **kw): return call(port, "/v1/messages", dict(dict(messages=U, max_tokens=300), **kw))[2]
def rsp(port, **kw): return call(port, "/v1/responses", dict(input="hi", **kw))[2]
_, e, t, p = up(TWO)
print("parallel_tool_calls false, chat:", names(chat(p, tools=OAI, parallel_tool_calls=False)))
print("parallel_tool_calls false, responses:", [o["name"] for o in rsp(p, tools=RSP, parallel_tool_calls=False)["output"]])
print("disable_parallel_tool_use:", [b["name"] for b in anth(p, tools=ANT, tool_choice={"type": "auto", "disable_parallel_tool_use": True})["content"]])
print("allowed_tools [b]:", names(chat(p, tools=OAI, tool_choice={"type": "allowed_tools", "allowed_tools": {"mode": "auto", "tools": [{"type": "function", "function": {"name": "b"}}]}})))
print("n=3 choices:", len(chat(p, n=3)), "| n=0 status:", call(p, "/v1/chat/completions", dict(messages=U, n=0))[0])
_, e, t, p = up(ONE)
strict = [{"type": "function", "function": {"name": "a", "strict": True, "parameters": P}}]
print("strict tool args:", chat(p, tools=strict)[0]["message"]["tool_calls"][0]["function"]["arguments"])
r = chat(p, functions=[{"name": "a", "parameters": P}], function_call={"name": "a"})[0]
print("functions + function_call:", r["finish_reason"], "| function_call:", r["message"].get("function_call"), "| tool_calls:", r["message"].get("tool_calls"), "| in prompt:", '"a"' in t.decode(e.last_prompt))
print("tool_choice none, responses:", [o["type"] for o in rsp(p, tools=RSP, tool_choice="none")["output"]])
print("tool_choice none, chat:", chat(p, tools=OAI, tool_choice="none")[0]["finish_reason"])
_, e, t, p = up("secret</think>\n\nanswer")
anth(p, messages=[U[0], {"role": "assistant", "content": "The answer is ("}])
print("prefill, prompt ends:", repr(t.decode(e.last_prompt)[-48:]))
anth(p, tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}])
print("typed tool, in prompt:", '{"name": "web_search", "description": "", "parameters": {}}' in t.decode(e.last_prompt))
hist = lambda **kw: [U[0], {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "a", "input": {}}]},
{"role": "user", "content": [dict(type="tool_result", tool_use_id="t1", content="boom", **kw)]}]
anth(p, tools=ANT, messages=hist(is_error=True)); bad = t.decode(e.last_prompt)
anth(p, tools=ANT, messages=hist()); print("is_error, same prompt as success:", bad == t.decode(e.last_prompt))
print("thinking display omitted:", anth(p, thinking={"type": "enabled", "budget_tokens": 2000, "display": "omitted"})["content"][0])
anth(p, thinking={"type": "between_tools"}); print("between_tools, prompt ends:", repr(t.decode(e.last_prompt)[-10:]))
s = call(p, "/v1/messages/count_tokens", dict(messages=U, tools=ANT, tool_choice={"type": "none"}))[2]["input_tokens"]
u = anth(p, tools=ANT, tool_choice={"type": "none"})["usage"]
print("count_tokens none:", s, "| usage none:", u["input_tokens"] + u.get("cache_creation_input_tokens", 0) + u.get("cache_read_input_tokens", 0))
print("/v1/models entry:", call(p, "/v1/models", method="GET")[2]["data"][0].keys())
print("responses usage details:", rsp(p)["usage"]["input_tokens_details"])
print("input_tokens:", call(p, "/v1/responses/input_tokens", dict(input="hi"))[0::2])
print("HEAD /health:", call(p, "/health", method="HEAD")[:2])
print("DELETE /v1/responses/x:", call(p, "/v1/responses/x", method="DELETE")[:2])
_, e, t, p = up("</think>\n\nanswer", LpEngine)
print("responses logprobs:", [c.get("logprobs") for o in rsp(p, top_logprobs=2, include=["message.output_text.logprobs"])["output"] if o["type"] == "message" for c in o["content"]])
print("chat logprobs entries:", len(chat(p, logprobs=True, top_logprobs=2)[0]["logprobs"]["content"]))
_, e, t, p = up('</think>\n\n{"a":"bad"}')
fmt = {"type": "json_schema", "json_schema": {"name": "s", "strict": True, "schema": {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]}}}
print("json_schema answer of the wrong type:", call(p, "/v1/chat/completions", dict(messages=U, response_format=fmt))[0])
svc, e, t, p = up()
real = images_api.sd_cli_args
images_api.sd_cli_args = lambda cfg, job, out: [sys.executable, str(TI.FAKE), *real(cfg, job, out)[1:]]
svc.images = images_api.ImageGen(TI.image_cfg(tempfile.mkdtemp(), keep_files=3))
r = call(p, "/v1/images/generations", dict(prompt="a fox", stream=True))
print("images stream:", r[0], r[1], list(r[2]) if isinstance(r[2], dict) else r[2][:80])I ran it with the file outside the repo. The image row uses the fake sd-cli from serve/test_images.py.
Reproduction status
Reproduced locally
With the mock engine only. Not reproduced on a real engine or GPU. The notes on what clients send or accept are unconfirmed static analysis, unless a row says "ran".
Strata version
Git commit c62d5e3 (0.2.9, main).
Graphics card, memory and system
None used (mock engine). Linux 7.2.9, x86-64.
Model, size and settings
MockEngine with ByteTokenizer and scripted replies, or LpEngine from serve/test_logprobs.py for the logprobs rows. jsonschema was not installed.
Logs
parallel_tool_calls false, chat: ['a', 'b']
parallel_tool_calls false, responses: ['a', 'b']
disable_parallel_tool_use: ['a', 'b']
allowed_tools [b]: ['a', 'b']
n=3 choices: 1 | n=0 status: 200
strict tool args: {"x":"not-int"}
functions + function_call: stop | function_call: None | tool_calls: None | in prompt: False
tool_choice none, responses: ['function_call']
tool_choice none, chat: stop
prefill, prompt ends: 'er is (<|im_end|>\n<|im_start|>assistant\n<think>\n'
typed tool, in prompt: True
is_error, same prompt as success: True
thinking display omitted: {'type': 'thinking', 'thinking': 'secret', 'signature': ''}
between_tools, prompt ends: 't\n<think>\n'
count_tokens none: 1391 | usage none: 246
/v1/models entry: dict_keys(['id', 'object', 'status', 'meta', 'architecture'])
responses usage details: {'cached_tokens': 0}
input_tokens: (404, {'error': {'message': 'this server keeps no responses (stateless); send the whole conversation to POST /v1/responses', 'type': 'invalid_request_error', 'param': None, 'code': 'not_found'}})
HEAD /health: (501, 'text/html;charset=utf-8')
DELETE /v1/responses/x: (501, 'text/html;charset=utf-8')
responses logprobs: [[]]
chat logprobs entries: 6
json_schema answer of the wrong type: 200
images stream: 200 application/json ['created', 'data', 'model', 'output_format', 'size', 'strata']
Written with an AI agent (Claude Code), which also ran the steps above.
🤖 Generated with Claude Code
Activity