sdpy-kit 0.7.1__tar.gz → 0.7.3__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.3}/PKG-INFO +19 -13
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/README.md +18 -12
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/__init__.py +1 -1
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/chat_service.py +42 -29
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/profiles.py +9 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/runner.py +32 -4
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/sdk_client.py +2 -4
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/.gitignore +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/_retry_after.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/_service_base.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/rerank_transport.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/ai/types.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/common_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/config/kit_config.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/article_io.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/article_models.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/deepseek_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/web_article_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/crawl/wx_article_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/audit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/base_db_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/crud.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/engine.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/query.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/session.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/decima_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/__init__.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/anydoc_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/docx_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/marker_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/md2doc_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/mineru_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/docs/rc_docparser_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/file_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/laya_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/license_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.7.1 → sdpy_kit-0.7.3}/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.3
|
|
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,45 @@ 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`)。
|
|
889
|
+
注意:该属性**仅在流正常结束后可读**,迭代中与异常 / 中断后为 `None`(避免把「正文还没到」误判成 `no_finish`);`requested_max_tokens` 无此限制。
|
|
887
890
|
|
|
888
891
|
```python
|
|
889
892
|
with AI.chat_stream(messages, max_tokens=450) as stream:
|
|
890
893
|
body = "".join(stream)
|
|
891
894
|
if not body:
|
|
892
895
|
match stream.empty_kind:
|
|
893
|
-
case "budget_exhausted":
|
|
894
|
-
... # 调大 max_tokens
|
|
896
|
+
case "budget_exhausted" | "truncated_empty":
|
|
897
|
+
... # 调大 max_tokens(或配独立推理预算)
|
|
895
898
|
case "no_finish":
|
|
896
|
-
... #
|
|
899
|
+
... # 原样重试
|
|
897
900
|
```
|
|
898
901
|
|
|
902
|
+
`requested_max_tokens` 是本次**实际下发**的输出上限(调用方传值优先,否则取档案 `max_tokens`;都没有为 `None`),
|
|
903
|
+
在发请求前写入,失败/中断时也可读——业务可据此判断「预算是否真的设够了」。
|
|
904
|
+
|
|
899
905
|
- 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
|
|
900
906
|
- 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
|
|
901
907
|
- 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
|
|
902
908
|
- `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
|
|
903
909
|
- **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
|
|
904
|
-
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False
|
|
910
|
+
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;正文为空则归为 `no_finish`(可原样重试);
|
|
905
911
|
- 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
|
|
906
912
|
|
|
907
913
|
#### 逐调用思考开关
|
|
@@ -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,45 @@ 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`)。
|
|
807
|
+
注意:该属性**仅在流正常结束后可读**,迭代中与异常 / 中断后为 `None`(避免把「正文还没到」误判成 `no_finish`);`requested_max_tokens` 无此限制。
|
|
805
808
|
|
|
806
809
|
```python
|
|
807
810
|
with AI.chat_stream(messages, max_tokens=450) as stream:
|
|
808
811
|
body = "".join(stream)
|
|
809
812
|
if not body:
|
|
810
813
|
match stream.empty_kind:
|
|
811
|
-
case "budget_exhausted":
|
|
812
|
-
... # 调大 max_tokens
|
|
814
|
+
case "budget_exhausted" | "truncated_empty":
|
|
815
|
+
... # 调大 max_tokens(或配独立推理预算)
|
|
813
816
|
case "no_finish":
|
|
814
|
-
... #
|
|
817
|
+
... # 原样重试
|
|
815
818
|
```
|
|
816
819
|
|
|
820
|
+
`requested_max_tokens` 是本次**实际下发**的输出上限(调用方传值优先,否则取档案 `max_tokens`;都没有为 `None`),
|
|
821
|
+
在发请求前写入,失败/中断时也可读——业务可据此判断「预算是否真的设够了」。
|
|
822
|
+
|
|
817
823
|
- 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
|
|
818
824
|
- 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
|
|
819
825
|
- 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
|
|
820
826
|
- `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
|
|
821
827
|
- **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
|
|
822
|
-
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False
|
|
828
|
+
- provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;正文为空则归为 `no_finish`(可原样重试);
|
|
823
829
|
- 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
|
|
824
830
|
|
|
825
831
|
#### 逐调用思考开关
|
|
@@ -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 默认档案文件)。"""
|
|
@@ -846,10 +846,14 @@ class _StreamIterator:
|
|
|
846
846
|
直接 ``for`` 中途 ``break`` 或迭代器被 GC,``close``/``__del__`` 也会
|
|
847
847
|
兜底取消后台 producer,避免任务残留。
|
|
848
848
|
|
|
849
|
-
|
|
850
|
-
``
|
|
851
|
-
|
|
852
|
-
|
|
849
|
+
流结束后可读八个只读属性:``reasoning_chars`` / ``content_chars`` /
|
|
850
|
+
``finish_reason`` / ``requested_max_tokens`` / ``truncated`` / ``empty_kind`` /
|
|
851
|
+
``thinking_applied`` / ``json_mode_applied``(跨线程共享同一累加器:
|
|
852
|
+
结束后为最终值,流式过程中为尽力而为的中间值;提前 ``break`` 时为截至
|
|
853
|
+
中断的累计值)。
|
|
854
|
+
|
|
855
|
+
``empty_kind`` 例外:**仅在流正常结束后可读**,迭代中与异常 / 中断后
|
|
856
|
+
恒为 ``None``(避免把「正文还没到」误判成 ``no_finish``)。
|
|
853
857
|
"""
|
|
854
858
|
|
|
855
859
|
def __init__(
|
|
@@ -871,6 +875,8 @@ class _StreamIterator:
|
|
|
871
875
|
self._started = False
|
|
872
876
|
self._closed = False
|
|
873
877
|
self._got_first = False
|
|
878
|
+
# 正常收尾(收到哨兵)才置真:empty_kind 只在结束后给结论
|
|
879
|
+
self._exhausted = False
|
|
874
880
|
self._task: concurrent.futures.Future[None] | None = None
|
|
875
881
|
|
|
876
882
|
def __iter__(self) -> Iterator[str]:
|
|
@@ -931,6 +937,7 @@ class _StreamIterator:
|
|
|
931
937
|
f"流式帧间隔超过 {_STREAM_FRAME_INTERVAL_TIMEOUT:g}s 未收到新帧"
|
|
932
938
|
) from exc
|
|
933
939
|
if item is _SENTINEL:
|
|
940
|
+
self._exhausted = True
|
|
934
941
|
self.close()
|
|
935
942
|
raise StopIteration
|
|
936
943
|
if isinstance(item, BaseException):
|
|
@@ -971,6 +978,27 @@ class _StreamIterator:
|
|
|
971
978
|
"""结构化输出是否生效:None 未要求注入 / True 已注入 / False 被忽略。"""
|
|
972
979
|
return self._stats.json_mode_applied
|
|
973
980
|
|
|
981
|
+
@property
|
|
982
|
+
def requested_max_tokens(self) -> int | None:
|
|
983
|
+
"""本次实际下发的输出上限(调用方 → 档案回落);未发请求为 None。"""
|
|
984
|
+
return self._stats.requested_max_tokens
|
|
985
|
+
|
|
986
|
+
@property
|
|
987
|
+
def empty_kind(self) -> str | None:
|
|
988
|
+
"""正文为空时的诊断分级;**仅在流正常结束后可读,其余时刻为 None**。
|
|
989
|
+
|
|
990
|
+
取值同 ``StreamStats.empty_kind``(``budget_exhausted`` /
|
|
991
|
+
``truncated_empty`` / ``reasoning_only`` / ``no_finish`` /
|
|
992
|
+
``stopped_empty``;正文非空为 None)。
|
|
993
|
+
|
|
994
|
+
迭代中不给中间结论:此时正文尚未产出,``content_chars`` 恒为 0,
|
|
995
|
+
直接转发会把「还没出正文」误报成 ``no_finish``;异常 / 中途 ``break``
|
|
996
|
+
同理不置收尾标志。
|
|
997
|
+
"""
|
|
998
|
+
if not self._exhausted:
|
|
999
|
+
return None
|
|
1000
|
+
return self._stats.empty_kind
|
|
1001
|
+
|
|
974
1002
|
|
|
975
1003
|
async def _delayed_close(services: list[Service]) -> None:
|
|
976
1004
|
"""延迟回收旧 service:给在途调用留宽限期,到期统一关闭(须在 loop 内)。"""
|
|
@@ -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
|