core: pass ReasoningContent to assistant messages in process.go

- 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)
This commit is contained in:
root
2026-07-28 17:06:59 +08:00
parent f91b20ee16
commit 51f190e0b6
10 changed files with 263 additions and 89 deletions

View File

@ -2,9 +2,11 @@
# Lua Adapter — LLM Source Adaptation Guide
Each LLM API source corresponds to a Lua script, responsible for request transformation (Go unified format → API format) and response transformation (API format → Go unified format).
> **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.go` and 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.
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
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
@ -19,59 +21,105 @@ adapter.version = "2.0.0"
adapter.endpoint = "/v1/chat/completions" -- API path, appended to base_url
adapter.headers = {} -- Additional HTTP request headers
-- Request transformation: Go → API
-- Request transformation: kernel private format → API format
function adapter.transform_request(raw_json)
-- raw_json: Go's CompletionRequest JSON string
-- raw_json: Kernel's CompletionRequest JSON string (full fields below)
-- Returns: JSON string to send to API
return transformed_json
end
-- Response transformation: API → Go
-- 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
-- Unified format:
-- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] }
-- 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: JSON string after data: in SSE
-- Returns: JSON of { content: "", done: bool }, return "" to skip this chunk
-- 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
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Unified CompletionRequest Format (Go → Adapter)
## Kernel Private CompletionRequest Format (Go → Lua)
```json
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "...", "tool_calls": [...] }
{ "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": [...],
"tool_choice": "auto"
"tools": [ { "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": { ... } } } ],
"tool_choice": "auto",
"disable_thinking": true
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
### Field Reference
## Unified CompletionResponse Format (Adapter → Go)
| 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:
```json
{ "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)
```json
{
"content": "Response content",
"reasoning_content": "Chain of thought",
"finish_reason": "stop",
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
"tool_calls": [
@ -80,7 +128,32 @@ return adapter
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
### 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:
```json
{ "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
@ -94,14 +167,12 @@ return adapter
`http_post(url, body)` — Perform HTTP POST request, returns response body as string
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 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=0 |
| **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 |
@ -109,12 +180,14 @@ return adapter
| **GitHub Models** | `/chat/completions` | `Authorization: Bearer <pat>` | OpenAI compatible |
| **Ollama** | `/api/chat` | None | Different options format |
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Steps to Add a New LLM Source
## Steps to Add a New Source
1. Write a Lua adapter script defining `transform_request` and `transform_response`
2. (Optional) Define `transform_stream_chunk` for streaming support
3. Place the script `<name>.lua` in the `adapters/` subdirectory under the data directory (i.e. `daemon.data_dir/adapters/`)
4. Reference the adapter in configuration: `"adapter": "<name>"` (must match `adapter.name` in the script)
5. No recompilation needed — the VM automatically scans and loads all `.lua` files from that directory at startup
1. Create `<name>.lua` under `internal/lua/adapters/`
2. Script defines `transform_request` and `transform_response`
3. (Optional) Define `transform_stream_chunk` for streaming support
4. Build verification: `go build ./cmd/homed/`
5. Test verification: `go test ./...`
> **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 in `daemon.data_dir/adapters/`.
> If the same adapter name exists in both locations, the custom file takes precedence.

View File

@ -2,9 +2,11 @@
# Lua Adapter — LLM 源适配指南
每个 LLM API 源对应一个 Lua 脚本负责请求转换Go 统一格式 → API 格式和响应转换API 格式 → Go 统一格式)
> **内核内部格式说明**:下文描述的 CompletionRequest / CompletionResponse JSON 格式是内核 LLM 适配器的**私有内部线缆协议**
> 该格式以 Go 结构体定义在 `internal/agent/api/provider.go` 中,**不导出为外部 API**。
> 本文档公开此格式的唯一目的是作为 Lua 适配器脚本的契约标准——用户按照此文档编写 Lua 脚本,即可接入任意 LLM API 源。
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
每个 LLM API 源对应一个 Lua 脚本,负责请求转换(内核私有格式 → API 格式和响应转换API 格式 → 内核私有格式)。
## 适配器契约
@ -19,59 +21,104 @@ adapter.version = "2.0.0"
adapter.endpoint = "/v1/chat/completions" -- API 路径,拼接到 base_url 后
adapter.headers = {} -- 额外 HTTP 请求头
-- 请求转换:Go → API
-- 请求转换:内核私有格式 → API 格式
function adapter.transform_request(raw_json)
-- raw_json: Go 的 CompletionRequest JSON 字符串
-- raw_json: 内核 CompletionRequest JSON 字符串(完整字段见下文)
-- 返回: 应发送给 API 的 JSON 字符串
return transformed_json
end
-- 响应转换API → Go
-- 响应转换API 格式 → 内核私有格式
function adapter.transform_response(raw_json)
-- raw_json: API 返回的原始 JSON 字符串
-- 返回: 统一 CompletionResponse JSON 字符串
-- 统一格式:
-- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] }
-- 返回: 统一 CompletionResponse JSON 字符串(完整格式见下文)
return unified_json
end
-- 流式块转换(可选)
function adapter.transform_stream_chunk(raw_line)
-- raw_line: SSE 中 data: 后的 JSON 字符串
-- 返回: { content: "", done: bool } 的 JSON,返回 "" 表示跳过该 chunk
-- raw_line: SSE 中 data: 后的原始 JSON 字符串
-- 返回: 统一 StreamChunk JSON 字符串(格式见下文),返回 "" 表示跳过该 chunk
return chunk_json
end
return adapter
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 统一 CompletionRequest 格式Go → Adapter
## 内核私有 CompletionRequest 格式Go → Lua
```json
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "...", "tool_calls": [...] }
{ "role": "system", "content": "你是 AI 助手" },
{ "role": "user", "content": "你好" },
{ "role": "assistant", "content": "你好!", "reasoning_content": "思考过程...", "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } } ] },
{ "role": "tool", "tool_call_id": "call_xxx", "content": "天气:晴" }
],
"temperature": 0.7,
"max_tokens": 4096,
"stream": false,
"tools": [...],
"tool_choice": "auto"
"tools": [ { "type": "function", "function": { "name": "get_weather", "description": "...", "parameters": { ... } } } ],
"tool_choice": "auto",
"disable_thinking": true
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
### 字段说明
## 统一 CompletionResponse 格式Adapter → Go
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `model` | string | 否 | 模型名,内核用 adapter 所在源的 BaseConfig.Model 自动填充 |
| `messages` | array | 是 | 对话消息列表(详见下方 Message |
| `temperature` | float | 否 | 采样温度,默认 0.7 |
| `max_tokens` | float | 否 | 最大生成 token 数,默认 4096 |
| `stream` | bool | 否 | 是否流式输出 |
| `tools` | array | 否 | 工具定义列表OpenAI tools 格式) |
| `tool_choice` | string/object | 否 | 工具选择策略,"auto" / "none" / { type: "function", function: { name: "..." } } |
| `disable_thinking` | bool | 否 | 是否禁用 CoT 思考(适用于 DeepSeek-R1 等推理模型) |
**扩展字段**:内核可能会将不在此表中的额外键值对合并到顶层 JSON通过内部 ExtraBody 机制Lua 脚本应当透传或按需处理这些字段。
### Message 对象
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `role` | string | 是 | 角色:`system` / `user` / `assistant` / `tool` |
| `content` | string/array | 否 | 文本内容;多模态时可为 ContentBlock 数组(见下方) |
| `reasoning_content` | string | 否 | 思维链/推理内容(仅 assistant 角色,如有) |
| `tool_call_id` | string | 否 | 工具调用 ID仅 tool 角色,与 assistant 的 tool_calls 对应) |
| `tool_calls` | array | 否 | 工具调用列表(仅 assistant 角色) |
### ContentBlock 对象(多模态消息)
`content` 为数组时,每个元素格式:
```json
{ "type": "text", "text": "描述图片" }
{ "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } }
{ "type": "audio_url", "audio_url": { "url": "https://..." } }
```
### ToolCall 对象CompletionRequest messages 中)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | 是 | 工具调用唯一 ID |
| `type` | string | 是 | 固定为 `"function"` |
| `function` | object | 是 | 内含 `name` (string) 和 `arguments` (JSON 字符串,非对象!) |
**注意**:在 CompletionRequest 的 messages 中tool_calls 使用的是 OpenAI 线缆格式:
`{id, type, function: {name: string, arguments: string}}`,其中 `arguments`**JSON 字符串**(非对象),因为内核序列化时对 ToolCall 做了此转换。
Lua 适配器在 `transform_response` 中返回给内核的 CompletionResponse 则使用**扁平格式**(见下节)。
## 内核私有 CompletionResponse 格式Lua → Go
```json
{
"content": "回复内容",
"reasoning_content": "思维链内容",
"finish_reason": "stop",
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
"tool_calls": [
@ -80,7 +127,32 @@ return adapter
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `content` | string | 是 | 回复文本内容 |
| `reasoning_content` | string | 否 | 思维链内容(如模型返回) |
| `finish_reason` | string | 否 | 结束原因:`"stop"` / `"tool_calls"` / `"length"` 等 |
| `token_usage` | object | 否 | Token 用量,含 `prompt` / `completion` / `total` 三个 int 字段 |
| `tool_calls` | array | 否 | 工具调用列表(扁平格式:`{id, type, name, arguments: {object}}`,与 CompletionRequest 中 messages 的 `function: {name, arguments: string}` 格式不同,请勿混淆) |
## 流式 StreamChunk 格式
Lua 的 `transform_stream_chunk` 应返回以下 JSON
```json
{ "content": "增量文本", "done": false }
{ "content": "", "done": true, "tool_call": { "id": "call_xxx", "type": "function", "name": "get_weather", "arguments": { "city": "北" } } }
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `content` | string | 是 | 本轮增量的文本内容 |
| `done` | bool | 是 | 是否结束 |
| `tool_call` | object | 否 | 工具调用增量(部分调用时 `arguments` 可能为不完整 JSON |
返回空字符串 `""` 表示跳过该 chunk。
## Lua VM 内置函数
@ -94,14 +166,12 @@ return adapter
`http_post(url, body)` — 发起 HTTP POST 请求,返回响应体字符串
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 适配典型 API
| API | endpoint | auth 方式 | 格式差异 |
|---|---|---|---|
| **OpenAI** | `/chat/completions` | `Authorization: Bearer <key>` | 标准 OpenAI 格式 |
| **DeepSeek** | `/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容,强制 temperature=0 |
| **DeepSeek** | `/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容,强制 temperature=1 |
| **Anthropic** | `/v1/messages` | `x-api-key: <key>` | Messages APIsystem 消息分离content 为 block 数组 |
| **Gemini** | `/v1/models/{model}:generateContent` | `?key=<key>` 或 Bearer | contents/parts 格式role 用 model 而非 assistant |
| **Mistral** | `/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容 |
@ -109,12 +179,14 @@ return adapter
| **GitHub Models** | `/chat/completions` | `Authorization: Bearer <pat>` | OpenAI 兼容 |
| **Ollama** | `/api/chat` | 无 | 不同的 options 格式 |
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 添加新 LLM 源步骤
## 添加新源步骤
1. 编写 Lua 适配器脚本,定义 `transform_request``transform_response` 函数
2. (可选)定义 `transform_stream_chunk` 支持流式
3. 将脚本文件 `<name>.lua` 放入数据目录下的 `adapters/` 文件夹中(即 `daemon.data_dir/adapters/`
4. 在配置中引用该适配器:`"adapter": "<name>"`(与脚本中 `adapter.name` 一致)
5. 无需重新编译——VM 启动时自动扫描该目录并加载所有 `.lua` 文件
1. `internal/lua/adapters/` 下创建 `<name>.lua`
2. 脚本定义 `transform_request``transform_response`
3. (可选)定义 `transform_stream_chunk` 支持流式
4. 编译验证:`go build ./cmd/homed/`
5. 测试验证:`go test ./...`
> **注意**:内置适配器存放在 `internal/lua/adapters/` 目录下,编译时嵌入二进制。
> 用户自定义适配器**不需要**放入源码目录,只需放入 `daemon.data_dir/adapters/` 即可。
> 同名适配器:自定义文件优先级高于内置文件。