deeptrade-quant 0.7.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/CHANGELOG.md +56 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/PKG-INFO +1 -1
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/__init__.py +1 -1
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/cli_plugin.py +0 -2
- deeptrade_quant-0.8.1/deeptrade/core/github_fetch.py +122 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/llm_client.py +62 -1
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/plugin_source.py +23 -12
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/registry.py +15 -4
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/pyproject.toml +1 -1
- deeptrade_quant-0.8.1/tests/core/test_github_fetch.py +126 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_llm_client.py +92 -1
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_plugin_source.py +78 -39
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_registry.py +25 -0
- deeptrade_quant-0.7.0/deeptrade/core/github_fetch.py +0 -202
- deeptrade_quant-0.7.0/tests/core/test_github_fetch.py +0 -206
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/.gitignore +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/LICENSE +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/README.md +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/cli.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/cli_config.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/cli_data.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/config.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/config_migrations.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/db.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/dep_installer.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/llm_manager.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/logging_config.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/migrations/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/migrations/core/20260509_001_init.sql +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/migrations/core/20260512_001_drop_legacy_tushare_cache.sql +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/migrations/core/20260515_002_affected_tables.sql +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/migrations/core/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/paths.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/plugin_manager.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/run_status.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/secrets.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/core/tushare_client.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/base.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/errors.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/events.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/llm.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/plugins_api/metadata.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/deeptrade/theme.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/cli/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/cli/test_config_cmd.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/cli/test_plugin_cmd.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/cli/test_routing.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/cli/test_user_facing_strings_are_chinese.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/conftest.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_config.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_config_migrations.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_db.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_llm_manager.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_paths.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_plugin_dependencies.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_plugin_install.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_plugin_security.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_plugin_upgrade.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_secrets.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_tushare_classifier.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_tushare_client.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/core/test_tushare_retry_r1.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/plugins_api/__init__.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/plugins_api/test_api_version_2.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/plugins_api/test_errors.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/plugins_api/test_protocol.py +0 -0
- {deeptrade_quant-0.7.0 → deeptrade_quant-0.8.1}/tests/test_smoke.py +0 -0
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DeepTrade. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and SemVer.
|
|
4
4
|
|
|
5
|
+
## [v0.8.1] — 2026-05-16 — Moonshot reasoning 模型 temperature 兼容性
|
|
6
|
+
|
|
7
|
+
`limit-up-board` 等插件接入 Kimi K2.6(``base_url = https://api.moonshot.cn/v1``)后,**所有** LLM 调用 100% 命中 ``HTTP 400 invalid temperature: only 1 is allowed for this model``。根因:Kimi K2 系列的 thinking / reasoning 变体(与 OpenAI o1/o3、Anthropic Sonnet thinking 同侧设计)在服务端硬约束 ``temperature``——仅接受模型专属的固定值,而插件 ``StageProfile`` 出于复现性给的是 ``0.0 ~ 0.2``。
|
|
8
|
+
|
|
9
|
+
修复职责完全在框架:插件不应感知具体 provider/model 的服务端约束,框架的契约是「插件给一个温度意图,框架在真正发出请求前 sanitize 到目标 provider/model 能接受的取值」。
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ``deeptrade/core/llm_client.py::OpenAICompatTransport``:新增 ``_adjust_temperature(model, temperature) -> float`` 钩子,默认 identity;``chat()`` 在写 kwargs 前调用钩子,并在改写时打一行 ``logger.info`` 便于排查。非 Moonshot 路径完全无感。
|
|
14
|
+
- 新增 ``MoonshotTransport(OpenAICompatTransport)``:``_FORCED_TEMPERATURE`` prefix 表强制 ``kimi-k2-thinking`` / ``kimi-k2.5`` / ``kimi-k2.6`` 到 ``1.0``、``kimi-for-coding`` 到 ``0.6``;fallthrough 走 ``min(temperature, 1.0)`` 兼顾非 reasoning 模型(``moonshot-v1-*`` / ``kimi-k2-instruct-*``)的 ``[0, 1]`` 上限——Pydantic 字段允许到 2.0,超界一样 400。
|
|
15
|
+
- ``_TRANSPORT_BY_BASE_URL`` 新增 ``("api.moonshot.cn", MoonshotTransport)``。substring 匹配自动覆盖 ``api.moonshot.cn`` / ``api.moonshot.cn/v1`` 所有形式;国际站 ``api.moonshot.ai`` 暂未支持,若后续需要追加一行即可。
|
|
16
|
+
|
|
17
|
+
### Why prefix match, not exact / regex
|
|
18
|
+
|
|
19
|
+
Moonshot 命名空间 ``<major>.<minor>[-<dated-revision>]`` 的天然分界就在 prefix。exact 会让 ``kimi-k2.6-1106`` / ``kimi-k2-thinking-128k`` 这类 dated revision 漏网,触发 0day 失败;regex 转义复杂度抬高 review 成本,收益不抵。
|
|
20
|
+
|
|
21
|
+
### Migration notes
|
|
22
|
+
|
|
23
|
+
- 插件零改动。``limit_up_board`` / 其他第三方插件的 ``profiles.py`` 不需要感知该约束。
|
|
24
|
+
- 用户原本在 Kimi reasoning 模型上设的 ``temperature=0.0`` 在改写后会被强制为 ``1.0``——这本来就是服务端唯一允许的取值,不改写就是 100% 失败。
|
|
25
|
+
- ``app.profile`` / ``llm.providers`` 配置无变动。
|
|
26
|
+
|
|
27
|
+
## [v0.8.0] — 2026-05-16 — 插件 install / upgrade 走 CDN,零 GitHub API 调用
|
|
28
|
+
|
|
29
|
+
`deeptrade plugin install` 与 `deeptrade plugin upgrade` 此前在解析"最新版本"与下载 tarball 时各打一次 ``api.github.com``,未认证用户共享 60/h 的 IP 级配额。一旦插件用户数上来,或者用户与浏览器 / `gh` CLI / `git clone` 公共仓库共用同一公网 IP,``HTTP 403: rate limit exceeded`` 就会把 install / upgrade 直接打死。共享 token 会违反 GitHub ToS,且配额仍会在那个 token 上聚合——不是解。
|
|
30
|
+
|
|
31
|
+
本版改造将插件分发热路径全部搬到 CDN 端点:
|
|
32
|
+
|
|
33
|
+
- **Tarball 下载**改走 ``codeload.github.com/<owner>/<repo>/tar.gz/<ref>``。codeload 是 CDN-backed 静态端点,不计入 REST API 限流。
|
|
34
|
+
- **"最新版本"**改从注册表 ``index.json`` 的新字段 ``latest_version`` 读取(``raw.githubusercontent.com`` 同样不限流,且已有 ETag 缓存)。
|
|
35
|
+
- **URL 形式安装**(``deeptrade plugin install https://github.com/...``)无 ``--ref`` 时默认 ``main`` 分支;其他默认分支需显式 ``--ref``。
|
|
36
|
+
|
|
37
|
+
净效果:未认证用户走 ``install`` / ``upgrade`` 全流程**零 API 调用**,60/h 限制对默认路径不再存在。``GITHUB_TOKEN`` 不再被框架读取,文档不再推荐设置。
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- ``deeptrade/core/github_fetch.py``:删除 ``latest_release_tag`` / ``NoMatchingReleaseError``;``fetch_tarball`` 改写为 codeload 单端点(移除 ``GITHUB_TOKEN`` / ``X-GitHub-Api-Version`` header)。
|
|
42
|
+
- ``deeptrade/core/registry.py``:``RegistryEntry`` 新增可选字段 ``latest_version: str | None``;解析器引入 ``_OPTIONAL_FIELDS`` 集合,含字段时填入,缺省 ``None``。schema_version 不变(仍为 1,向后兼容旧注册表文件)。
|
|
43
|
+
- ``deeptrade/core/plugin_source.py``:``_resolve_short_name`` 用 ``entry.latest_version`` 替代 ``latest_release_tag``;缺字段且无 ``--ref`` 时抛 ``SourceResolveError`` 提示需补 ``--ref``。``_resolve_url`` 无 ``--ref`` 默认 ``main``;codeload 失败时错误信息提示用户切换 ``--ref``。
|
|
44
|
+
- ``deeptrade/cli_plugin.py``:移除 ``NoMatchingReleaseError`` 引用。
|
|
45
|
+
|
|
46
|
+
### Removed
|
|
47
|
+
|
|
48
|
+
- ``latest_release_tag`` 公共函数与 ``NoMatchingReleaseError`` 异常类。
|
|
49
|
+
- 对 ``GITHUB_TOKEN`` 环境变量的读取(github_fetch.py 中)。
|
|
50
|
+
|
|
51
|
+
### Migration notes
|
|
52
|
+
|
|
53
|
+
- **注册表维护者**:在 ``DeepTradePluginOfficial/registry/index.json`` 每个 plugin entry 中加 ``latest_version`` 字段,每次插件发版后同步该字段。可由 plugin 仓库 release workflow 自动 PR 到注册表仓库。
|
|
54
|
+
- **存量旧版框架用户**:旧版(v0.7 及以下)仍会调 ``api.github.com``。``latest_version`` 字段是可选的、旧框架读取时会被忽略,不破坏现有用户;旧用户升级到本版后限流问题自动消失。
|
|
55
|
+
- **URL 形式高级用户**:若你 host 的仓库默认分支不是 ``main``,``deeptrade plugin install https://github.com/<your>/<repo>`` 需显式 ``--ref <branch>``。
|
|
56
|
+
|
|
57
|
+
### Why not "use a shared token"
|
|
58
|
+
|
|
59
|
+
GitHub ToS 明确禁止 token 跨用户共享(被发现 token 会被吊销);即便允许,5000/h 也会被规模化使用快速吃掉,且 token 泄露需要维护者承担权限滥用风险。CDN 改造是唯一既不需要每用户认证、又能稳定服务的方案。未来若需要支持私有仓库或企业内部分发,再走 OAuth Device Flow(``gh auth login`` 同款流程)让每个用户认证自己的 token。
|
|
60
|
+
|
|
5
61
|
## [v0.7.0] — 2026-05-15 — 依赖隔离稳健化 + timezone IANA 校验
|
|
6
62
|
|
|
7
63
|
本版按 2026-05-15 评审研判文档 §5 v0.7 路线落地 2 项主线,至此 v0.5 § 5 全部"采纳项"清零:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deeptrade-quant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: LLM-driven A-share (Shanghai/Shenzhen main board) stock screening CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/ty19880929/deeptrade
|
|
6
6
|
Project-URL: Repository, https://github.com/ty19880929/deeptrade
|
|
@@ -12,7 +12,6 @@ from deeptrade.core import paths
|
|
|
12
12
|
from deeptrade.core.db import Database
|
|
13
13
|
from deeptrade.core.github_fetch import (
|
|
14
14
|
GitHubFetchError,
|
|
15
|
-
NoMatchingReleaseError,
|
|
16
15
|
TarballFetchError,
|
|
17
16
|
)
|
|
18
17
|
from deeptrade.core.plugin_manager import (
|
|
@@ -41,7 +40,6 @@ _RESOLVE_ERRORS: tuple[type[Exception], ...] = (
|
|
|
41
40
|
RegistryNotFoundError,
|
|
42
41
|
RegistryFetchError,
|
|
43
42
|
RegistrySchemaError,
|
|
44
|
-
NoMatchingReleaseError,
|
|
45
43
|
TarballFetchError,
|
|
46
44
|
GitHubFetchError,
|
|
47
45
|
SourceResolveError,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""GitHub tarball download helper (CDN-only).
|
|
2
|
+
|
|
3
|
+
The plugin install / upgrade path downloads release tarballs from
|
|
4
|
+
``codeload.github.com`` instead of ``api.github.com``. codeload is a
|
|
5
|
+
CDN-backed static endpoint and does **not** count against the GitHub REST
|
|
6
|
+
API rate limit (60/h for anonymous IPs), so plugin install works for
|
|
7
|
+
every user out of the box — no ``GITHUB_TOKEN`` required.
|
|
8
|
+
|
|
9
|
+
"Latest version" used to be resolved via ``GET /repos/<repo>/releases``,
|
|
10
|
+
but that costs an API request per install. It now comes from the
|
|
11
|
+
``latest_version`` field in the registry index, which is served from
|
|
12
|
+
``raw.githubusercontent.com`` (also CDN, also un-metered). See
|
|
13
|
+
``deeptrade.core.plugin_source`` for the wiring.
|
|
14
|
+
|
|
15
|
+
See ``CHANGELOG.md`` for the distribution / install design context.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import shutil
|
|
22
|
+
import tarfile
|
|
23
|
+
import tempfile
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from urllib.error import HTTPError, URLError
|
|
26
|
+
from urllib.request import Request, urlopen
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
CODELOAD_BASE = "https://codeload.github.com"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GitHubFetchError(Exception):
|
|
34
|
+
"""Generic GitHub fetch error."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TarballFetchError(GitHubFetchError):
|
|
38
|
+
"""Tarball download or extraction failure."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _user_agent() -> str:
|
|
42
|
+
from deeptrade import __version__
|
|
43
|
+
|
|
44
|
+
return f"deeptrade-cli/{__version__}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_request(url: str) -> Request:
|
|
48
|
+
return Request(url, headers={"User-Agent": _user_agent()})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fetch_tarball(repo: str, ref: str, dest_dir: Path, *, timeout: float = 60.0) -> Path:
|
|
52
|
+
"""Download ``repo`` at ``ref`` from codeload.github.com and extract.
|
|
53
|
+
|
|
54
|
+
``ref`` may be a tag (``v1.0.0`` or ``limit-up-board/v0.4.0``), a branch
|
|
55
|
+
name (``main``), a SHA, or a full git ref (``refs/tags/v1.0.0`` /
|
|
56
|
+
``refs/heads/main``). codeload resolves all of these transparently.
|
|
57
|
+
|
|
58
|
+
Returns the unique top-level directory created inside ``dest_dir``
|
|
59
|
+
(codeload tarballs are wrapped in ``<owner>-<repo>-<sha7>/``).
|
|
60
|
+
|
|
61
|
+
Raises :class:`TarballFetchError` on network, HTTP, or extraction failure.
|
|
62
|
+
"""
|
|
63
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
url = f"{CODELOAD_BASE}/{repo}/tar.gz/{ref}"
|
|
65
|
+
|
|
66
|
+
tmp_path: Path | None = None
|
|
67
|
+
try:
|
|
68
|
+
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
|
|
69
|
+
tmp_path = Path(tmp.name)
|
|
70
|
+
|
|
71
|
+
req = _build_request(url)
|
|
72
|
+
try:
|
|
73
|
+
with urlopen(req, timeout=timeout) as resp, tmp_path.open("wb") as fout:
|
|
74
|
+
shutil.copyfileobj(resp, fout)
|
|
75
|
+
except HTTPError as e:
|
|
76
|
+
raise TarballFetchError(
|
|
77
|
+
f"HTTP {e.code} downloading tarball {repo}@{ref} from codeload: {e}"
|
|
78
|
+
) from e
|
|
79
|
+
except URLError as e:
|
|
80
|
+
raise TarballFetchError(
|
|
81
|
+
f"network error downloading tarball {repo}@{ref} from codeload: {e}"
|
|
82
|
+
) from e
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
with tarfile.open(tmp_path, mode="r:gz") as tf:
|
|
86
|
+
_safe_extract(tf, dest_dir)
|
|
87
|
+
except tarfile.TarError as e:
|
|
88
|
+
raise TarballFetchError(f"failed to extract tarball: {e}") from e
|
|
89
|
+
finally:
|
|
90
|
+
if tmp_path is not None:
|
|
91
|
+
try:
|
|
92
|
+
tmp_path.unlink()
|
|
93
|
+
except OSError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
entries = [p for p in dest_dir.iterdir() if p.is_dir()]
|
|
97
|
+
if len(entries) != 1:
|
|
98
|
+
raise TarballFetchError(
|
|
99
|
+
f"expected one top-level dir in tarball, found {len(entries)}: "
|
|
100
|
+
f"{[p.name for p in entries]}"
|
|
101
|
+
)
|
|
102
|
+
return entries[0]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _safe_extract(tf: tarfile.TarFile, dest: Path) -> None:
|
|
106
|
+
"""Extract ``tf`` into ``dest`` with path-traversal protection.
|
|
107
|
+
|
|
108
|
+
Uses tarfile's ``data`` filter on Python 3.12+, with an explicit
|
|
109
|
+
relative-path check on every member as a belt-and-braces guard for 3.11.
|
|
110
|
+
"""
|
|
111
|
+
dest_resolved = dest.resolve()
|
|
112
|
+
for m in tf.getmembers():
|
|
113
|
+
member_path = (dest / m.name).resolve()
|
|
114
|
+
try:
|
|
115
|
+
member_path.relative_to(dest_resolved)
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
raise tarfile.TarError(f"unsafe path in tarball (would escape dest): {m.name!r}") from e
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
tf.extractall(dest, filter="data")
|
|
121
|
+
except TypeError:
|
|
122
|
+
tf.extractall(dest) # noqa: S202 — paths already validated above
|
|
@@ -162,6 +162,16 @@ class OpenAICompatTransport(LLMTransport):
|
|
|
162
162
|
del thinking # base class has no provider knobs
|
|
163
163
|
return {}
|
|
164
164
|
|
|
165
|
+
def _adjust_temperature(self, *, model: str, temperature: float) -> float:
|
|
166
|
+
"""Provider/model-specific temperature sanitization hook.
|
|
167
|
+
|
|
168
|
+
Default: identity. Subclasses override to clamp / force temperature
|
|
169
|
+
for models with server-side hard constraints (e.g. Moonshot reasoning
|
|
170
|
+
variants that only accept ``temperature == 1``).
|
|
171
|
+
"""
|
|
172
|
+
del model # base class has no per-model constraints
|
|
173
|
+
return temperature
|
|
174
|
+
|
|
165
175
|
def chat(
|
|
166
176
|
self,
|
|
167
177
|
*,
|
|
@@ -175,6 +185,15 @@ class OpenAICompatTransport(LLMTransport):
|
|
|
175
185
|
) -> LLMResponse:
|
|
176
186
|
from openai import APIError, APITimeoutError # noqa: PLC0415
|
|
177
187
|
|
|
188
|
+
adjusted_temperature = self._adjust_temperature(model=model, temperature=temperature)
|
|
189
|
+
if adjusted_temperature != temperature:
|
|
190
|
+
logger.info(
|
|
191
|
+
"transport adjusted temperature for model=%s: %.3f -> %.3f",
|
|
192
|
+
model,
|
|
193
|
+
temperature,
|
|
194
|
+
adjusted_temperature,
|
|
195
|
+
)
|
|
196
|
+
|
|
178
197
|
kwargs: dict[str, Any] = {
|
|
179
198
|
"model": model,
|
|
180
199
|
"messages": [
|
|
@@ -182,7 +201,7 @@ class OpenAICompatTransport(LLMTransport):
|
|
|
182
201
|
{"role": "user", "content": user},
|
|
183
202
|
],
|
|
184
203
|
"response_format": {"type": "json_object"},
|
|
185
|
-
"temperature":
|
|
204
|
+
"temperature": adjusted_temperature,
|
|
186
205
|
"max_tokens": max_tokens,
|
|
187
206
|
"stream": False,
|
|
188
207
|
}
|
|
@@ -236,6 +255,47 @@ class DashScopeTransport(OpenAICompatTransport):
|
|
|
236
255
|
return {"enable_thinking": thinking}
|
|
237
256
|
|
|
238
257
|
|
|
258
|
+
class MoonshotTransport(OpenAICompatTransport):
|
|
259
|
+
"""Moonshot Kimi (``api.moonshot.cn``).
|
|
260
|
+
|
|
261
|
+
Reasoning-variant models (``kimi-k2-thinking`` / ``kimi-k2.5`` /
|
|
262
|
+
``kimi-k2.6``) have a server-side hard constraint: ``temperature`` MUST
|
|
263
|
+
equal a model-specific fixed value (1.0 for thinking variants, 0.6 for
|
|
264
|
+
``kimi-for-coding``). Any other value returns HTTP 400 ``invalid
|
|
265
|
+
temperature``.
|
|
266
|
+
|
|
267
|
+
Non-reasoning Moonshot models accept the full ``[0, 1]`` range; values
|
|
268
|
+
above 1 also 400. We handle both: forced equality on known reasoning
|
|
269
|
+
variants, then fall through to range clamp for everyone else.
|
|
270
|
+
|
|
271
|
+
``_FORCED_TEMPERATURE`` uses **prefix** match so that dated revisions
|
|
272
|
+
(``kimi-k2.6-1106``, ``kimi-k2-thinking-128k``, …) inherit the same
|
|
273
|
+
constraint without a code change. Only include models with confirmed
|
|
274
|
+
server-side enforcement, not just "recommended" values.
|
|
275
|
+
|
|
276
|
+
Note: the international site (``api.moonshot.ai``) shares the same
|
|
277
|
+
constraints — add a routing-table entry there if/when the framework
|
|
278
|
+
supports it.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
# model-name prefix → forced temperature value
|
|
282
|
+
_FORCED_TEMPERATURE: tuple[tuple[str, float], ...] = (
|
|
283
|
+
("kimi-k2-thinking", 1.0),
|
|
284
|
+
("kimi-k2.5", 1.0),
|
|
285
|
+
("kimi-k2.6", 1.0),
|
|
286
|
+
("kimi-for-coding", 0.6),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def _adjust_temperature(self, *, model: str, temperature: float) -> float:
|
|
290
|
+
for prefix, forced in self._FORCED_TEMPERATURE:
|
|
291
|
+
if model.startswith(prefix):
|
|
292
|
+
return forced
|
|
293
|
+
# Moonshot accepts only [0, 1] across the whole API; upper-clamp guards
|
|
294
|
+
# non-reasoning models (moonshot-v1-*, kimi-k2-instruct-*) against a
|
|
295
|
+
# StageProfile that goes above 1.0 (the Pydantic field allows up to 2).
|
|
296
|
+
return min(temperature, 1.0)
|
|
297
|
+
|
|
298
|
+
|
|
239
299
|
class OpenAIOfficialTransport(OpenAICompatTransport):
|
|
240
300
|
"""OpenAI's own ``api.openai.com`` endpoint.
|
|
241
301
|
|
|
@@ -259,6 +319,7 @@ class OpenAIOfficialTransport(OpenAICompatTransport):
|
|
|
259
319
|
# nowhere else; user-facing config has no "dialect" knob on purpose.
|
|
260
320
|
_TRANSPORT_BY_BASE_URL: tuple[tuple[str, type[OpenAICompatTransport]], ...] = (
|
|
261
321
|
("dashscope.aliyuncs.com", DashScopeTransport),
|
|
322
|
+
("api.moonshot.cn", MoonshotTransport),
|
|
262
323
|
("api.openai.com", OpenAIOfficialTransport),
|
|
263
324
|
)
|
|
264
325
|
|
|
@@ -28,9 +28,7 @@ from packaging.version import InvalidVersion, Version
|
|
|
28
28
|
|
|
29
29
|
from deeptrade.core.github_fetch import (
|
|
30
30
|
GitHubFetchError,
|
|
31
|
-
NoMatchingReleaseError,
|
|
32
31
|
fetch_tarball,
|
|
33
|
-
latest_release_tag,
|
|
34
32
|
)
|
|
35
33
|
from deeptrade.core.registry import (
|
|
36
34
|
RegistryClient,
|
|
@@ -109,14 +107,20 @@ class SourceResolver:
|
|
|
109
107
|
self._check_framework_version(entry)
|
|
110
108
|
|
|
111
109
|
if ref is None:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
if not entry.latest_version:
|
|
111
|
+
raise SourceResolveError(
|
|
112
|
+
f"registry entry for {plugin_id!r} is missing 'latest_version' and no "
|
|
113
|
+
f"--ref was supplied; pass --ref <tag> explicitly, or wait for the "
|
|
114
|
+
f"registry to publish a latest_version for this plugin"
|
|
115
|
+
)
|
|
116
|
+
ref = entry.latest_version
|
|
116
117
|
|
|
117
118
|
tmp = tempfile.TemporaryDirectory(prefix="deeptrade-plugin-")
|
|
118
119
|
try:
|
|
119
|
-
|
|
120
|
+
try:
|
|
121
|
+
top = fetch_tarball(entry.repo, ref, Path(tmp.name))
|
|
122
|
+
except GitHubFetchError as e:
|
|
123
|
+
raise SourceResolveError(str(e)) from e
|
|
120
124
|
plugin_path = top / entry.subdir
|
|
121
125
|
if not plugin_path.is_dir():
|
|
122
126
|
raise SourceResolveError(f"subdir {entry.subdir!r} not found in {entry.repo}@{ref}")
|
|
@@ -145,15 +149,22 @@ class SourceResolver:
|
|
|
145
149
|
owner, repo_name = _parse_github_url(url)
|
|
146
150
|
repo = f"{owner}/{repo_name}"
|
|
147
151
|
|
|
152
|
+
# URL form has no registry entry to consult, so we cannot know the
|
|
153
|
+
# "latest release tag" without hitting api.github.com (which the v0.8
|
|
154
|
+
# CDN refactor explicitly avoids). Default to the ``main`` branch; if
|
|
155
|
+
# the repo's default branch is something else, the codeload 404 surfaces
|
|
156
|
+
# with a hint to pass ``--ref`` explicitly.
|
|
148
157
|
if ref is None:
|
|
149
|
-
|
|
150
|
-
ref = latest_release_tag(repo, "")
|
|
151
|
-
except (NoMatchingReleaseError, GitHubFetchError) as e:
|
|
152
|
-
raise SourceResolveError(str(e)) from e
|
|
158
|
+
ref = "main"
|
|
153
159
|
|
|
154
160
|
tmp = tempfile.TemporaryDirectory(prefix="deeptrade-plugin-")
|
|
155
161
|
try:
|
|
156
|
-
|
|
162
|
+
try:
|
|
163
|
+
top = fetch_tarball(repo, ref, Path(tmp.name))
|
|
164
|
+
except GitHubFetchError as e:
|
|
165
|
+
raise SourceResolveError(
|
|
166
|
+
f"{e}; if your repo's default branch is not 'main', pass --ref <branch-or-tag>"
|
|
167
|
+
) from e
|
|
157
168
|
yaml_path = top / "deeptrade_plugin.yaml"
|
|
158
169
|
if not yaml_path.is_file():
|
|
159
170
|
raise SourceResolveError(
|
|
@@ -28,6 +28,12 @@ REGISTRY_URL = (
|
|
|
28
28
|
_REQUIRED_FIELDS = frozenset(
|
|
29
29
|
{"name", "type", "description", "repo", "subdir", "tag_prefix", "min_framework_version"}
|
|
30
30
|
)
|
|
31
|
+
# Optional fields parsed when present; absence is not a schema error.
|
|
32
|
+
# ``latest_version``: registry-curated "current release tag", consumed by the
|
|
33
|
+
# install/upgrade resolver so it never has to hit ``api.github.com`` to find
|
|
34
|
+
# the latest release. Plugin release CI should keep this field up-to-date
|
|
35
|
+
# in ``DeepTradePluginOfficial/registry/index.json``.
|
|
36
|
+
_OPTIONAL_FIELDS = frozenset({"latest_version"})
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
class RegistryError(Exception):
|
|
@@ -56,6 +62,10 @@ class RegistryEntry:
|
|
|
56
62
|
subdir: str
|
|
57
63
|
tag_prefix: str
|
|
58
64
|
min_framework_version: str
|
|
65
|
+
# CDN-only install path: when present, the resolver uses this as the
|
|
66
|
+
# tag to download from codeload instead of calling the GitHub releases
|
|
67
|
+
# API. Optional for backward compat with registries written before v0.8.
|
|
68
|
+
latest_version: str | None = None
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
@dataclass(frozen=True)
|
|
@@ -93,10 +103,11 @@ def _parse_registry(data: Any) -> Registry:
|
|
|
93
103
|
missing = _REQUIRED_FIELDS - set(raw)
|
|
94
104
|
if missing:
|
|
95
105
|
raise RegistrySchemaError(f"plugins.{plugin_id} missing fields: {sorted(missing)}")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
fields: dict[str, Any] = {k: raw[k] for k in _REQUIRED_FIELDS}
|
|
107
|
+
for k in _OPTIONAL_FIELDS:
|
|
108
|
+
if k in raw:
|
|
109
|
+
fields[k] = raw[k]
|
|
110
|
+
entries[plugin_id] = RegistryEntry(plugin_id=plugin_id, **fields)
|
|
100
111
|
return Registry(schema_version=schema_version, plugins=entries)
|
|
101
112
|
|
|
102
113
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Unit tests for deeptrade.core.github_fetch.
|
|
2
|
+
|
|
3
|
+
Network calls (``urlopen``) are patched. Tarball extraction is exercised on
|
|
4
|
+
real .tar.gz produced in-memory. Since the v0.8 CDN refactor, github_fetch
|
|
5
|
+
only exposes ``fetch_tarball`` (latest-tag resolution moved to the registry
|
|
6
|
+
``latest_version`` field).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import tarfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from unittest.mock import patch
|
|
16
|
+
from urllib.error import HTTPError
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from deeptrade.core.github_fetch import (
|
|
21
|
+
CODELOAD_BASE,
|
|
22
|
+
TarballFetchError,
|
|
23
|
+
fetch_tarball,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _FakeResponse:
|
|
32
|
+
def __init__(self, body: bytes, headers: dict[str, str] | None = None) -> None:
|
|
33
|
+
self._buf = io.BytesIO(body)
|
|
34
|
+
self.headers = headers or {}
|
|
35
|
+
|
|
36
|
+
def read(self, n: int = -1) -> bytes:
|
|
37
|
+
return self._buf.read() if n == -1 else self._buf.read(n)
|
|
38
|
+
|
|
39
|
+
def __enter__(self) -> _FakeResponse:
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *args: Any) -> None:
|
|
43
|
+
self._buf.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_tarball(top_dir: str, files: dict[str, bytes]) -> bytes:
|
|
47
|
+
"""Build an in-memory .tar.gz with a single top-level directory."""
|
|
48
|
+
buf = io.BytesIO()
|
|
49
|
+
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
|
|
50
|
+
info = tarfile.TarInfo(name=top_dir)
|
|
51
|
+
info.type = tarfile.DIRTYPE
|
|
52
|
+
info.mode = 0o755
|
|
53
|
+
tf.addfile(info)
|
|
54
|
+
for path, content in files.items():
|
|
55
|
+
info = tarfile.TarInfo(name=f"{top_dir}/{path}")
|
|
56
|
+
info.size = len(content)
|
|
57
|
+
info.mode = 0o644
|
|
58
|
+
tf.addfile(info, io.BytesIO(content))
|
|
59
|
+
return buf.getvalue()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# fetch_tarball
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_fetch_tarball_extracts_and_returns_top_dir(tmp_path: Path) -> None:
|
|
68
|
+
tarball = _build_tarball(
|
|
69
|
+
"owner-repo-abc1234",
|
|
70
|
+
{"deeptrade_plugin.yaml": b"plugin_id: x\n", "data.py": b"# code\n"},
|
|
71
|
+
)
|
|
72
|
+
with patch(
|
|
73
|
+
"deeptrade.core.github_fetch.urlopen",
|
|
74
|
+
return_value=_FakeResponse(tarball),
|
|
75
|
+
):
|
|
76
|
+
top = fetch_tarball("owner/repo", "v1.0.0", tmp_path)
|
|
77
|
+
|
|
78
|
+
assert top.is_dir()
|
|
79
|
+
assert top.name == "owner-repo-abc1234"
|
|
80
|
+
assert (top / "deeptrade_plugin.yaml").is_file()
|
|
81
|
+
assert (top / "data.py").read_text() == "# code\n"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_fetch_tarball_uses_codeload_url(tmp_path: Path) -> None:
|
|
85
|
+
"""Sanity-check the new endpoint: no api.github.com, codeload only."""
|
|
86
|
+
tarball = _build_tarball("owner-repo-abc1234", {"x": b""})
|
|
87
|
+
captured: dict[str, str] = {}
|
|
88
|
+
|
|
89
|
+
def fake_urlopen(req: Any, **_: Any) -> _FakeResponse:
|
|
90
|
+
captured["url"] = req.full_url
|
|
91
|
+
return _FakeResponse(tarball)
|
|
92
|
+
|
|
93
|
+
with patch("deeptrade.core.github_fetch.urlopen", side_effect=fake_urlopen):
|
|
94
|
+
fetch_tarball("owner/repo", "limit-up-board/v0.4.0", tmp_path)
|
|
95
|
+
|
|
96
|
+
assert captured["url"].startswith(CODELOAD_BASE + "/")
|
|
97
|
+
assert "api.github.com" not in captured["url"]
|
|
98
|
+
assert "owner/repo/tar.gz/limit-up-board/v0.4.0" in captured["url"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_fetch_tarball_http_error_raises(tmp_path: Path) -> None:
|
|
102
|
+
err = HTTPError("https://x", 404, "Not Found", {}, None) # type: ignore[arg-type]
|
|
103
|
+
with patch("deeptrade.core.github_fetch.urlopen", side_effect=err):
|
|
104
|
+
with pytest.raises(TarballFetchError, match="HTTP 404"):
|
|
105
|
+
fetch_tarball("owner/repo", "v1.0.0", tmp_path)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_fetch_tarball_blocks_path_traversal(tmp_path: Path) -> None:
|
|
109
|
+
"""A tarball whose members escape dest_dir must be rejected."""
|
|
110
|
+
dest = tmp_path / "extract_here"
|
|
111
|
+
buf = io.BytesIO()
|
|
112
|
+
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
|
|
113
|
+
info = tarfile.TarInfo(name="owner-repo-abc/")
|
|
114
|
+
info.type = tarfile.DIRTYPE
|
|
115
|
+
tf.addfile(info)
|
|
116
|
+
info = tarfile.TarInfo(name="../escaped.txt")
|
|
117
|
+
info.size = 4
|
|
118
|
+
tf.addfile(info, io.BytesIO(b"evil"))
|
|
119
|
+
|
|
120
|
+
with patch(
|
|
121
|
+
"deeptrade.core.github_fetch.urlopen",
|
|
122
|
+
return_value=_FakeResponse(buf.getvalue()),
|
|
123
|
+
):
|
|
124
|
+
with pytest.raises(TarballFetchError, match="extract"):
|
|
125
|
+
fetch_tarball("owner/repo", "v1.0.0", dest)
|
|
126
|
+
assert not (tmp_path / "escaped.txt").exists()
|
|
@@ -22,6 +22,7 @@ from deeptrade.core.llm_client import (
|
|
|
22
22
|
LLMTransport,
|
|
23
23
|
LLMTransportError,
|
|
24
24
|
LLMValidationError,
|
|
25
|
+
MoonshotTransport,
|
|
25
26
|
OpenAICompatTransport,
|
|
26
27
|
OpenAIOfficialTransport,
|
|
27
28
|
RecordedTransport,
|
|
@@ -407,6 +408,96 @@ def test_select_transport_class_routes_dashscope_by_base_url() -> None:
|
|
|
407
408
|
)
|
|
408
409
|
|
|
409
410
|
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# Moonshot — server-side temperature constraint sanitization
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_base_transport_adjust_temperature_is_identity() -> None:
|
|
417
|
+
"""Default hook MUST NOT alter temperature — every non-Moonshot transport
|
|
418
|
+
relies on this. If this regresses, DashScope / DeepSeek / OpenAI / … will
|
|
419
|
+
silently start sending different temperatures than the caller requested.
|
|
420
|
+
"""
|
|
421
|
+
t = GenericOpenAITransport(api_key="dummy", base_url="https://api.deepseek.com", timeout=10)
|
|
422
|
+
assert t._adjust_temperature(model="deepseek-chat", temperature=0.0) == 0.0
|
|
423
|
+
assert t._adjust_temperature(model="deepseek-chat", temperature=0.7) == 0.7
|
|
424
|
+
assert t._adjust_temperature(model="anything", temperature=1.5) == 1.5
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_moonshot_transport_forces_temperature_for_reasoning_variants() -> None:
|
|
428
|
+
"""Kimi K2 reasoning variants only accept ``temperature == <forced>`` on
|
|
429
|
+
the wire; any other value returns HTTP 400. The transport must clamp to
|
|
430
|
+
the forced value regardless of what the StageProfile asks for.
|
|
431
|
+
"""
|
|
432
|
+
t = MoonshotTransport(api_key="dummy", base_url="https://api.moonshot.cn/v1", timeout=10)
|
|
433
|
+
# forced to 1.0
|
|
434
|
+
assert t._adjust_temperature(model="kimi-k2.6", temperature=0.2) == 1.0
|
|
435
|
+
assert t._adjust_temperature(model="kimi-k2.6-1106", temperature=0.1) == 1.0
|
|
436
|
+
assert t._adjust_temperature(model="kimi-k2-thinking", temperature=0.0) == 1.0
|
|
437
|
+
assert t._adjust_temperature(model="kimi-k2-thinking-128k", temperature=0.5) == 1.0
|
|
438
|
+
assert t._adjust_temperature(model="kimi-k2.5", temperature=0.2) == 1.0
|
|
439
|
+
# forced to 0.6
|
|
440
|
+
assert t._adjust_temperature(model="kimi-for-coding", temperature=0.0) == 0.6
|
|
441
|
+
# no-op when caller already supplied the forced value
|
|
442
|
+
assert t._adjust_temperature(model="kimi-k2.6", temperature=1.0) == 1.0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_moonshot_transport_clamps_non_reasoning_to_one() -> None:
|
|
446
|
+
"""Non-reasoning Moonshot models accept [0, 1]; values above 1 also 400.
|
|
447
|
+
Pass through inside the range; clamp above."""
|
|
448
|
+
t = MoonshotTransport(api_key="dummy", base_url="https://api.moonshot.cn/v1", timeout=10)
|
|
449
|
+
assert t._adjust_temperature(model="moonshot-v1-32k", temperature=0.1) == 0.1
|
|
450
|
+
assert t._adjust_temperature(model="kimi-k2-instruct-0905", temperature=0.2) == 0.2
|
|
451
|
+
assert t._adjust_temperature(model="moonshot-v1-32k", temperature=1.0) == 1.0
|
|
452
|
+
assert t._adjust_temperature(model="moonshot-v1-32k", temperature=1.5) == 1.0
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_moonshot_transport_sends_forced_temperature_on_wire(
|
|
456
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
457
|
+
) -> None:
|
|
458
|
+
"""End-to-end wire-shape regression: chat() composes kwargs with the
|
|
459
|
+
*adjusted* temperature, not the caller's original value."""
|
|
460
|
+
from types import SimpleNamespace
|
|
461
|
+
|
|
462
|
+
captured: dict[str, Any] = {}
|
|
463
|
+
|
|
464
|
+
def fake_create(**kwargs: Any) -> Any:
|
|
465
|
+
captured.update(kwargs)
|
|
466
|
+
choice = SimpleNamespace(message=SimpleNamespace(content='{"k": 1}'))
|
|
467
|
+
return SimpleNamespace(
|
|
468
|
+
choices=[choice],
|
|
469
|
+
usage=SimpleNamespace(prompt_tokens=1, completion_tokens=1),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
t = MoonshotTransport(api_key="dummy", base_url="https://api.moonshot.cn/v1", timeout=10)
|
|
473
|
+
monkeypatch.setattr(t._client.chat.completions, "create", fake_create)
|
|
474
|
+
t.chat(
|
|
475
|
+
model="kimi-k2.6",
|
|
476
|
+
system="s",
|
|
477
|
+
user="u",
|
|
478
|
+
temperature=0.2,
|
|
479
|
+
max_tokens=64,
|
|
480
|
+
thinking=False,
|
|
481
|
+
reasoning_effort="",
|
|
482
|
+
)
|
|
483
|
+
assert captured["temperature"] == 1.0
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_select_transport_class_routes_moonshot() -> None:
|
|
487
|
+
"""``api.moonshot.cn`` (with or without ``/v1``) routes to MoonshotTransport
|
|
488
|
+
via substring match, same pattern as the other entries in the routing table.
|
|
489
|
+
"""
|
|
490
|
+
assert _select_transport_class("https://api.moonshot.cn/v1") is MoonshotTransport
|
|
491
|
+
assert _select_transport_class("https://api.moonshot.cn") is MoonshotTransport
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_moonshot_transport_inherits_reasoning_effort_default() -> None:
|
|
495
|
+
"""Moonshot does not document support for the ``reasoning_effort`` field;
|
|
496
|
+
it inherits the base-class default (False) — confirm we didn't accidentally
|
|
497
|
+
flip it on along with adding the transport."""
|
|
498
|
+
assert MoonshotTransport.supports_reasoning_effort is False
|
|
499
|
+
|
|
500
|
+
|
|
410
501
|
# ---------------------------------------------------------------------------
|
|
411
502
|
# v0.6 H5 — reasoning_effort gating
|
|
412
503
|
# ---------------------------------------------------------------------------
|
|
@@ -532,4 +623,4 @@ def test_select_transport_class_defaults_to_generic() -> None:
|
|
|
532
623
|
actually reaches the wire; that case is covered separately below.
|
|
533
624
|
"""
|
|
534
625
|
assert _select_transport_class("https://api.deepseek.com") is GenericOpenAITransport
|
|
535
|
-
assert _select_transport_class("https://
|
|
626
|
+
assert _select_transport_class("https://openrouter.ai/api/v1") is GenericOpenAITransport
|