优化日志
This commit is contained in:
parent
4de6db13f9
commit
3c4a50dc05
10 changed files with 233 additions and 36 deletions
26
README.md
26
README.md
|
|
@ -72,7 +72,8 @@ docker compose up -d
|
||||||
| `PROXY_PORT` | 服务监听端口 | `3029` |
|
| `PROXY_PORT` | 服务监听端口 | `3029` |
|
||||||
| `API_TIMEOUT` | 请求超时(秒) | `300` |
|
| `API_TIMEOUT` | 请求超时(秒) | `300` |
|
||||||
| `ACCESS_API_KEY` | 访问鉴权密钥,留空不启用 | |
|
| `ACCESS_API_KEY` | 访问鉴权密钥,留空不启用 | |
|
||||||
| `DEBUG` | 调试模式,输出详细请求/响应日志 | `false` |
|
| `DEBUG` | 兼容旧版调试开关,开启后等价于 `DEBUG_MODE=simple` | `false` |
|
||||||
|
| `DEBUG_MODE` | 调试模式:`off` / `simple` / `verbose` | `off` |
|
||||||
|
|
||||||
### 模型映射
|
### 模型映射
|
||||||
|
|
||||||
|
|
@ -80,8 +81,9 @@ docker compose up -d
|
||||||
|
|
||||||
- **Cursor 模型名** — 在 Cursor 自定义模型中填入的名称
|
- **Cursor 模型名** — 在 Cursor 自定义模型中填入的名称
|
||||||
- **上游模型名** — 发送到中转站的实际模型名
|
- **上游模型名** — 发送到中转站的实际模型名
|
||||||
- **后端类型** — `openai` (CC 格式) / `anthropic` (Messages 格式) / `responses` (Responses 格式) / `auto` (自动检测)
|
- **后端类型** — `openai` (CC 格式) / `anthropic` (Messages 格式) / `responses` (Responses 格式) / `gemini` (Gemini Contents 格式) / `auto` (自动检测)
|
||||||
- **自定义地址/密钥** — 可选,覆盖全局设置,实现分流到不同中转站
|
- **自定义地址/密钥** — 可选,覆盖全局设置,实现分流到不同中转站
|
||||||
|
- **日志模式** — 可在管理面板全局设置中切换 `off` / `simple` / `verbose`
|
||||||
|
|
||||||
**示例**:在 Cursor 中添加 `claude-sonnet-4-5-20250929`,映射到上游 `gpt-5.3-codex`,后端选 `openai`。Cursor 会用 CC 格式发送请求,代理直接转发到中转站的 `/v1/chat/completions`。
|
**示例**:在 Cursor 中添加 `claude-sonnet-4-5-20250929`,映射到上游 `gpt-5.3-codex`,后端选 `openai`。Cursor 会用 CC 格式发送请求,代理直接转发到中转站的 `/v1/chat/completions`。
|
||||||
|
|
||||||
|
|
@ -89,6 +91,26 @@ docker compose up -d
|
||||||
|
|
||||||
> **提示**:使用 Claude 风格的模型名(如 `claude-sonnet-4-5-20250929`)可以让 Cursor 显示思考过程(thinking)。
|
> **提示**:使用 Claude 风格的模型名(如 `claude-sonnet-4-5-20250929`)可以让 Cursor 显示思考过程(thinking)。
|
||||||
|
|
||||||
|
### 调试日志模式
|
||||||
|
|
||||||
|
项目支持三档调试模式,可通过环境变量 `DEBUG_MODE` 或管理面板全局设置切换:
|
||||||
|
|
||||||
|
- `off` — 关闭调试日志
|
||||||
|
- `simple` — 仅输出控制台调试日志,不写文件
|
||||||
|
- `verbose` — 输出控制台调试日志,并写入详细的对话级文件日志
|
||||||
|
|
||||||
|
详细日志会写入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/conversations/YYYY-MM-DD/{conversation_id}.json
|
||||||
|
```
|
||||||
|
|
||||||
|
特性:
|
||||||
|
- 同一段多轮对话聚合到同一个文件
|
||||||
|
- 自动记录 client request、upstream request/response、client response、错误信息
|
||||||
|
- 流式事件只保留前 12 条和后 12 条,中间部分折叠计数,避免文件膨胀
|
||||||
|
- 流式 `client_response` 只记录 summary,不重复保存完整事件数组
|
||||||
|
|
||||||
### 在 Cursor 中配置
|
### 在 Cursor 中配置
|
||||||
|
|
||||||
1. 打开 Cursor 设置 → Models
|
1. 打开 Cursor 设置 → Models
|
||||||
|
|
|
||||||
16
config.py
16
config.py
|
|
@ -20,5 +20,17 @@ class Config:
|
||||||
API_TIMEOUT = int(os.getenv('API_TIMEOUT', '300'))
|
API_TIMEOUT = int(os.getenv('API_TIMEOUT', '300'))
|
||||||
# 访问鉴权密钥,留空则不启用鉴权
|
# 访问鉴权密钥,留空则不启用鉴权
|
||||||
ACCESS_API_KEY = os.getenv('ACCESS_API_KEY', '')
|
ACCESS_API_KEY = os.getenv('ACCESS_API_KEY', '')
|
||||||
# 调试模式:开启后输出详细的请求/响应日志
|
|
||||||
DEBUG = os.getenv('DEBUG', '').lower() in ('1', 'true', 'yes', 'on')
|
# 调试模式分级:
|
||||||
|
# - off: 关闭调试
|
||||||
|
# - simple: 仅控制台调试日志
|
||||||
|
# - verbose: 控制台调试 + 详细文件日志
|
||||||
|
_debug_mode_raw = os.getenv('DEBUG_MODE', '').strip().lower()
|
||||||
|
_legacy_debug = os.getenv('DEBUG', '').lower() in ('1', 'true', 'yes', 'on')
|
||||||
|
if _debug_mode_raw in ('off', 'simple', 'verbose'):
|
||||||
|
DEBUG_MODE = _debug_mode_raw
|
||||||
|
else:
|
||||||
|
DEBUG_MODE = 'simple' if _legacy_debug else 'off'
|
||||||
|
|
||||||
|
DEBUG = DEBUG_MODE in ('simple', 'verbose')
|
||||||
|
VERBOSE_FILE_LOG = DEBUG_MODE == 'verbose'
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ def get_settings():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'proxy_target_url': s.get('proxy_target_url', ''),
|
'proxy_target_url': s.get('proxy_target_url', ''),
|
||||||
'proxy_api_key': s.get('proxy_api_key', ''),
|
'proxy_api_key': s.get('proxy_api_key', ''),
|
||||||
|
'debug_mode': s.get('debug_mode', '') or Config.DEBUG_MODE,
|
||||||
'env_target_url': Config.PROXY_TARGET_URL,
|
'env_target_url': Config.PROXY_TARGET_URL,
|
||||||
'env_api_key': '***' if Config.PROXY_API_KEY else '',
|
'env_api_key': '***' if Config.PROXY_API_KEY else '',
|
||||||
})
|
})
|
||||||
|
|
@ -99,7 +100,7 @@ def update_settings():
|
||||||
return err
|
return err
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
s = settings.get()
|
s = settings.get()
|
||||||
for key in ('proxy_target_url', 'proxy_api_key'):
|
for key in ('proxy_target_url', 'proxy_api_key', 'debug_mode'):
|
||||||
if key in data:
|
if key in data:
|
||||||
s[key] = data[key]
|
s[key] = data[key]
|
||||||
return _save_and_respond(s, '全局设置已更新')
|
return _save_and_respond(s, '全局设置已更新')
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import settings
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
from adapters.cc_anthropic_adapter import (
|
from adapters.cc_anthropic_adapter import (
|
||||||
|
|
@ -79,7 +80,7 @@ bp = Blueprint('chat', __name__)
|
||||||
|
|
||||||
def _dbg(message: str) -> None:
|
def _dbg(message: str) -> None:
|
||||||
"""仅在调试模式下输出详细日志。"""
|
"""仅在调试模式下输出详细日志。"""
|
||||||
if Config.DEBUG:
|
if settings.get_debug_mode() in ('simple', 'verbose'):
|
||||||
logger.info('[聊天补全调试] %s', message)
|
logger.info('[聊天补全调试] %s', message)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -233,7 +234,7 @@ def _handle_openai_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'chat.completion.stream.summary',
|
'type': 'chat.completion.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'chunks': client_chunks,
|
'chunk_count': len(client_chunks),
|
||||||
'usage': last_usage,
|
'usage': last_usage,
|
||||||
})
|
})
|
||||||
finalize_turn(turn, usage=last_usage)
|
finalize_turn(turn, usage=last_usage)
|
||||||
|
|
@ -274,7 +275,7 @@ def _handle_openai_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'chat.completion.stream.summary',
|
'type': 'chat.completion.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'chunks': client_chunks,
|
'chunk_count': len(client_chunks),
|
||||||
'usage': last_usage,
|
'usage': last_usage,
|
||||||
})
|
})
|
||||||
finalize_turn(turn, usage=last_usage)
|
finalize_turn(turn, usage=last_usage)
|
||||||
|
|
@ -384,7 +385,7 @@ def _handle_responses_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'chat.completion.stream.summary',
|
'type': 'chat.completion.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'chunks': client_chunks,
|
'chunk_count': len(client_chunks),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
@ -486,7 +487,7 @@ def _handle_gemini_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'chat.completion.stream.summary',
|
'type': 'chat.completion.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'chunks': client_chunks,
|
'chunk_count': len(client_chunks),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
@ -599,7 +600,7 @@ def _handle_anthropic_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'chat.completion.stream.summary',
|
'type': 'chat.completion.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'chunks': client_chunks,
|
'chunk_count': len(client_chunks),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ def messages_passthrough():
|
||||||
set_stream_summary(turn, summary)
|
set_stream_summary(turn, summary)
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'messages.stream.summary',
|
'type': 'messages.stream.summary',
|
||||||
'events': client_events,
|
'event_count': len(client_events),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
except req_lib.RequestException as e:
|
except req_lib.RequestException as e:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import settings
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
from adapters.cc_anthropic_adapter import cc_to_messages_request, messages_to_cc_response
|
from adapters.cc_anthropic_adapter import cc_to_messages_request, messages_to_cc_response
|
||||||
|
|
@ -64,7 +65,7 @@ bp = Blueprint('responses', __name__)
|
||||||
|
|
||||||
def _dbg(message: str) -> None:
|
def _dbg(message: str) -> None:
|
||||||
"""仅在调试模式下输出详细日志。"""
|
"""仅在调试模式下输出详细日志。"""
|
||||||
if Config.DEBUG:
|
if settings.get_debug_mode() in ('simple', 'verbose'):
|
||||||
logger.info('[响应生成调试] %s', message)
|
logger.info('[响应生成调试] %s', message)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -203,7 +204,7 @@ def _handle_openai_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'responses.stream.summary',
|
'type': 'responses.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'events': client_events,
|
'event_count': len(client_events),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
return
|
return
|
||||||
|
|
@ -319,7 +320,7 @@ def _handle_responses_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'responses.stream.summary',
|
'type': 'responses.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'events': client_events,
|
'event_count': len(client_events),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
@ -422,7 +423,7 @@ def _handle_gemini_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'responses.stream.summary',
|
'type': 'responses.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'events': client_events,
|
'event_count': len(client_events),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
@ -530,7 +531,7 @@ def _handle_anthropic_stream(
|
||||||
attach_client_response(turn, {
|
attach_client_response(turn, {
|
||||||
'type': 'responses.stream.summary',
|
'type': 'responses.stream.summary',
|
||||||
'model': ctx.client_model,
|
'model': ctx.client_model,
|
||||||
'events': client_events,
|
'event_count': len(client_events),
|
||||||
})
|
})
|
||||||
finalize_turn(turn)
|
finalize_turn(turn)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ _cache = None
|
||||||
_DEFAULTS = {
|
_DEFAULTS = {
|
||||||
'proxy_target_url': '',
|
'proxy_target_url': '',
|
||||||
'proxy_api_key': '',
|
'proxy_api_key': '',
|
||||||
|
'debug_mode': '',
|
||||||
'model_mappings': {},
|
'model_mappings': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +78,12 @@ def get_key():
|
||||||
return get().get('proxy_api_key') or Config.PROXY_API_KEY
|
return get().get('proxy_api_key') or Config.PROXY_API_KEY
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_mode():
|
||||||
|
"""获取当前生效的调试模式,优先使用持久化配置。"""
|
||||||
|
mode = (get().get('debug_mode') or '').strip().lower()
|
||||||
|
return mode if mode in ('off', 'simple', 'verbose') else Config.DEBUG_MODE
|
||||||
|
|
||||||
|
|
||||||
def resolve_model(model_name):
|
def resolve_model(model_name):
|
||||||
"""解析模型映射并返回完整的上游路由信息。"""
|
"""解析模型映射并返回完整的上游路由信息。"""
|
||||||
settings = get()
|
settings = get()
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="hint" id="envKey"></div>
|
<div class="hint" id="envKey"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>日志模式</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<select id="debugMode">
|
||||||
|
<option value="off">关闭</option>
|
||||||
|
<option value="simple">简易日志(仅控制台)</option>
|
||||||
|
<option value="verbose">详细日志(写入文件)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="hint">关闭:不输出调试日志;简易日志:仅控制台输出;详细日志:额外写入对话级文件日志,并对流式事件做采样截断。</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
|
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ async function loadDashboard() {
|
||||||
const s = await api('/api/admin/settings');
|
const s = await api('/api/admin/settings');
|
||||||
document.getElementById('targetUrl').value = s.proxy_target_url || '';
|
document.getElementById('targetUrl').value = s.proxy_target_url || '';
|
||||||
document.getElementById('proxyKey').value = s.proxy_api_key || '';
|
document.getElementById('proxyKey').value = s.proxy_api_key || '';
|
||||||
|
document.getElementById('debugMode').value = s.debug_mode || 'off';
|
||||||
document.getElementById('envUrl').textContent = s.env_target_url ? '环境变量: ' + s.env_target_url : '';
|
document.getElementById('envUrl').textContent = s.env_target_url ? '环境变量: ' + s.env_target_url : '';
|
||||||
document.getElementById('envKey').textContent = s.env_api_key ? '环境变量: (已配置)' : '环境变量: (未设置)';
|
document.getElementById('envKey').textContent = s.env_api_key ? '环境变量: (已配置)' : '环境变量: (未设置)';
|
||||||
await loadMappings();
|
await loadMappings();
|
||||||
|
|
@ -130,6 +131,7 @@ async function saveSettings() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
proxy_target_url: document.getElementById('targetUrl').value.trim(),
|
proxy_target_url: document.getElementById('targetUrl').value.trim(),
|
||||||
proxy_api_key: document.getElementById('proxyKey').value.trim(),
|
proxy_api_key: document.getElementById('proxyKey').value.trim(),
|
||||||
|
debug_mode: document.getElementById('debugMode').value,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
toast('设置已保存');
|
toast('设置已保存');
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""对话级文件日志
|
"""对话级文件日志
|
||||||
|
|
||||||
将同一段多轮对话聚合到一个 JSON 文件中,而不是按单次请求散落成多个文件。
|
将同一段多轮对话聚合到一个 JSON 文件中,而不是按单次请求散落成多个文件。
|
||||||
仅在 DEBUG 开启时记录。
|
仅在详细日志模式开启时记录。
|
||||||
日志目录: data/conversations/YYYY-MM-DD/{conversation_id}.json
|
日志目录: data/conversations/YYYY-MM-DD/{conversation_id}.json
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@ from typing import Any
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from settings import DATA_DIR
|
from settings import DATA_DIR
|
||||||
|
import settings
|
||||||
from utils.http import gen_id
|
from utils.http import gen_id
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -25,6 +26,8 @@ logger = logging.getLogger(__name__)
|
||||||
_LOG_DIR = os.path.join(DATA_DIR, 'conversations')
|
_LOG_DIR = os.path.join(DATA_DIR, 'conversations')
|
||||||
_LOCKS: dict[str, threading.Lock] = {}
|
_LOCKS: dict[str, threading.Lock] = {}
|
||||||
_LOCKS_GUARD = threading.Lock()
|
_LOCKS_GUARD = threading.Lock()
|
||||||
|
_STREAM_KEEP_HEAD = 12
|
||||||
|
_STREAM_KEEP_TAIL = 12
|
||||||
|
|
||||||
|
|
||||||
def start_turn(
|
def start_turn(
|
||||||
|
|
@ -40,7 +43,7 @@ def start_turn(
|
||||||
metadata: dict[str, Any] | None = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""创建一条新的对话 turn 上下文。"""
|
"""创建一条新的对话 turn 上下文。"""
|
||||||
if not Config.DEBUG:
|
if settings.get_debug_mode() != 'verbose':
|
||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.utcnow().isoformat() + 'Z'
|
now = datetime.utcnow().isoformat() + 'Z'
|
||||||
|
|
@ -66,6 +69,10 @@ def start_turn(
|
||||||
'stream_trace': {
|
'stream_trace': {
|
||||||
'upstream_events': [],
|
'upstream_events': [],
|
||||||
'client_events': [],
|
'client_events': [],
|
||||||
|
'upstream_total': 0,
|
||||||
|
'client_total': 0,
|
||||||
|
'upstream_dropped': 0,
|
||||||
|
'client_dropped': 0,
|
||||||
'summary': {},
|
'summary': {},
|
||||||
},
|
},
|
||||||
'error': None,
|
'error': None,
|
||||||
|
|
@ -111,18 +118,18 @@ def attach_client_response(turn: dict[str, Any] | None, response_data: Any) -> N
|
||||||
|
|
||||||
|
|
||||||
def append_upstream_event(turn: dict[str, Any] | None, event: Any) -> None:
|
def append_upstream_event(turn: dict[str, Any] | None, event: Any) -> None:
|
||||||
"""记录一条上游流式事件。"""
|
"""记录一条上游流式事件,超限时截断保留头尾。"""
|
||||||
if turn is None:
|
if turn is None:
|
||||||
return
|
return
|
||||||
turn['stream_trace']['upstream_events'].append(deep_copy_jsonable(event))
|
_append_stream_event(turn['stream_trace'], 'upstream', deep_copy_jsonable(event))
|
||||||
_touch(turn)
|
_touch(turn)
|
||||||
|
|
||||||
|
|
||||||
def append_client_event(turn: dict[str, Any] | None, event: Any) -> None:
|
def append_client_event(turn: dict[str, Any] | None, event: Any) -> None:
|
||||||
"""记录一条返回给客户端的流式事件。"""
|
"""记录一条返回给客户端的流式事件,超限时截断保留头尾。"""
|
||||||
if turn is None:
|
if turn is None:
|
||||||
return
|
return
|
||||||
turn['stream_trace']['client_events'].append(deep_copy_jsonable(event))
|
_append_stream_event(turn['stream_trace'], 'client', deep_copy_jsonable(event))
|
||||||
_touch(turn)
|
_touch(turn)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -149,7 +156,7 @@ def finalize_turn(
|
||||||
duration_ms: int = 0,
|
duration_ms: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""将 turn 追加/更新到对应的会话日志文件。"""
|
"""将 turn 追加/更新到对应的会话日志文件。"""
|
||||||
if turn is None or not Config.DEBUG:
|
if turn is None or settings.get_debug_mode() != 'verbose':
|
||||||
return
|
return
|
||||||
|
|
||||||
turn['updated_at'] = datetime.utcnow().isoformat() + 'Z'
|
turn['updated_at'] = datetime.utcnow().isoformat() + 'Z'
|
||||||
|
|
@ -157,6 +164,15 @@ def finalize_turn(
|
||||||
if usage is not None:
|
if usage is not None:
|
||||||
turn['usage'] = deep_copy_jsonable(usage)
|
turn['usage'] = deep_copy_jsonable(usage)
|
||||||
|
|
||||||
|
stream_trace = turn.get('stream_trace', {})
|
||||||
|
summary = stream_trace.setdefault('summary', {})
|
||||||
|
summary['upstream_total'] = stream_trace.get('upstream_total', 0)
|
||||||
|
summary['client_total'] = stream_trace.get('client_total', 0)
|
||||||
|
summary['upstream_dropped'] = stream_trace.get('upstream_dropped', 0)
|
||||||
|
summary['client_dropped'] = stream_trace.get('client_dropped', 0)
|
||||||
|
if stream_trace.get('upstream_dropped', 0) or stream_trace.get('client_dropped', 0):
|
||||||
|
summary['truncated'] = True
|
||||||
|
|
||||||
threading.Thread(target=_write_turn, args=(deep_copy_jsonable(turn),), daemon=True).start()
|
threading.Thread(target=_write_turn, args=(deep_copy_jsonable(turn),), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -235,6 +251,29 @@ def _get_lock(conversation_id: str) -> threading.Lock:
|
||||||
return _LOCKS[conversation_id]
|
return _LOCKS[conversation_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _append_stream_event(stream_trace: dict[str, Any], kind: str, event: Any) -> None:
|
||||||
|
events_key = f'{kind}_events'
|
||||||
|
total_key = f'{kind}_total'
|
||||||
|
dropped_key = f'{kind}_dropped'
|
||||||
|
|
||||||
|
events = stream_trace.setdefault(events_key, [])
|
||||||
|
stream_trace[total_key] = stream_trace.get(total_key, 0) + 1
|
||||||
|
|
||||||
|
# 前 KEEP_HEAD 条完整保留;之后只保留最后 KEEP_TAIL 条,
|
||||||
|
# 中间部分通过 dropped 计数折叠,避免文件膨胀。
|
||||||
|
if len(events) < (_STREAM_KEEP_HEAD + _STREAM_KEEP_TAIL):
|
||||||
|
events.append(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
head = events[:_STREAM_KEEP_HEAD]
|
||||||
|
tail = events[_STREAM_KEEP_HEAD:]
|
||||||
|
if len(tail) >= _STREAM_KEEP_TAIL:
|
||||||
|
tail.pop(0)
|
||||||
|
stream_trace[dropped_key] = stream_trace.get(dropped_key, 0) + 1
|
||||||
|
tail.append(event)
|
||||||
|
stream_trace[events_key] = head + tail
|
||||||
|
|
||||||
|
|
||||||
def _touch(turn: dict[str, Any] | None) -> None:
|
def _touch(turn: dict[str, Any] | None) -> None:
|
||||||
if turn is None:
|
if turn is None:
|
||||||
return
|
return
|
||||||
|
|
@ -259,25 +298,126 @@ def _pick_explicit_conversation_id(payload: dict[str, Any]) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _conversation_seed(route: str, payload: dict[str, Any]) -> str:
|
def _conversation_seed(route: str, payload: dict[str, Any]) -> str:
|
||||||
|
"""生成稳定的对话种子。
|
||||||
|
|
||||||
|
关键原则:不能直接把整段历史消息都放进 seed,
|
||||||
|
否则每一轮历史增长都会导致 conversation_id 改变,最终每次请求都新建文件。
|
||||||
|
|
||||||
|
这里改为基于“对话根消息”生成种子:
|
||||||
|
- chat/messages: 第一条 user + 第一条 assistant(没有 assistant 时退化为第一条 user)
|
||||||
|
- responses: input 中的第一条 user + 第一条 assistant(没有 assistant 时退化为第一条 user)
|
||||||
|
"""
|
||||||
if route == 'chat':
|
if route == 'chat':
|
||||||
messages = payload.get('messages', [])
|
return 'chat|' + _root_seed_from_messages(payload.get('messages', []))
|
||||||
return 'chat|' + _normalize_messages_seed(messages)
|
|
||||||
|
|
||||||
if route == 'responses':
|
if route == 'responses':
|
||||||
instructions = payload.get('instructions') or ''
|
return 'responses|' + _root_seed_from_responses_input(payload)
|
||||||
input_data = payload.get('input', [])
|
|
||||||
if isinstance(input_data, str):
|
|
||||||
seed_input = input_data
|
|
||||||
else:
|
|
||||||
seed_input = json.dumps(input_data, ensure_ascii=False, default=str)
|
|
||||||
return 'responses|' + instructions + '|' + seed_input
|
|
||||||
|
|
||||||
if route == 'messages':
|
if route == 'messages':
|
||||||
messages = payload.get('messages', [])
|
|
||||||
system = payload.get('system', '')
|
system = payload.get('system', '')
|
||||||
return 'messages|' + str(system) + '|' + json.dumps(messages, ensure_ascii=False, default=str)
|
root = _root_seed_from_messages(payload.get('messages', []))
|
||||||
|
return 'messages|' + str(system) + '|' + root
|
||||||
|
|
||||||
return route + '|' + json.dumps(payload, ensure_ascii=False, default=str)
|
return route + '|' + _pick_explicit_conversation_id(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _root_seed_from_messages(messages: Any) -> str:
|
||||||
|
if not isinstance(messages, list):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
first_user = None
|
||||||
|
first_assistant = None
|
||||||
|
for msg in messages:
|
||||||
|
if not isinstance(msg, dict):
|
||||||
|
continue
|
||||||
|
role = msg.get('role', '')
|
||||||
|
if role in ('system', 'developer'):
|
||||||
|
continue
|
||||||
|
normalized = {
|
||||||
|
'role': role,
|
||||||
|
'content': _normalize_content(msg.get('content')),
|
||||||
|
'tool_call_id': msg.get('tool_call_id', ''),
|
||||||
|
'tool_calls': [
|
||||||
|
{
|
||||||
|
'id': tc.get('id', ''),
|
||||||
|
'name': (tc.get('function') or {}).get('name', ''),
|
||||||
|
}
|
||||||
|
for tc in msg.get('tool_calls', [])
|
||||||
|
if isinstance(tc, dict)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if role == 'user' and first_user is None:
|
||||||
|
first_user = normalized
|
||||||
|
elif role == 'assistant' and first_assistant is None:
|
||||||
|
first_assistant = normalized
|
||||||
|
if first_user is not None and first_assistant is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
seed_parts = []
|
||||||
|
if first_user is not None:
|
||||||
|
seed_parts.append(first_user)
|
||||||
|
if first_assistant is not None:
|
||||||
|
seed_parts.append(first_assistant)
|
||||||
|
return json.dumps(seed_parts, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
def _root_seed_from_responses_input(payload: dict[str, Any]) -> str:
|
||||||
|
instructions = payload.get('instructions') or ''
|
||||||
|
input_data = payload.get('input', [])
|
||||||
|
|
||||||
|
if isinstance(input_data, str):
|
||||||
|
seed_input = input_data
|
||||||
|
elif isinstance(input_data, list):
|
||||||
|
seed_input = _root_seed_from_responses_items(input_data)
|
||||||
|
else:
|
||||||
|
seed_input = json.dumps(input_data, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
return instructions + '|' + seed_input
|
||||||
|
|
||||||
|
|
||||||
|
def _root_seed_from_responses_items(items: list[Any]) -> str:
|
||||||
|
first_user = None
|
||||||
|
first_assistant = None
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
item_type = item.get('type', '')
|
||||||
|
role = item.get('role', '')
|
||||||
|
|
||||||
|
if item_type in ('message', 'input_text', 'output_text'):
|
||||||
|
normalized = {
|
||||||
|
'type': item_type,
|
||||||
|
'role': role,
|
||||||
|
'content': _normalize_content(
|
||||||
|
item.get('content')
|
||||||
|
or item.get('text')
|
||||||
|
or item.get('input_text')
|
||||||
|
or item.get('output_text')
|
||||||
|
or ''
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if role == 'user' and first_user is None:
|
||||||
|
first_user = normalized
|
||||||
|
elif role == 'assistant' and first_assistant is None:
|
||||||
|
first_assistant = normalized
|
||||||
|
|
||||||
|
elif item_type == 'function_call' and first_assistant is None:
|
||||||
|
first_assistant = {
|
||||||
|
'type': 'function_call',
|
||||||
|
'name': item.get('name', ''),
|
||||||
|
'call_id': item.get('call_id', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
if first_user is not None and first_assistant is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
seed_parts = []
|
||||||
|
if first_user is not None:
|
||||||
|
seed_parts.append(first_user)
|
||||||
|
if first_assistant is not None:
|
||||||
|
seed_parts.append(first_assistant)
|
||||||
|
return json.dumps(seed_parts, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
def _normalize_messages_seed(messages: Any) -> str:
|
def _normalize_messages_seed(messages: Any) -> str:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue