Summary
The OpenAI-compatible provider can emit multiple system messages, including system messages placed after user, assistant, or tool messages.
Some OpenAI-compatible servers and model chat templates require the system message to be the first message and permit at most one system message. For example, a local vLLM server running Qwen3.8 rejects Autolith requests with:
The provider returned HTTP 400.
System message must be at the beginning.
This prevents otherwise working custom OpenAI-compatible providers from being used with Autolith.
Environment
- Autolith: current master / 0.33.8
- Platform: Linux x86_64
- Provider type: custom OpenAI-compatible provider
- Server: local vLLM OpenAI-compatible API
- Model:
qwen3.8-27b
- Endpoint:
http://localhost:18020/v1/chat/completions
The problem occurs with both the Nix invocation and a normally installed Autolith binary.
Provider configuration
(in-package #:autolith)
(register-openai-compatible-provider
:name "local-vllm"
:description "Local Qwen3.8-27B served by vLLM"
:endpoint "http://localhost:18020/v1/chat/completions"
:models-endpoint "http://localhost:18020/v1/models"
:models
'((:name "qwen3.8-27b"
:description "Qwen3.8-27B"
:context-window 262144
:reasoning-efforts ("xhigh" "medium" "low")))
:reasoning-parameter "reasoning_effort")
Steps to reproduce
-
Start a local vLLM server using a model whose chat template requires the system message to be first, such as Qwen3.8.
-
Register the server as a custom OpenAI-compatible provider.
-
Authenticate the provider.
-
Start Autolith:
AUTOLITH_MODEL=qwen3.8-27b autolith
-
Send any ordinary user message.
Actual behavior
The request is rejected by vLLM:
✗ error
The provider returned HTTP 400.
System message must be at the beginning.
The user message itself does not appear to be duplicated. The issue is the ordering and number of system messages.
provider-request-object in src/provider/openai-compatible.lisp currently constructs the message list in approximately this order:
system: main Autolith system prompt
system: optional goal context
user/assistant/tool: projected conversation
system: optional context-delivery content
system: optional compaction instructions
Therefore, ordinary requests can contain a system message after the conversation, and requests with goal context can contain more than one system message before the conversation.
Compaction requests can similarly place the compaction instruction system message after the projected conversation.
Strict chat templates reject these request shapes.
Expected behavior
The OpenAI-compatible provider should support servers that require exactly one leading system message.
All Autolith-controlled system content could be combined in its original logical order:
[
{
"role": "system",
"content": "<main system prompt>\n\n<goal context>\n\n<context delivery or compaction instructions>"
},
{
"role": "user",
"content": "..."
}
]
There should be no additional system messages after index 0.
If changing the behavior globally would affect providers that support interleaved system messages, a provider registration option such as the following could control normalization:
:system-message-policy :single-leading
For a generic OpenAI-compatible provider, making :single-leading the default would likely provide the broadest compatibility.
Confirmed workaround
I added an :around method for provider-request-object in init.lisp that:
- Calls the existing method to construct the complete request.
- Extracts every message whose role is
system.
- Joins their textual contents in their original order.
- Inserts one combined system message at index
0.
- Preserves the relative order of all non-system messages.
After applying that normalization, the same Autolith configuration and vLLM server work correctly.
This confirms that the failure is caused by system-message construction rather than a duplicated user message, authentication, streaming, or the model identifier.
Suggested implementation
Add a request-normalization function for Chat Completions messages, conceptually:
(openai-compatible--normalize-system-messages messages)
It should:
- preserve the order of non-system messages;
- gather all system-message content in encounter order;
- emit at most one system message;
- place that message at index
0;
- handle string content and typed text-content arrays;
- avoid producing an empty system message if there is no system content.
Normalization should happen after all request-local context, goal context, conversation items, and compaction instructions have been assembled, but before the message list is converted to the final JSON vector.
Suggested regression tests
A normal request with context delivery should assert that:
(string= (json-get (aref messages 0) "role") "system")
and:
(zerop
(count "system"
messages
:start 1
:test #'string=
:key (lambda (message)
(json-get message "role"))))
Tests should also verify that:
- the main system prompt remains present;
- goal context remains present;
- context-delivery content remains present;
- compaction instructions remain present during compaction;
- user, assistant, function-call, and tool-result messages retain their relative order;
- a user message is included exactly once.
Summary
The OpenAI-compatible provider can emit multiple
systemmessages, includingsystemmessages placed afteruser,assistant, ortoolmessages.Some OpenAI-compatible servers and model chat templates require the system message to be the first message and permit at most one system message. For example, a local vLLM server running Qwen3.8 rejects Autolith requests with:
This prevents otherwise working custom OpenAI-compatible providers from being used with Autolith.
Environment
qwen3.8-27bhttp://localhost:18020/v1/chat/completionsThe problem occurs with both the Nix invocation and a normally installed Autolith binary.
Provider configuration
Steps to reproduce
Start a local vLLM server using a model whose chat template requires the system message to be first, such as Qwen3.8.
Register the server as a custom OpenAI-compatible provider.
Authenticate the provider.
Start Autolith:
Send any ordinary user message.
Actual behavior
The request is rejected by vLLM:
The user message itself does not appear to be duplicated. The issue is the ordering and number of system messages.
provider-request-objectinsrc/provider/openai-compatible.lispcurrently constructs the message list in approximately this order:Therefore, ordinary requests can contain a system message after the conversation, and requests with goal context can contain more than one system message before the conversation.
Compaction requests can similarly place the compaction instruction system message after the projected conversation.
Strict chat templates reject these request shapes.
Expected behavior
The OpenAI-compatible provider should support servers that require exactly one leading system message.
All Autolith-controlled system content could be combined in its original logical order:
[ { "role": "system", "content": "<main system prompt>\n\n<goal context>\n\n<context delivery or compaction instructions>" }, { "role": "user", "content": "..." } ]There should be no additional
systemmessages after index0.If changing the behavior globally would affect providers that support interleaved system messages, a provider registration option such as the following could control normalization:
For a generic OpenAI-compatible provider, making
:single-leadingthe default would likely provide the broadest compatibility.Confirmed workaround
I added an
:aroundmethod forprovider-request-objectininit.lispthat:system.0.After applying that normalization, the same Autolith configuration and vLLM server work correctly.
This confirms that the failure is caused by system-message construction rather than a duplicated user message, authentication, streaming, or the model identifier.
Suggested implementation
Add a request-normalization function for Chat Completions messages, conceptually:
It should:
0;Normalization should happen after all request-local context, goal context, conversation items, and compaction instructions have been assembled, but before the message list is converted to the final JSON vector.
Suggested regression tests
A normal request with context delivery should assert that:
and:
Tests should also verify that: