Switch LLM provider to z.ai GLM
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# LLM Trade Bot
|
# LLM Trade Bot
|
||||||
|
|
||||||
Автономный DeepSeek-агент для выделенного **Bybit Demo Trading** либо **Binance Testnet** аккаунта. Один процесс работает только с одной биржей, выбранной в `config.toml`; одновременная торговля на обеих не поддерживается. Бот анализирует Spot и `linear` USDT-рынки два раза в день, умеет искать рыночный контекст в интернете и управляется через Telegram.
|
Автономный GLM-агент через **z.ai** для выделенного **Bybit Demo Trading** либо **Binance Testnet** аккаунта. Один процесс работает только с одной биржей, выбранной в `config.toml`; одновременная торговля на обеих не поддерживается. Бот анализирует Spot и `linear` USDT-рынки два раза в день, умеет искать рыночный контекст в интернете и управляется через Telegram.
|
||||||
|
|
||||||
`linear` — публичное имя USDⓈ-M Futures: оно намеренно сохранено в инструментах агента и промптах. Production endpoints не поддерживаются. Для Binance адреса Spot Testnet и USDⓈ-M Futures Demo Testnet зафиксированы в коде и не настраиваются через TOML.
|
`linear` — публичное имя USDⓈ-M Futures: оно намеренно сохранено в инструментах агента и промптах. Production endpoints не поддерживаются. Для Binance адреса Spot Testnet и USDⓈ-M Futures Demo Testnet зафиксированы в коде и не настраиваются через TOML.
|
||||||
|
|
||||||
@@ -24,10 +24,12 @@
|
|||||||
2. Для Bybit создайте ключ в основном аккаунте после переключения в **Demo Trading**, а не на testnet. Настройте Unified account в `ISOLATED_MARGIN` и one-way position mode.
|
2. Для Bybit создайте ключ в основном аккаунте после переключения в **Demo Trading**, а не на testnet. Настройте Unified account в `ISOLATED_MARGIN` и one-way position mode.
|
||||||
3. Для Binance создайте два отдельных набора ключей: Spot Testnet на [testnet.binance.vision](https://testnet.binance.vision/) и USDⓈ-M Futures Testnet/Demo. У ключей должны быть права чтения и торговли. Futures-аккаунт должен использовать one-way mode; бот проверит это при запуске и выставит isolated margin/плечо перед входом.
|
3. Для Binance создайте два отдельных набора ключей: Spot Testnet на [testnet.binance.vision](https://testnet.binance.vision/) и USDⓈ-M Futures Testnet/Demo. У ключей должны быть права чтения и торговли. Futures-аккаунт должен использовать one-way mode; бот проверит это при запуске и выставит isolated margin/плечо перед входом.
|
||||||
4. Создайте Telegram-бота и получите `api_id`/`api_hash` приложения Telegram.
|
4. Создайте Telegram-бота и получите `api_id`/`api_hash` приложения Telegram.
|
||||||
5. Получите DeepSeek API key и Brave Search API key.
|
5. Получите z.ai API key и Brave Search API key.
|
||||||
6. Скопируйте `config.example.toml` в `config.toml`, а `prompts.example.md` в `prompts.md`.
|
6. Скопируйте `config.example.toml` в `config.toml`, а `prompts.example.md` в `prompts.md`.
|
||||||
7. Замените секреты только выбранного провайдера. Локальные конфиги исключены из Git.
|
7. Замените секреты только выбранного провайдера. Локальные конфиги исключены из Git.
|
||||||
|
|
||||||
|
Для GLM используйте раздел `[zai]`: базовый URL `https://api.z.ai/api/paas/v4` и модель `glm-4.6` уже заданы в примере. Ключ создаётся в z.ai; раздел `[deepseek]` больше не поддерживается.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Copy-Item config.example.toml config.toml
|
Copy-Item config.example.toml config.toml
|
||||||
Copy-Item prompts.example.md prompts.md
|
Copy-Item prompts.example.md prompts.md
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[deepseek]
|
[zai]
|
||||||
api_key = "replace-me"
|
api_key = "replace-me"
|
||||||
base_url = "https://api.deepseek.com"
|
base_url = "https://api.z.ai/api/paas/v4"
|
||||||
model = "deepseek-flash"
|
model = "glm-4.6"
|
||||||
thinking = true
|
thinking = true
|
||||||
max_tool_rounds = 256
|
max_tool_rounds = 256
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "llm-trade-bot"
|
name = "llm-trade-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Autonomous DeepSeek trading agent for Bybit Demo"
|
description = "Autonomous GLM trading agent for Bybit Demo"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Azimkin", email = "112006710+Azimkin@users.noreply.github.com" }
|
{ name = "Azimkin", email = "112006710+Azimkin@users.noreply.github.com" }
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from micro_agents import (
|
|||||||
Injected,
|
Injected,
|
||||||
LlmMessage,
|
LlmMessage,
|
||||||
MessageAdded,
|
MessageAdded,
|
||||||
OpenAIBasedLLM,
|
|
||||||
SessionContext,
|
SessionContext,
|
||||||
Tool,
|
Tool,
|
||||||
ToolCallFailed,
|
ToolCallFailed,
|
||||||
@@ -31,6 +30,7 @@ from llm_trade_bot.models import Category, OpenTradeRequest, OrderType, Side
|
|||||||
from llm_trade_bot.prompts import PromptStore
|
from llm_trade_bot.prompts import PromptStore
|
||||||
from llm_trade_bot.risk import RiskEngine
|
from llm_trade_bot.risk import RiskEngine
|
||||||
from llm_trade_bot.web_tools import SafeWebTools
|
from llm_trade_bot.web_tools import SafeWebTools
|
||||||
|
from llm_trade_bot.zai_llm import ZaiLLM
|
||||||
|
|
||||||
|
|
||||||
class AgentTools:
|
class AgentTools:
|
||||||
@@ -353,18 +353,18 @@ class AgentRunner:
|
|||||||
self.exchange = exchange
|
self.exchange = exchange
|
||||||
self.bybit = exchange
|
self.bybit = exchange
|
||||||
self.prompts = PromptStore(config.prompts.file_path)
|
self.prompts = PromptStore(config.prompts.file_path)
|
||||||
llm = OpenAIBasedLLM(
|
llm = ZaiLLM(
|
||||||
api_url=config.deepseek.base_url,
|
api_url=config.zai.base_url,
|
||||||
api_key=config.deepseek.api_key,
|
api_key=config.zai.api_key,
|
||||||
model=config.deepseek.model,
|
model=config.zai.model,
|
||||||
)
|
)
|
||||||
self.agent_tools = AgentTools(config, database, exchange)
|
self.agent_tools = AgentTools(config, database, exchange)
|
||||||
self.aggregator = Aggregator(
|
self.aggregator = Aggregator(
|
||||||
llm,
|
llm,
|
||||||
ToolManager(self.agent_tools.tools()),
|
ToolManager(self.agent_tools.tools()),
|
||||||
master_prompt=_master_prompt,
|
master_prompt=_master_prompt,
|
||||||
max_tool_rounds=config.deepseek.max_tool_rounds,
|
max_tool_rounds=config.zai.max_tool_rounds,
|
||||||
thinking=config.deepseek.thinking,
|
thinking=config.zai.thinking,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run(self, trigger: str) -> str:
|
async def run(self, trigger: str) -> str:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from llm_trade_bot.service import TradeBotService
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="Autonomous DeepSeek trader for Bybit Demo or Binance Testnet")
|
parser = argparse.ArgumentParser(description="Autonomous GLM trader for Bybit Demo or Binance Testnet")
|
||||||
parser.add_argument("--config", default="config.toml", help="Path to TOML configuration")
|
parser.add_argument("--config", default="config.toml", help="Path to TOML configuration")
|
||||||
parser.add_argument("--log-level", default="INFO", choices=("DEBUG", "INFO", "WARNING", "ERROR"))
|
parser.add_argument("--log-level", default="INFO", choices=("DEBUG", "INFO", "WARNING", "ERROR"))
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ class ConfigError(ValueError):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class DeepSeekConfig:
|
class ZaiConfig:
|
||||||
api_key: str
|
api_key: str
|
||||||
base_url: str = "https://api.deepseek.com"
|
base_url: str = "https://api.z.ai/api/paas/v4"
|
||||||
model: str = "deepseek-flash"
|
model: str = "glm-4.6"
|
||||||
thinking: bool = True
|
thinking: bool = True
|
||||||
max_tool_rounds: int = 16
|
max_tool_rounds: int = 16
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class StorageConfig:
|
|||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
deepseek: DeepSeekConfig
|
zai: ZaiConfig
|
||||||
bybit: BybitConfig | None
|
bybit: BybitConfig | None
|
||||||
telegram: TelegramConfig
|
telegram: TelegramConfig
|
||||||
brave: BraveConfig
|
brave: BraveConfig
|
||||||
@@ -121,7 +121,7 @@ def load_config(path: str | Path) -> AppConfig:
|
|||||||
except tomllib.TOMLDecodeError as error:
|
except tomllib.TOMLDecodeError as error:
|
||||||
raise ConfigError(f"Invalid TOML in {config_path}: {error}") from error
|
raise ConfigError(f"Invalid TOML in {config_path}: {error}") from error
|
||||||
|
|
||||||
deepseek_raw = _section(raw, "deepseek")
|
zai_raw = _section(raw, "zai")
|
||||||
exchange_raw = raw.get("exchange")
|
exchange_raw = raw.get("exchange")
|
||||||
if exchange_raw is None:
|
if exchange_raw is None:
|
||||||
if not isinstance(raw.get("bybit"), dict):
|
if not isinstance(raw.get("bybit"), dict):
|
||||||
@@ -142,15 +142,15 @@ def load_config(path: str | Path) -> AppConfig:
|
|||||||
risk_raw = _section(raw, "risk")
|
risk_raw = _section(raw, "risk")
|
||||||
storage_raw = _section(raw, "storage")
|
storage_raw = _section(raw, "storage")
|
||||||
|
|
||||||
deepseek = DeepSeekConfig(
|
zai = ZaiConfig(
|
||||||
api_key=_secret(deepseek_raw, "api_key"),
|
api_key=_secret(zai_raw, "api_key"),
|
||||||
base_url=_string(deepseek_raw, "base_url", "https://api.deepseek.com").rstrip("/"),
|
base_url=_string(zai_raw, "base_url", "https://api.z.ai/api/paas/v4").rstrip("/"),
|
||||||
model=_string(deepseek_raw, "model", "deepseek-flash"),
|
model=_string(zai_raw, "model", "glm-4.6"),
|
||||||
thinking=_boolean(deepseek_raw, "thinking", True),
|
thinking=_boolean(zai_raw, "thinking", True),
|
||||||
max_tool_rounds=_integer(deepseek_raw, "max_tool_rounds", 16, minimum=1, maximum=256),
|
max_tool_rounds=_integer(zai_raw, "max_tool_rounds", 16, minimum=1, maximum=256),
|
||||||
)
|
)
|
||||||
if deepseek.base_url != "https://api.deepseek.com":
|
if zai.base_url != "https://api.z.ai/api/paas/v4":
|
||||||
raise ConfigError("deepseek.base_url must be https://api.deepseek.com")
|
raise ConfigError("zai.base_url must be https://api.z.ai/api/paas/v4")
|
||||||
|
|
||||||
bybit: BybitConfig | None = None
|
bybit: BybitConfig | None = None
|
||||||
binance: BinanceConfig | None = None
|
binance: BinanceConfig | None = None
|
||||||
@@ -228,7 +228,7 @@ def load_config(path: str | Path) -> AppConfig:
|
|||||||
max_notes=_integer(storage_raw, "max_notes", 50, minimum=1, maximum=1000),
|
max_notes=_integer(storage_raw, "max_notes", 50, minimum=1, maximum=1000),
|
||||||
max_notes_chars=_integer(storage_raw, "max_notes_chars", 20_000, minimum=100, maximum=200_000),
|
max_notes_chars=_integer(storage_raw, "max_notes_chars", 20_000, minimum=100, maximum=200_000),
|
||||||
)
|
)
|
||||||
return AppConfig(deepseek, bybit, telegram, brave, prompts, schedule, scanner, risk, storage, exchange, binance)
|
return AppConfig(zai, bybit, telegram, brave, prompts, schedule, scanner, risk, storage, exchange, binance)
|
||||||
|
|
||||||
|
|
||||||
def _reject_binance_endpoint_overrides(section: dict[str, Any]) -> None:
|
def _reject_binance_endpoint_overrides(section: dict[str, Any]) -> None:
|
||||||
|
|||||||
148
src/llm_trade_bot/zai_llm.py
Normal file
148
src/llm_trade_bot/zai_llm.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""z.ai adapter for the provider-neutral micro-agents interface."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
from micro_agents import OpenAIBasedLLM
|
||||||
|
from micro_agents.conversation import (
|
||||||
|
Conversation,
|
||||||
|
LlmMessage,
|
||||||
|
Message,
|
||||||
|
ReasoningMessage,
|
||||||
|
ToolCallMessage,
|
||||||
|
ToolCallResponseMessage,
|
||||||
|
UserMessage,
|
||||||
|
)
|
||||||
|
import micro_agents.tools as tools_package
|
||||||
|
|
||||||
|
|
||||||
|
class ZaiLLM(OpenAIBasedLLM):
|
||||||
|
"""Use z.ai's OpenAI-compatible Chat Completions endpoint.
|
||||||
|
|
||||||
|
``OpenAIBasedLLM`` in micro-agents targets OpenAI's Responses API, which
|
||||||
|
z.ai does not expose. This adapter retains its tool-schema inference while
|
||||||
|
translating the agent conversation to the Chat Completions format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def ask(
|
||||||
|
self,
|
||||||
|
conversation: Conversation,
|
||||||
|
tools: list[tools_package.Tool],
|
||||||
|
*,
|
||||||
|
instructions: str | None = None,
|
||||||
|
thinking: bool | None = None,
|
||||||
|
) -> list[Message]:
|
||||||
|
extra_body: dict[str, object] | None = None
|
||||||
|
if thinking is not None:
|
||||||
|
extra_body = {"thinking": {"type": "enabled" if thinking else "disabled"}}
|
||||||
|
response = await self._ai.chat.completions.create(
|
||||||
|
model=self._model,
|
||||||
|
messages=self._serialize_conversation(conversation, instructions),
|
||||||
|
tools=self._serialize_chat_tools(tools),
|
||||||
|
stream=False,
|
||||||
|
extra_body=extra_body,
|
||||||
|
)
|
||||||
|
if not response.choices:
|
||||||
|
raise ValueError("z.ai returned no choices")
|
||||||
|
message = response.choices[0].message
|
||||||
|
tool_calls = message.tool_calls or []
|
||||||
|
out: list[Message] = []
|
||||||
|
|
||||||
|
# GLM returns this field when thinking is enabled. Keeping it in the
|
||||||
|
# conversation lets the next call include it with the tool invocation.
|
||||||
|
reasoning = getattr(message, "reasoning_content", None)
|
||||||
|
if isinstance(reasoning, str) and reasoning:
|
||||||
|
out.append(ReasoningMessage(reasoning))
|
||||||
|
|
||||||
|
# A Chat Completions response with tool calls is represented by one
|
||||||
|
# assistant message. The content is normally empty in that case; do
|
||||||
|
# not create a second assistant message before the tool-call message.
|
||||||
|
if not tool_calls and isinstance(message.content, str) and message.content:
|
||||||
|
out.append(LlmMessage(message.content))
|
||||||
|
for call in tool_calls:
|
||||||
|
function = call.function
|
||||||
|
if not isinstance(call.id, str) or not isinstance(function.name, str):
|
||||||
|
raise ValueError("z.ai returned an invalid function call")
|
||||||
|
try:
|
||||||
|
arguments = orjson.loads(function.arguments)
|
||||||
|
except orjson.JSONDecodeError as error:
|
||||||
|
raise ValueError("z.ai returned invalid JSON function arguments") from error
|
||||||
|
if not isinstance(arguments, dict):
|
||||||
|
raise ValueError("z.ai function-call arguments must be an object")
|
||||||
|
out.append(ToolCallMessage(call.id, function.name, arguments))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _serialize_chat_tools(self, tools: list[tools_package.Tool]) -> list[dict[str, object]]:
|
||||||
|
result: list[dict[str, object]] = []
|
||||||
|
for tool in tools:
|
||||||
|
schema = self._serialize_tool(tool)
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": schema["name"],
|
||||||
|
"description": schema["description"],
|
||||||
|
"parameters": schema["parameters"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_conversation(conversation: Conversation, instructions: str | None) -> list[dict[str, Any]]:
|
||||||
|
messages: list[dict[str, Any]] = []
|
||||||
|
if instructions:
|
||||||
|
messages.append({"role": "system", "content": instructions})
|
||||||
|
|
||||||
|
items = conversation.messages
|
||||||
|
index = 0
|
||||||
|
pending_reasoning: str | None = None
|
||||||
|
while index < len(items):
|
||||||
|
item = items[index]
|
||||||
|
if isinstance(item, ReasoningMessage):
|
||||||
|
pending_reasoning = item.text()
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
if isinstance(item, UserMessage):
|
||||||
|
messages.append({"role": "user", "content": item.message()})
|
||||||
|
elif isinstance(item, LlmMessage):
|
||||||
|
assistant: dict[str, Any] = {"role": "assistant", "content": item.message()}
|
||||||
|
if pending_reasoning:
|
||||||
|
assistant["reasoning_content"] = pending_reasoning
|
||||||
|
pending_reasoning = None
|
||||||
|
messages.append(assistant)
|
||||||
|
elif isinstance(item, ToolCallMessage):
|
||||||
|
calls: list[dict[str, object]] = []
|
||||||
|
while index < len(items) and isinstance(items[index], ToolCallMessage):
|
||||||
|
call = items[index]
|
||||||
|
calls.append(
|
||||||
|
{
|
||||||
|
"id": call.unique_id(),
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": call.tool_name(),
|
||||||
|
"arguments": orjson.dumps(call.arguments()).decode("utf-8"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index += 1
|
||||||
|
assistant = {"role": "assistant", "content": None, "tool_calls": calls}
|
||||||
|
if pending_reasoning:
|
||||||
|
assistant["reasoning_content"] = pending_reasoning
|
||||||
|
pending_reasoning = None
|
||||||
|
messages.append(assistant)
|
||||||
|
continue
|
||||||
|
elif isinstance(item, ToolCallResponseMessage):
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": item.unique_id(),
|
||||||
|
"content": item.text(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown type of message {type(item).__name__}")
|
||||||
|
index += 1
|
||||||
|
return messages
|
||||||
@@ -10,7 +10,7 @@ from llm_trade_bot.config import (
|
|||||||
AppConfig,
|
AppConfig,
|
||||||
BraveConfig,
|
BraveConfig,
|
||||||
BybitConfig,
|
BybitConfig,
|
||||||
DeepSeekConfig,
|
ZaiConfig,
|
||||||
PromptsConfig,
|
PromptsConfig,
|
||||||
RiskConfig,
|
RiskConfig,
|
||||||
ScannerConfig,
|
ScannerConfig,
|
||||||
@@ -48,7 +48,7 @@ class FakeBybit:
|
|||||||
|
|
||||||
def make_config(database_path: Path) -> AppConfig:
|
def make_config(database_path: Path) -> AppConfig:
|
||||||
return AppConfig(
|
return AppConfig(
|
||||||
DeepSeekConfig("key"),
|
ZaiConfig("key"),
|
||||||
BybitConfig("key", "secret"),
|
BybitConfig("key", "secret"),
|
||||||
TelegramConfig(1, "hash", "token", frozenset({1})),
|
TelegramConfig(1, "hash", "token", frozenset({1})),
|
||||||
BraveConfig("key"),
|
BraveConfig("key"),
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import pytest
|
|||||||
from llm_trade_bot.config import ConfigError, load_config
|
from llm_trade_bot.config import ConfigError, load_config
|
||||||
|
|
||||||
CONFIG = """
|
CONFIG = """
|
||||||
[deepseek]
|
[zai]
|
||||||
api_key = "deepseek-key"
|
api_key = "zai-key"
|
||||||
[bybit]
|
[bybit]
|
||||||
api_key = "bybit-key"
|
api_key = "bybit-key"
|
||||||
api_secret = "bybit-secret"
|
api_secret = "bybit-secret"
|
||||||
@@ -51,7 +51,7 @@ def write_config(tmp_path: Path, content: str = CONFIG) -> Path:
|
|||||||
def test_loads_and_resolves_database_path(tmp_path: Path):
|
def test_loads_and_resolves_database_path(tmp_path: Path):
|
||||||
path = write_config(tmp_path)
|
path = write_config(tmp_path)
|
||||||
config = load_config(path)
|
config = load_config(path)
|
||||||
assert config.deepseek.model == "deepseek-flash"
|
assert config.zai.model == "glm-4.6"
|
||||||
assert config.telegram.admin_user_ids == frozenset({42})
|
assert config.telegram.admin_user_ids == frozenset({42})
|
||||||
assert config.storage.database_path == tmp_path / "state.db"
|
assert config.storage.database_path == tmp_path / "state.db"
|
||||||
assert config.prompts.file_path == tmp_path / "prompts.md"
|
assert config.prompts.file_path == tmp_path / "prompts.md"
|
||||||
@@ -63,8 +63,8 @@ def test_rejects_risk_above_hard_limit(tmp_path: Path):
|
|||||||
load_config(path)
|
load_config(path)
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_non_official_deepseek_endpoint(tmp_path: Path):
|
def test_rejects_non_official_zai_endpoint(tmp_path: Path):
|
||||||
changed = CONFIG.replace("api_key = \"deepseek-key\"", 'api_key = "deepseek-key"\nbase_url = "https://example.com"', 1)
|
changed = CONFIG.replace("api_key = \"zai-key\"", 'api_key = "zai-key"\nbase_url = "https://example.com"', 1)
|
||||||
path = write_config(tmp_path, changed)
|
path = write_config(tmp_path, changed)
|
||||||
with pytest.raises(ConfigError, match="base_url"):
|
with pytest.raises(ConfigError, match="base_url"):
|
||||||
load_config(path)
|
load_config(path)
|
||||||
|
|||||||
34
tests/test_zai_llm.py
Normal file
34
tests/test_zai_llm.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from micro_agents.conversation import Conversation, ReasoningMessage, ToolCallMessage, ToolCallResponseMessage, UserMessage
|
||||||
|
|
||||||
|
from llm_trade_bot.zai_llm import ZaiLLM
|
||||||
|
|
||||||
|
|
||||||
|
def test_serializes_glm_tool_round_in_chat_completions_format():
|
||||||
|
conversation = Conversation(
|
||||||
|
[
|
||||||
|
UserMessage("analyse BTC"),
|
||||||
|
ReasoningMessage("I need market data."),
|
||||||
|
ToolCallMessage("call-1", "get_market_data", {"symbol": "BTCUSDT"}),
|
||||||
|
ToolCallResponseMessage("call-1", '{"last_price":"100000"}'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = ZaiLLM._serialize_conversation(conversation, "Follow the risk limits.")
|
||||||
|
|
||||||
|
assert messages == [
|
||||||
|
{"role": "system", "content": "Follow the risk limits."},
|
||||||
|
{"role": "user", "content": "analyse BTC"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"reasoning_content": "I need market data.",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call-1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": "get_market_data", "arguments": '{"symbol":"BTCUSDT"}'},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"role": "tool", "tool_call_id": "call-1", "content": '{"last_price":"100000"}'},
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user