- process.go: attach resp.ReasoningContent when building assistant messages - deepseek.lua v2.1.0: remove last_reasoning closure hack; rely on core-provided reasoning_content in messages - openai.lua: strip reasoning_content from messages in transform_request (not supported by OpenAI API)
9.0 KiB
中文 | English
Lua Adapter — LLM Source Adaptation Guide
Kernel Internal Format Notice: The CompletionRequest / CompletionResponse JSON formats described below are the kernel LLM adapter's private internal wire protocol. These types are defined as Go structs in
internal/agent/api/provider.goand are not exported as an external API. This document publicly describes this format solely as the contract standard for Lua adapter scripts — users follow this documentation to write Lua scripts that integrate any LLM API source.
Each LLM API source corresponds to a Lua script, responsible for request transformation (kernel private format → API format) and response transformation (API format → kernel private format).
Adapter Contract
The Lua script must return a table containing the following fields and functions:
local adapter = {}
-- Metadata
adapter.name = "my_provider" -- Unique identifier, matches adapter field in config
adapter.version = "2.0.0"
adapter.endpoint = "/v1/chat/completions" -- API path, appended to base_url
adapter.headers = {} -- Additional HTTP request headers
-- Request transformation: kernel private format → API format
function adapter.transform_request(raw_json)
-- raw_json: Kernel's CompletionRequest JSON string (full fields below)
-- Returns: JSON string to send to API
return transformed_json
end
-- Response transformation: API format → kernel private format
function adapter.transform_response(raw_json)
-- raw_json: API's raw response JSON string
-- Returns: Unified CompletionResponse JSON string (full format below)
return unified_json
end
-- Stream chunk transformation (optional)
function adapter.transform_stream_chunk(raw_line)
-- raw_line: Raw JSON string after data: in SSE
-- Returns: Unified StreamChunk JSON string (format below), return "" to skip this chunk
return chunk_json
end
return adapter
Kernel Private CompletionRequest Format (Go → Lua)
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "You are an AI assistant" },
{ "role": "user", "content": "Hello" },
{ "role": "assistant", "content": "Hi!", "reasoning_content": "thinking...", "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"Beijing\"}" } } ] },
{ "role": "tool", "tool_call_id": "call_xxx", "content": "Weather: sunny" }
],
"temperature": 0.7,
"max_tokens": 4096,
"stream": false,
"tools": [ { "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": { ... } } } ],
"tool_choice": "auto",
"disable_thinking": true
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | no | Model name, auto-filled by kernel from BaseConfig.Model |
messages |
array | yes | Message list (see Message below) |
temperature |
float | no | Sampling temperature, default 0.7 |
max_tokens |
int | no | Max generated tokens, default 4096 |
stream |
bool | no | Whether to stream output |
tools |
array | no | Tool definitions (OpenAI tools format) |
tool_choice |
string/object | no | Tool selection strategy: "auto" / "none" / { type: "function", function: { name: "..." } } |
disable_thinking |
bool | no | Disable CoT reasoning (for reasoning models like DeepSeek-R1) |
Extra fields: The kernel may merge additional keys into the top-level JSON object (via an internal ExtraBody mechanism not listed in this table). Lua scripts should pass through or handle these fields as needed.
Message Object
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | yes | Role: system / user / assistant / tool |
content |
string/array | no | Text content; for multimodal, can be an array of ContentBlock (see below) |
reasoning_content |
string | no | Chain-of-thought reasoning content (assistant only, if available) |
tool_call_id |
string | no | Tool call ID (tool role only, corresponding to assistant's tool_calls) |
tool_calls |
array | no | Tool call list (assistant role only) |
ContentBlock Object (Multimodal Messages)
When content is an array, each element format:
{ "type": "text", "text": "Describe the image" }
{ "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } }
{ "type": "audio_url", "audio_url": { "url": "https://..." } }
ToolCall Object (in CompletionRequest messages)
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique tool call ID |
type |
string | yes | Always "function" |
function |
object | yes | Contains name (string) and arguments (JSON string, not an object!) |
Note: In CompletionRequest messages, tool_calls use the OpenAI wire format:
{id, type, function: {name: string, arguments: string}} — arguments is a stringified JSON, not an object.
The kernel's Message.MarshalJSON performs this conversion.
In the CompletionResponse (returned by transform_response), tool_calls use a flat format as documented in the next section.
Kernel Private CompletionResponse Format (Lua → Go)
{
"content": "Response content",
"reasoning_content": "Chain of thought",
"finish_reason": "stop",
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
"tool_calls": [
{ "id": "call_xxx", "type": "function", "name": "tool_name", "arguments": { "key": "val" } }
]
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Response text |
reasoning_content |
string | no | Chain-of-thought content (if returned by model) |
finish_reason |
string | no | Finish reason: "stop" / "tool_calls" / "length" etc. |
token_usage |
object | no | Token usage with prompt / completion / total (int) fields |
tool_calls |
array | no | Tool call list in flat format: {id, type, name, arguments: {object}}. This differs from the function:{name, arguments:string} wire format used inside CompletionRequest messages — do not confuse them. |
Stream Chunk Format (StreamChunk)
transform_stream_chunk should return JSON in this format:
{ "content": "delta text", "done": false }
{ "content": "", "done": true, "tool_call": { "id": "call_xxx", "type": "function", "name": "get_weather", "arguments": { "city": "Bei" } } }
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Delta content for this chunk |
done |
bool | yes | Whether stream is finished |
tool_call |
object | no | Tool call delta (arguments may be partial/incomplete JSON) |
Return empty string "" to skip the chunk.
Lua VM Built-in Functions
json.encode(table) — Encode Lua table to JSON string
json.decode(string) — Decode JSON string to Lua table
log(level, message) — Output log (level: info/warn/error)
http_get(url) — Perform HTTP GET request, returns response body as string
http_post(url, body) — Perform HTTP POST request, returns response body as string
Adapting Typical APIs
| API | endpoint | auth method | Format differences |
|---|---|---|---|
| OpenAI | /chat/completions |
Authorization: Bearer <key> |
Standard OpenAI format |
| DeepSeek | /chat/completions |
Authorization: Bearer <key> |
OpenAI compatible, forces temperature=1 |
| Anthropic | /v1/messages |
x-api-key: <key> |
Messages API, system message separated, content as block array |
| Gemini | /v1/models/{model}:generateContent |
?key=<key> or Bearer |
contents/parts format, role uses model instead of assistant |
| Mistral | /v1/chat/completions |
Authorization: Bearer <key> |
OpenAI compatible |
| Groq | /openai/v1/chat/completions |
Authorization: Bearer <key> |
OpenAI compatible |
| GitHub Models | /chat/completions |
Authorization: Bearer <pat> |
OpenAI compatible |
| Ollama | /api/chat |
None | Different options format |
Steps to Add a New LLM Source
- Write a Lua adapter script defining
transform_requestandtransform_response - (Optional) Define
transform_stream_chunkfor streaming support - Place the script
<name>.luain theadapters/subdirectory under the data directory (i.e.daemon.data_dir/adapters/) - Reference the adapter in configuration:
"adapter": "<name>"(must matchadapter.namein the script) - No recompilation needed — the VM automatically scans and loads all
.luafiles from that directory at startup
Note
: Built-in adapters are located in
internal/lua/adapters/and compiled into the binary. User-defined custom adapters do not need to go into the source directory — place them indaemon.data_dir/adapters/. If the same adapter name exists in both locations, the custom file takes precedence.