sdpy-kit 0.7.1__tar.gz → 0.7.2__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.
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/PKG-INFO +18 -13
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/README.md +17 -12
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/__init__.py +1 -1
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/chat_service.py +42 -29
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/profiles.py +9 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/sdk_client.py +2 -4
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/.gitignore +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/_retry_after.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/_service_base.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/rerank_transport.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/ai/types.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/common_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/config/kit_config.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/article_io.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/article_models.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/deepseek_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/web_article_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/crawl/wx_article_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/audit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/base_db_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/crud.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/engine.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/query.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/session.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/decima_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/anydoc_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/docx_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/marker_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/md2doc_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/mineru_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/docs/rc_docparser_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/file_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/laya_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/license_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -835,7 +835,7 @@ shutdown() # 应用退出前
|
|
|
835
835
|
|------|------|------|
|
|
836
836
|
| chat | `chat_complete(prompt, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 单轮对话(仅 user 消息) |
|
|
837
837
|
| chat | `chat_ask(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 多轮对话,`messages` 为 `[{"role": "user", "content": "..."}]` |
|
|
838
|
-
| chat | `chat_stream(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None) -> Iterator[str]` | 同步流式逐 token 迭代器(建议 `with` 使用);结束后可读 `reasoning_chars` / `content_chars` / `finish_reason` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
|
|
838
|
+
| chat | `chat_stream(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None) -> Iterator[str]` | 同步流式逐 token 迭代器(建议 `with` 使用);结束后可读 `reasoning_chars` / `content_chars` / `finish_reason` / `requested_max_tokens` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
|
|
839
839
|
| embedding | `embed_text(text) -> list[float]` | 单文本向量化 |
|
|
840
840
|
| embedding | `embed_texts(texts) -> list[list[float]]` | 批量向量化 |
|
|
841
841
|
| rerank | `rerank(query, documents, top_n=None) -> list[RerankHit]` | 返回 `RerankHit`(`index` / `score` / `document`)降序列表;**`top_n` 是返回条数上限,由 kit 本地裁剪**(服务端返回超量也按分数截断),`top_n=None` 返回全部命中 |
|
|
@@ -869,39 +869,44 @@ with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
|
|
|
869
869
|
|
|
870
870
|
#### 流式收尾统计(思考型模型必备)
|
|
871
871
|
|
|
872
|
-
|
|
873
|
-
`truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied
|
|
874
|
-
|
|
875
|
-
|
|
872
|
+
迭代器在流结束后可读八个字段(`reasoning_chars` / `content_chars` / `finish_reason` /
|
|
873
|
+
`requested_max_tokens` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied`),
|
|
874
|
+
其中 `truncated` 与 `empty_kind` 是只读派生属性。用于区分「模型真的没返回」与「输出预算被思考吃光」——
|
|
875
|
+
思考型模型的推理 token 计入 `max_tokens`,长上下文 + 小预算时常出现「返回 200 但正文为空」
|
|
876
|
+
(同一套字段也可用于非流式 `ask` / `complete`,见下文「结构化输出」)。
|
|
876
877
|
|
|
877
|
-
`empty_kind`
|
|
878
|
+
`empty_kind` 把「正文为空」分成五类,不再一律归因为预算耗尽:
|
|
878
879
|
|
|
879
880
|
| 取值 | 判定 | 建议处置 |
|
|
880
881
|
|---|---|---|
|
|
881
882
|
| `budget_exhausted` | 被长度截断且有推理 | 调大 `max_tokens`,或对该调用传 `thinking=False` |
|
|
883
|
+
| `truncated_empty` | 被长度截断且无推理 | 调大 `max_tokens`;若模型支持独立推理预算,确认推理是否计入输出配额 |
|
|
882
884
|
| `reasoning_only` | 有推理、`finish_reason=stop`、正文 0 | 提高预算或换模型 |
|
|
883
|
-
| `no_finish` |
|
|
885
|
+
| `no_finish` | 结束原因缺失或非 `stop`(端点未发送 `finish_reason`) | 可原样重试;连续出现检查端点 |
|
|
884
886
|
| `stopped_empty` | 无推理、`finish_reason=stop`、正文 0 | 检查提示词与模型输出 |
|
|
885
887
|
|
|
886
|
-
正文非空时 `empty_kind is None
|
|
888
|
+
正文非空时 `empty_kind is None`。判定只用「是否截断 + 是否有推理 + 结束原因」,不依赖 token 数(kit 目前不采集 `usage`):
|
|
887
889
|
|
|
888
890
|
```python
|
|
889
891
|
with AI.chat_stream(messages, max_tokens=450) as stream:
|
|
890
892
|
body = "".join(stream)
|
|
891
893
|
if not body:
|
|
892
894
|
match stream.empty_kind:
|
|
893
|
-
case "budget_exhausted":
|
|
894
|
-
... # 调大 max_tokens
|
|
895
|
+
case "budget_exhausted" | "truncated_empty":
|
|
896
|
+
... # 调大 max_tokens(或配独立推理预算)
|
|
895
897
|
case "no_finish":
|
|
896
|
-
... #
|
|
898
|
+
... # 原样重试
|
|
897
899
|
```
|
|
898
900
|
|
|
901
|
+
`requested_max_tokens` 是本次**实际下发**的输出上限(调用方传值优先,否则取档案 `max_tokens`;都没有为 `None`),
|
|
902
|
+
在发请求前写入,失败/中断时也可读——业务可据此判断「预算是否真的设够了」。
|
|
903
|
+
|
|
899
904
|
- 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
|
|
900
905
|
- 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
|
|
901
906
|
- 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
|
|
902
907
|
- `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
|
|
903
908
|
- **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
|
|
904
|
-
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False
|
|
909
|
+
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;正文为空则归为 `no_finish`(可原样重试);
|
|
905
910
|
- 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
|
|
906
911
|
|
|
907
912
|
#### 逐调用思考开关
|
|
@@ -753,7 +753,7 @@ shutdown() # 应用退出前
|
|
|
753
753
|
|------|------|------|
|
|
754
754
|
| chat | `chat_complete(prompt, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 单轮对话(仅 user 消息) |
|
|
755
755
|
| chat | `chat_ask(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 多轮对话,`messages` 为 `[{"role": "user", "content": "..."}]` |
|
|
756
|
-
| chat | `chat_stream(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None) -> Iterator[str]` | 同步流式逐 token 迭代器(建议 `with` 使用);结束后可读 `reasoning_chars` / `content_chars` / `finish_reason` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
|
|
756
|
+
| chat | `chat_stream(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None) -> Iterator[str]` | 同步流式逐 token 迭代器(建议 `with` 使用);结束后可读 `reasoning_chars` / `content_chars` / `finish_reason` / `requested_max_tokens` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
|
|
757
757
|
| embedding | `embed_text(text) -> list[float]` | 单文本向量化 |
|
|
758
758
|
| embedding | `embed_texts(texts) -> list[list[float]]` | 批量向量化 |
|
|
759
759
|
| rerank | `rerank(query, documents, top_n=None) -> list[RerankHit]` | 返回 `RerankHit`(`index` / `score` / `document`)降序列表;**`top_n` 是返回条数上限,由 kit 本地裁剪**(服务端返回超量也按分数截断),`top_n=None` 返回全部命中 |
|
|
@@ -787,39 +787,44 @@ with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
|
|
|
787
787
|
|
|
788
788
|
#### 流式收尾统计(思考型模型必备)
|
|
789
789
|
|
|
790
|
-
|
|
791
|
-
`truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied
|
|
792
|
-
|
|
793
|
-
|
|
790
|
+
迭代器在流结束后可读八个字段(`reasoning_chars` / `content_chars` / `finish_reason` /
|
|
791
|
+
`requested_max_tokens` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied`),
|
|
792
|
+
其中 `truncated` 与 `empty_kind` 是只读派生属性。用于区分「模型真的没返回」与「输出预算被思考吃光」——
|
|
793
|
+
思考型模型的推理 token 计入 `max_tokens`,长上下文 + 小预算时常出现「返回 200 但正文为空」
|
|
794
|
+
(同一套字段也可用于非流式 `ask` / `complete`,见下文「结构化输出」)。
|
|
794
795
|
|
|
795
|
-
`empty_kind`
|
|
796
|
+
`empty_kind` 把「正文为空」分成五类,不再一律归因为预算耗尽:
|
|
796
797
|
|
|
797
798
|
| 取值 | 判定 | 建议处置 |
|
|
798
799
|
|---|---|---|
|
|
799
800
|
| `budget_exhausted` | 被长度截断且有推理 | 调大 `max_tokens`,或对该调用传 `thinking=False` |
|
|
801
|
+
| `truncated_empty` | 被长度截断且无推理 | 调大 `max_tokens`;若模型支持独立推理预算,确认推理是否计入输出配额 |
|
|
800
802
|
| `reasoning_only` | 有推理、`finish_reason=stop`、正文 0 | 提高预算或换模型 |
|
|
801
|
-
| `no_finish` |
|
|
803
|
+
| `no_finish` | 结束原因缺失或非 `stop`(端点未发送 `finish_reason`) | 可原样重试;连续出现检查端点 |
|
|
802
804
|
| `stopped_empty` | 无推理、`finish_reason=stop`、正文 0 | 检查提示词与模型输出 |
|
|
803
805
|
|
|
804
|
-
正文非空时 `empty_kind is None
|
|
806
|
+
正文非空时 `empty_kind is None`。判定只用「是否截断 + 是否有推理 + 结束原因」,不依赖 token 数(kit 目前不采集 `usage`):
|
|
805
807
|
|
|
806
808
|
```python
|
|
807
809
|
with AI.chat_stream(messages, max_tokens=450) as stream:
|
|
808
810
|
body = "".join(stream)
|
|
809
811
|
if not body:
|
|
810
812
|
match stream.empty_kind:
|
|
811
|
-
case "budget_exhausted":
|
|
812
|
-
... # 调大 max_tokens
|
|
813
|
+
case "budget_exhausted" | "truncated_empty":
|
|
814
|
+
... # 调大 max_tokens(或配独立推理预算)
|
|
813
815
|
case "no_finish":
|
|
814
|
-
... #
|
|
816
|
+
... # 原样重试
|
|
815
817
|
```
|
|
816
818
|
|
|
819
|
+
`requested_max_tokens` 是本次**实际下发**的输出上限(调用方传值优先,否则取档案 `max_tokens`;都没有为 `None`),
|
|
820
|
+
在发请求前写入,失败/中断时也可读——业务可据此判断「预算是否真的设够了」。
|
|
821
|
+
|
|
817
822
|
- 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
|
|
818
823
|
- 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
|
|
819
824
|
- 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
|
|
820
825
|
- `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
|
|
821
826
|
- **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
|
|
822
|
-
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False
|
|
827
|
+
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;正文为空则归为 `no_finish`(可原样重试);
|
|
823
828
|
- 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
|
|
824
829
|
|
|
825
830
|
#### 逐调用思考开关
|
|
@@ -29,7 +29,11 @@ from dataclasses import dataclass
|
|
|
29
29
|
from typing import Any, ClassVar, Final, cast
|
|
30
30
|
|
|
31
31
|
from kit.ai.exceptions import AIError, AIResponseError
|
|
32
|
-
from kit.ai.profiles import
|
|
32
|
+
from kit.ai.profiles import (
|
|
33
|
+
_RESERVED_PAYLOAD_KEYS,
|
|
34
|
+
ModelProfile,
|
|
35
|
+
resolve_max_tokens,
|
|
36
|
+
)
|
|
33
37
|
from kit.ai.sdk_client import SdkServiceBase
|
|
34
38
|
from kit.ai.types import JSONObject
|
|
35
39
|
from kit.config.log_config import get_default_logger
|
|
@@ -50,15 +54,17 @@ _TRUNCATED_REASONS: Final[frozenset[str]] = frozenset({"length", "max_tokens"})
|
|
|
50
54
|
# 空正文诊断分级 → 告警文案(正文为 0 时才有分级,见 _classify_empty)
|
|
51
55
|
_EMPTY_HINTS: Final[dict[str, str]] = {
|
|
52
56
|
"budget_exhausted": "输出预算疑被思考耗尽(长度截断)",
|
|
57
|
+
"truncated_empty": "输出被长度截断、正文为空(无推理)",
|
|
53
58
|
"reasoning_only": "只产出推理、正文为空(finish=stop)",
|
|
54
|
-
"no_finish": "
|
|
59
|
+
"no_finish": "正文为空且未收到正常结束原因",
|
|
55
60
|
"stopped_empty": "无正文输出(finish=stop)",
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
_EMPTY_ACTIONS: Final[dict[str, str]] = {
|
|
59
64
|
"budget_exhausted": "(可调大 max_tokens,或对该调用传 thinking=False 关闭思考)",
|
|
65
|
+
"truncated_empty": "(调大 max_tokens;若模型支持独立推理预算,确认推理是否计入输出配额)",
|
|
60
66
|
"reasoning_only": "(推理未触顶但正文为空,可提高预算或换模型)",
|
|
61
|
-
"no_finish": "
|
|
67
|
+
"no_finish": "(端点未发送 finish_reason 或异常结束,可原样重试)",
|
|
62
68
|
"stopped_empty": "(检查提示词与模型输出)",
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -70,20 +76,24 @@ def _classify_empty(
|
|
|
70
76
|
finish_reason: str | None,
|
|
71
77
|
truncated: bool,
|
|
72
78
|
) -> str | None:
|
|
73
|
-
"""
|
|
79
|
+
"""空正文诊断分级;正文非空返回 None。
|
|
74
80
|
|
|
75
81
|
- ``budget_exhausted``:被截断且有推理(预算被思考吃满)
|
|
82
|
+
- ``truncated_empty``:被截断且无推理(一个正文字符都没出)
|
|
76
83
|
- ``reasoning_only``:有推理、``finish_reason=stop``
|
|
77
|
-
- ``no_finish
|
|
84
|
+
- ``no_finish``:结束原因缺失或非 stop(无论有无推理)
|
|
78
85
|
- ``stopped_empty``:无推理、``finish_reason=stop``
|
|
79
86
|
"""
|
|
80
87
|
if content_chars:
|
|
81
88
|
return None
|
|
82
|
-
if truncated
|
|
83
|
-
return "budget_exhausted"
|
|
89
|
+
if truncated:
|
|
90
|
+
return "budget_exhausted" if reasoning_chars else "truncated_empty"
|
|
84
91
|
if reasoning_chars:
|
|
85
92
|
return "reasoning_only" if finish_reason == "stop" else "no_finish"
|
|
86
|
-
|
|
93
|
+
if finish_reason == "stop":
|
|
94
|
+
return "stopped_empty"
|
|
95
|
+
return "no_finish"
|
|
96
|
+
|
|
87
97
|
|
|
88
98
|
# 「无注入值」哨兵:区分「不注入」与「注入值为 None」
|
|
89
99
|
_UNSET: Final[object] = object()
|
|
@@ -172,10 +182,13 @@ class StreamStats:
|
|
|
172
182
|
True 已按档案声明注入 / False 要求了但档案完全没有 thinking 声明
|
|
173
183
|
json_mode_applied: 结构化输出是否生效——None 本次未要求注入 /
|
|
174
184
|
True 已注入 / False 要求了但档案声明不支持(已忽略)
|
|
185
|
+
requested_max_tokens: 本次实际下发的输出上限(调用方 → 档案回落);
|
|
186
|
+
None 表示未下发、由 provider 默认值决定。在发出请求前写入,
|
|
187
|
+
失败/中断时也可读
|
|
175
188
|
|
|
176
189
|
只读派生属性:``truncated``(是否截断)、``empty_kind``(正文为空时的
|
|
177
|
-
诊断分级:``budget_exhausted`` / ``
|
|
178
|
-
``stopped_empty``;正文非空为 None)。
|
|
190
|
+
诊断分级:``budget_exhausted`` / ``truncated_empty`` / ``reasoning_only`` /
|
|
191
|
+
``no_finish`` / ``stopped_empty``;正文非空为 None)。
|
|
179
192
|
"""
|
|
180
193
|
|
|
181
194
|
reasoning_chars: int = 0
|
|
@@ -183,6 +196,7 @@ class StreamStats:
|
|
|
183
196
|
finish_reason: str | None = None
|
|
184
197
|
thinking_applied: bool | None = None
|
|
185
198
|
json_mode_applied: bool | None = None
|
|
199
|
+
requested_max_tokens: int | None = None
|
|
186
200
|
|
|
187
201
|
@property
|
|
188
202
|
def truncated(self) -> bool:
|
|
@@ -193,9 +207,10 @@ class StreamStats:
|
|
|
193
207
|
def empty_kind(self) -> str | None:
|
|
194
208
|
"""正文为空时的诊断分级;正文非空返回 None。
|
|
195
209
|
|
|
196
|
-
取值:``budget_exhausted`` / ``
|
|
197
|
-
``stopped_empty``。不依赖 token
|
|
198
|
-
|
|
210
|
+
取值:``budget_exhausted`` / ``truncated_empty`` / ``reasoning_only`` /
|
|
211
|
+
``no_finish`` / ``stopped_empty``。不依赖 token 数量,用「是否截断 +
|
|
212
|
+
是否有推理 + 结束原因」区分「预算吃满」「截断且无正文」「只推理」
|
|
213
|
+
「未正常收尾」「正常停住」。
|
|
199
214
|
"""
|
|
200
215
|
return _classify_empty(
|
|
201
216
|
content_chars=self.content_chars,
|
|
@@ -211,6 +226,7 @@ class StreamStats:
|
|
|
211
226
|
self.finish_reason = None
|
|
212
227
|
self.thinking_applied = None
|
|
213
228
|
self.json_mode_applied = None
|
|
229
|
+
self.requested_max_tokens = None
|
|
214
230
|
|
|
215
231
|
|
|
216
232
|
@dataclass(frozen=True)
|
|
@@ -383,7 +399,8 @@ class ChatService(SdkServiceBase):
|
|
|
383
399
|
response_format: 协议级原样透传(含 ``json_schema`` 严格模式);
|
|
384
400
|
与 ``json_mode`` 互斥,schema 由调用方负责
|
|
385
401
|
stats: 收尾统计累加器(可选);传入后可读 ``finish_reason`` /
|
|
386
|
-
``truncated`` / ``
|
|
402
|
+
``truncated`` / ``empty_kind`` / ``requested_max_tokens`` /
|
|
403
|
+
``reasoning_chars`` / ``json_mode_applied``
|
|
387
404
|
deadline: 整链预算截止时刻(``time.monotonic()`` 秒),约束
|
|
388
405
|
5xx 重试预算;None 表示不限制
|
|
389
406
|
"""
|
|
@@ -397,6 +414,9 @@ class ChatService(SdkServiceBase):
|
|
|
397
414
|
stats=stats,
|
|
398
415
|
)
|
|
399
416
|
extra_body, response_format_arg = self._build_injection(request)
|
|
417
|
+
effective_max_tokens = resolve_max_tokens(self._profile, request.max_tokens)
|
|
418
|
+
if stats is not None:
|
|
419
|
+
stats.requested_max_tokens = effective_max_tokens
|
|
400
420
|
self._debug_payload(
|
|
401
421
|
{
|
|
402
422
|
"model": self._profile.model,
|
|
@@ -434,11 +454,6 @@ class ChatService(SdkServiceBase):
|
|
|
434
454
|
finish_reason=finish,
|
|
435
455
|
truncated=truncated,
|
|
436
456
|
)
|
|
437
|
-
effective_max_tokens = (
|
|
438
|
-
request.max_tokens
|
|
439
|
-
if request.max_tokens is not None
|
|
440
|
-
else self._profile.max_tokens
|
|
441
|
-
)
|
|
442
457
|
if kind is not None:
|
|
443
458
|
self._log_empty(
|
|
444
459
|
kind=kind,
|
|
@@ -505,11 +520,12 @@ class ChatService(SdkServiceBase):
|
|
|
505
520
|
kit 不做增量解析
|
|
506
521
|
response_format: 协议级原样透传(与 ``json_mode`` 互斥)
|
|
507
522
|
stats: 收尾统计累加器(可选);None 时内部创建后丢弃——需要
|
|
508
|
-
``reasoning_chars`` / ``finish_reason`` / ``truncated``
|
|
509
|
-
|
|
523
|
+
``reasoning_chars`` / ``finish_reason`` / ``truncated`` /
|
|
524
|
+
``empty_kind`` / ``requested_max_tokens`` 时由调用方传入
|
|
510
525
|
|
|
511
526
|
推理内容(``delta.reasoning_content`` / ``delta.reasoning``)只计数
|
|
512
|
-
|
|
527
|
+
不产出;正文为空时按 ``empty_kind`` 分级输出 warning(不依赖是否有
|
|
528
|
+
推理或是否截断)。
|
|
513
529
|
"""
|
|
514
530
|
stats = stats if stats is not None else StreamStats()
|
|
515
531
|
request = ChatRequest(
|
|
@@ -523,6 +539,8 @@ class ChatService(SdkServiceBase):
|
|
|
523
539
|
stats=stats,
|
|
524
540
|
)
|
|
525
541
|
extra_body, response_format_arg = self._build_injection(request)
|
|
542
|
+
effective_max_tokens = resolve_max_tokens(self._profile, request.max_tokens)
|
|
543
|
+
stats.requested_max_tokens = effective_max_tokens
|
|
526
544
|
desc = self._log_desc()
|
|
527
545
|
# 惰性 Start:异步生成器体在首次 __anext__ 前不执行,不消费不打日志
|
|
528
546
|
logger.info(f"********** {desc} ********** Start")
|
|
@@ -561,11 +579,6 @@ class ChatService(SdkServiceBase):
|
|
|
561
579
|
note = "(中断)"
|
|
562
580
|
raise
|
|
563
581
|
else:
|
|
564
|
-
effective_max_tokens = (
|
|
565
|
-
request.max_tokens
|
|
566
|
-
if request.max_tokens is not None
|
|
567
|
-
else self._profile.max_tokens
|
|
568
|
-
)
|
|
569
582
|
self._warn_if_empty(stats, effective_max_tokens)
|
|
570
583
|
finally:
|
|
571
584
|
log = logger.error if failed else logger.info
|
|
@@ -574,7 +587,7 @@ class ChatService(SdkServiceBase):
|
|
|
574
587
|
f"耗时 {time.perf_counter() - start:.1f}秒{note}"
|
|
575
588
|
)
|
|
576
589
|
|
|
577
|
-
def _warn_if_empty(self, stats: StreamStats, max_tokens:
|
|
590
|
+
def _warn_if_empty(self, stats: StreamStats, max_tokens: int | None) -> None:
|
|
578
591
|
"""正文为空时按诊断分级告警(不再一律归因为「预算被思考耗尽」)。"""
|
|
579
592
|
kind = stats.empty_kind
|
|
580
593
|
if kind is None:
|
|
@@ -590,7 +603,7 @@ class ChatService(SdkServiceBase):
|
|
|
590
603
|
self,
|
|
591
604
|
*,
|
|
592
605
|
kind: str,
|
|
593
|
-
max_tokens:
|
|
606
|
+
max_tokens: int | None,
|
|
594
607
|
finish: str | None,
|
|
595
608
|
reasoning_chars: int,
|
|
596
609
|
) -> None:
|
|
@@ -536,6 +536,15 @@ def with_api_key(profile: ModelProfile, api_key: str) -> ModelProfile:
|
|
|
536
536
|
return replace(profile, api_key=api_key)
|
|
537
537
|
|
|
538
538
|
|
|
539
|
+
def resolve_max_tokens(profile: ModelProfile, max_tokens: int | None) -> int | None:
|
|
540
|
+
"""解析本次实际下发的输出上限:调用方 → 档案 → ``None``(provider 默认)。
|
|
541
|
+
|
|
542
|
+
单一数据源:``ChatService``(写入 ``StreamStats`` 与日志)与
|
|
543
|
+
``SdkClient._chat_params``(构造请求体)共用,避免两处回落逻辑漂移。
|
|
544
|
+
"""
|
|
545
|
+
return max_tokens if max_tokens is not None else profile.max_tokens
|
|
546
|
+
|
|
547
|
+
|
|
539
548
|
@lru_cache(maxsize=1)
|
|
540
549
|
def get_model_registry() -> ModelRegistry:
|
|
541
550
|
"""获取全局单例 ModelRegistry(首次调用自动 load 默认档案文件)。"""
|
|
@@ -54,7 +54,7 @@ from kit.ai.exceptions import (
|
|
|
54
54
|
AIResponseError,
|
|
55
55
|
AITimeoutError,
|
|
56
56
|
)
|
|
57
|
-
from kit.ai.profiles import ModelProfile
|
|
57
|
+
from kit.ai.profiles import ModelProfile, resolve_max_tokens
|
|
58
58
|
from kit.ai.types import JSONObject
|
|
59
59
|
from kit.config.log_config import get_default_logger
|
|
60
60
|
|
|
@@ -193,9 +193,7 @@ class SdkClient:
|
|
|
193
193
|
}
|
|
194
194
|
if temperature is not None:
|
|
195
195
|
params["temperature"] = temperature
|
|
196
|
-
effective_max_tokens = (
|
|
197
|
-
max_tokens if max_tokens is not None else self._profile.max_tokens
|
|
198
|
-
)
|
|
196
|
+
effective_max_tokens = resolve_max_tokens(self._profile, max_tokens)
|
|
199
197
|
if effective_max_tokens is not None:
|
|
200
198
|
params["max_tokens"] = effective_max_tokens
|
|
201
199
|
if response_format is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|