sdpy-kit 0.6.11__tar.gz → 0.7.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.
Files changed (58) hide show
  1. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/PKG-INFO +87 -58
  2. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/README.md +64 -34
  3. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/__init__.py +2 -1
  5. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/chat_service.py +143 -27
  6. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/exceptions.py +10 -0
  7. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/failover.py +13 -5
  8. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/image_service.py +18 -6
  9. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/model_builder.py +7 -5
  10. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/model_client.py +12 -9
  11. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/pool.py +18 -5
  12. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/profiles.py +29 -12
  13. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/rerank_service.py +28 -10
  14. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/rerank_transport.py +1 -1
  15. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/runner.py +58 -22
  16. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/sdk_client.py +33 -27
  17. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/__init__.py +1 -0
  18. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/md2doc_kit.py +1 -10
  19. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/pyproject.toml +8 -2
  20. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/.gitignore +0 -0
  21. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/_retry_after.py +0 -0
  22. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/_service_base.py +0 -0
  23. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/embedding_service.py +0 -0
  24. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/ai/types.py +0 -0
  25. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/common_kit.py +0 -0
  26. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/config/__init__.py +0 -0
  27. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/config/kit_config.py +0 -0
  28. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/config/log_config.py +0 -0
  29. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/__init__.py +0 -0
  30. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/article_io.py +0 -0
  31. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/article_models.py +0 -0
  32. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/deepseek_kit.py +0 -0
  33. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/web_article_kit.py +0 -0
  34. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/crawl/wx_article_kit.py +0 -0
  35. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/__init__.py +0 -0
  36. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/audit.py +0 -0
  37. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/base_db_kit.py +0 -0
  38. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/crud.py +0 -0
  39. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/engine.py +0 -0
  40. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/exceptions.py +0 -0
  41. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/models/__init__.py +0 -0
  42. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/models/base.py +0 -0
  43. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/mysql_kit.py +0 -0
  44. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/pg_kit.py +0 -0
  45. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/query.py +0 -0
  46. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/session.py +0 -0
  47. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/sqlite_kit.py +0 -0
  48. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/db/transaction.py +0 -0
  49. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/decima_kit.py +0 -0
  50. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/anydoc_kit.py +0 -0
  51. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/docx_kit.py +0 -0
  52. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/marker_kit.py +0 -0
  53. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/mineru_kit.py +0 -0
  54. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/docs/rc_docparser_kit.py +0 -0
  55. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/file_kit.py +0 -0
  56. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/laya_kit.py +0 -0
  57. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/license_kit.py +0 -0
  58. {sdpy_kit-0.6.11 → sdpy_kit-0.7.1}/kit/redis_kit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.6.11
3
+ Version: 0.7.1
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -26,27 +26,6 @@ Provides-Extra: ai
26
26
  Requires-Dist: httpx>=0.28.1; extra == 'ai'
27
27
  Requires-Dist: openai<4,>=3.44; extra == 'ai'
28
28
  Requires-Dist: python-dotenv>=1.2.4; extra == 'ai'
29
- Provides-Extra: all
30
- Requires-Dist: decima-ai>=2.5.1; extra == 'all'
31
- Requires-Dist: docxtpl>=0.20.2; extra == 'all'
32
- Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
33
- Requires-Dist: httpx>=0.28.1; extra == 'all'
34
- Requires-Dist: huggingface-hub<2,>=1.5; extra == 'all'
35
- Requires-Dist: laya>=0.4.22; extra == 'all'
36
- Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
37
- Requires-Dist: numpy>=2.5.4; extra == 'all'
38
- Requires-Dist: onnxruntime>=1.18; extra == 'all'
39
- Requires-Dist: openai<4,>=3.44; extra == 'all'
40
- Requires-Dist: pikepdf>=10.17.0; extra == 'all'
41
- Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
42
- Requires-Dist: psycopg2-binary>=2.9.13; extra == 'all'
43
- Requires-Dist: pymysql>=1.2.3; extra == 'all'
44
- Requires-Dist: python-dotenv>=1.2.4; extra == 'all'
45
- Requires-Dist: rc-docparser>=0.2.2; extra == 'all'
46
- Requires-Dist: redis>=8.1.0; extra == 'all'
47
- Requires-Dist: requests>=2.34.2; extra == 'all'
48
- Requires-Dist: sqliteai-vector>=1.2.0; extra == 'all'
49
- Requires-Dist: trafilatura>=2.3.1; extra == 'all'
50
29
  Provides-Extra: anydoc
51
30
  Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
52
31
  Requires-Dist: pikepdf>=10.17.0; extra == 'anydoc'
@@ -61,10 +40,9 @@ Requires-Dist: rc-docparser>=0.2.2; extra == 'docx-parse'
61
40
  Provides-Extra: laya
62
41
  Requires-Dist: huggingface-hub<2,>=1.5; extra == 'laya'
63
42
  Requires-Dist: laya>=0.4.22; extra == 'laya'
64
- Provides-Extra: marker
65
- Requires-Dist: marker-pdf>=2.0.0; extra == 'marker'
66
43
  Provides-Extra: md2doc
67
44
  Requires-Dist: markdown-it-py>=4.2.0; extra == 'md2doc'
45
+ Requires-Dist: pillow>=13.0.0; extra == 'md2doc'
68
46
  Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'md2doc'
69
47
  Provides-Extra: mineru
70
48
  Requires-Dist: requests>=2.34.2; extra == 'mineru'
@@ -72,6 +50,27 @@ Provides-Extra: mysql
72
50
  Requires-Dist: pymysql>=1.2.3; extra == 'mysql'
73
51
  Provides-Extra: pg
74
52
  Requires-Dist: psycopg2-binary>=2.9.13; extra == 'pg'
53
+ Provides-Extra: recommended
54
+ Requires-Dist: decima-ai>=2.5.1; extra == 'recommended'
55
+ Requires-Dist: docxtpl>=0.20.2; extra == 'recommended'
56
+ Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'recommended'
57
+ Requires-Dist: httpx>=0.28.1; extra == 'recommended'
58
+ Requires-Dist: huggingface-hub<2,>=1.5; extra == 'recommended'
59
+ Requires-Dist: laya>=0.4.22; extra == 'recommended'
60
+ Requires-Dist: markdown-it-py>=4.2.0; extra == 'recommended'
61
+ Requires-Dist: numpy>=2.5.4; extra == 'recommended'
62
+ Requires-Dist: onnxruntime>=1.18; extra == 'recommended'
63
+ Requires-Dist: openai<4,>=3.44; extra == 'recommended'
64
+ Requires-Dist: pikepdf>=10.17.0; extra == 'recommended'
65
+ Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'recommended'
66
+ Requires-Dist: psycopg2-binary>=2.9.13; extra == 'recommended'
67
+ Requires-Dist: pymysql>=1.2.3; extra == 'recommended'
68
+ Requires-Dist: python-dotenv>=1.2.4; extra == 'recommended'
69
+ Requires-Dist: rc-docparser>=0.2.2; extra == 'recommended'
70
+ Requires-Dist: redis>=8.1.0; extra == 'recommended'
71
+ Requires-Dist: requests>=2.34.2; extra == 'recommended'
72
+ Requires-Dist: sqliteai-vector>=1.2.0; extra == 'recommended'
73
+ Requires-Dist: trafilatura>=2.3.1; extra == 'recommended'
75
74
  Provides-Extra: redis
76
75
  Requires-Dist: redis>=8.1.0; extra == 'redis'
77
76
  Provides-Extra: sqliteai
@@ -87,7 +86,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
87
86
 
88
87
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
89
88
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
90
- - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
89
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(0.7.0 起暂停发布,需要请用 `sdpy-kit==0.6.10`) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
91
90
 
92
91
  > 要求 Python 3.13+
93
92
 
@@ -107,7 +106,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
107
106
  | `kit.docs.rc_docparser_kit` | `RcDocParserKit` | 离线 docx → Markdown + Images(无模型) | `docx-parse` |
108
107
  | `kit.docs.md2doc_kit` | `Md2DocKit` | Markdown → Word / PDF / HTML(含图片) | `md2doc` |
109
108
  | `kit.docs.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
110
- | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images) | `marker` |
109
+ | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images);**0.7.0 起暂停发布**,需要请用 `sdpy-kit==0.6.10` | — |
111
110
  | `kit.laya_kit` | `LayaKit` | 类型化决策:choice / multi_choice 多选 / score / noul,另含 decide 多字段 / decide_batch 批量 / predict_long 长文本 | `laya` |
112
111
  | `kit.decima_kit` | `DecimaKit` | 决策模型:choice / score / noul / multi_choice(原生 rank);选项乱序不变、置信度可门控;另含 predict / predict_long / brief / choice_batch(方法名与 `LayaKit` 对齐) | `decima` |
113
112
  | `kit.crawl.article_models` | `ArticleInfo` / `ArticleResult` / `SavedOutput` 等 | 文章抓取通用模型(三个 `*ArticleKit` 共用,含 `reason` 分类与非致命 `warnings`) | 无 |
@@ -138,11 +137,10 @@ uv add sdpy-kit
138
137
  | `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
139
138
  | `docx-parse` | RcDocParserKit 离线 docx 解析(rc-docparser → Markdown + images/) | `pip install "sdpy-kit[docx-parse]"` |
140
139
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
141
- | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大)。**与 `[ai]` 互斥**:marker-pdf 锁 `openai<3`,不能与 `openai>=3.44` 同环境安装 | `pip install "sdpy-kit[marker]"` |
142
140
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
143
141
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
144
142
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
145
- | `all` | 常用能力集合,**不含 `marker`**(0.6.11 起,避免与 `[ai]` 的 openai 约束冲突);需要 Marker 请单独装 `[marker]` | `pip install "sdpy-kit[all]"` |
143
+ | `recommended` | 常用能力集合(DB / 缓存 / AI / 文档 / 抓取);不含重依赖(laya / decima)与 Marker(0.7.0 起暂停发布) | `pip install "sdpy-kit[recommended]"` |
146
144
 
147
145
  ## 配置(.env)
148
146
 
@@ -811,7 +809,7 @@ page = qb.paginate(page=1, per_page=20) # {"items", "total", ...}
811
809
 
812
810
  安装:`pip install "sdpy-kit[ai]"`
813
811
 
814
- 传输(0.6.11):chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
812
+ 传输:chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
815
813
  自带 `httpx2`),rerank 走 httpx 自研传输(SDK 无 `/rerank` 资源)。
816
814
  档案里的 `endpoint` **对前三类不再生效**——SDK 固定拼 `/chat/completions`、
817
815
  `/embeddings`、`/images/generations`,仅 rerank 与探活仍使用该字段。
@@ -837,14 +835,29 @@ shutdown() # 应用退出前
837
835
  |------|------|------|
838
836
  | chat | `chat_complete(prompt, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 单轮对话(仅 user 消息) |
839
837
  | chat | `chat_ask(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 多轮对话,`messages` 为 `[{"role": "user", "content": "..."}]` |
840
- | 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` / `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` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
841
839
  | embedding | `embed_text(text) -> list[float]` | 单文本向量化 |
842
840
  | embedding | `embed_texts(texts) -> list[list[float]]` | 批量向量化 |
843
- | rerank | `rerank(query, documents, top_n=None) -> list[tuple[int, float]]` | 返回 `[(原索引, 相关分)]` 降序列表 |
844
- | image | `generate_image(prompt, *, size=None, n=1) -> str` | 文生图,返回图片 URL |
841
+ | rerank | `rerank(query, documents, top_n=None) -> list[RerankHit]` | 返回 `RerankHit`(`index` / `score` / `document`)降序列表;**`top_n` 是返回条数上限,由 kit 本地裁剪**(服务端返回超量也按分数截断),`top_n=None` 返回全部命中 |
842
+ | image | `generate_image(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` | 文生图,返回 `ImageResult`(`url` / `b64` 至少一个非空) |
845
843
  | 管理 | `use(model_type, profile) -> None` | 切换该类型当前档案(`chat` / `embedding` / `rerank` / `image`) |
846
- | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置与被剔除者) |
847
- | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启 |
844
+ | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置密钥与被剔除者);**类型未在 `ai_models.json` 中配置时返回 `[]`**(不再抛异常),类型名非法仍抛 `AIConfigError` |
845
+ | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启;同时清空思考/结构化输出的告警去重表(修正档案后同一原因会重新提示) |
846
+
847
+ **可用性查询(`available`)**:`available` 返回的是**状态快照**,不是「可用档案列表」。
848
+ 判断某类型是否已配置用 `if not AI.available(t)`,不要靠捕获异常:
849
+
850
+ ```python
851
+ from kit.ai.runner import AI
852
+
853
+ profiles = AI.available("image") # ai_models.json 无 image 段 → []
854
+ if not profiles:
855
+ raise RuntimeError("尚未配置图像生成模型")
856
+ current = next((p for p in profiles if p.current), None)
857
+ ```
858
+
859
+ > 旧写法 `try: AI.available(t) except AIConfigError: ...` 仍可用(异常分支不再触发);
860
+ > `AI.available(t)[0]` 在类型未配置时会抛 `IndexError`,请改用上面的判空写法。
848
861
 
849
862
  ```python
850
863
  from kit.ai.runner import AI
@@ -856,30 +869,42 @@ with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
856
869
 
857
870
  #### 流式收尾统计(思考型模型必备)
858
871
 
859
- 迭代器在流结束后可读六个只读属性(`reasoning_chars` / `content_chars` / `finish_reason` /
860
- `truncated` / `thinking_applied` / `json_mode_applied`),用于区分「模型真的没返回」与「输出预算被思考吃光」——思考型模型的推理
872
+ 迭代器在流结束后可读七个只读属性(`reasoning_chars` / `content_chars` / `finish_reason` /
873
+ `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied`),用于区分「模型真的没返回」与「输出预算被思考吃光」——思考型模型的推理
861
874
  token 计入 `max_tokens`,长上下文 + 小预算时常出现「返回 200 但正文为空」(同一套字段也可用于非流式 `ask` / `complete`,
862
- 见下文「结构化输出」):
875
+ 见下文「结构化输出」)。
876
+
877
+ `empty_kind` 把「正文为空」分成四类,不再一律归因为预算耗尽:
878
+
879
+ | 取值 | 判定 | 建议处置 |
880
+ |---|---|---|
881
+ | `budget_exhausted` | 被长度截断且有推理 | 调大 `max_tokens`,或对该调用传 `thinking=False` |
882
+ | `reasoning_only` | 有推理、`finish_reason=stop`、正文 0 | 提高预算或换模型 |
883
+ | `no_finish` | 有推理但没收到结束原因(链路中断 / 端点未收尾) | 优先重试,连续出现检查端点/代理超时 |
884
+ | `stopped_empty` | 无推理、`finish_reason=stop`、正文 0 | 检查提示词与模型输出 |
885
+
886
+ 正文非空时 `empty_kind is None`。判定只用「是否有推理 + 结束原因」,不依赖 token 数(kit 目前不采集 `usage`):
863
887
 
864
888
  ```python
865
889
  with AI.chat_stream(messages, max_tokens=450) as stream:
866
890
  body = "".join(stream)
867
- if not body and stream.truncated and stream.reasoning_chars:
868
- raise ValueError(
869
- f"输出预算被思考消耗(推理 {stream.reasoning_chars} 字符),"
870
- "请调大 max_tokens 或对该调用传 thinking=False"
871
- )
891
+ if not body:
892
+ match stream.empty_kind:
893
+ case "budget_exhausted":
894
+ ... # 调大 max_tokens 或 thinking=False
895
+ case "no_finish":
896
+ ... # 优先重试
872
897
  ```
873
898
 
874
899
  - 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
875
- - 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` 判定是否被截断;
876
- - 正文为空且(被截断或发生过思考)时 kit 自行打 `WARNING`,含模型与 `max_tokens`;
900
+ - 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
901
+ - 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
877
902
  - `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
878
903
  - **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
879
904
  - provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;
880
905
  - 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
881
906
 
882
- #### 逐调用思考开关(0.6.7)
907
+ #### 逐调用思考开关
883
908
 
884
909
  同一档案下按调用点切换思考(入库抽取关、章节生成开),不必复制档案:
885
910
 
@@ -898,14 +923,14 @@ kit 不认识任何厂商字段名,请求体字段由档案 `thinking` 声明
898
923
 
899
924
  `thinking_applied`(流式迭代器属性 / `StreamStats` 字段):`None` 未要求干预 / `True` 已按声明注入 / `False` 要求了但被忽略。
900
925
 
901
- > 这是对 0.6.5「不新增 thinking / reasoning_effort 具名字段、只用 `extra_body`」结论的**有意修订**:
902
- > 当时的否决理由(候选链跨厂商,备胎收到未知字段会 400 → 冷却)已由「档案能力声明」机制解除——
926
+ > 设计说明:曾否决「新增 thinking / reasoning_effort 具名字段、只用 `extra_body`」,
927
+ > 该否决理由(候选链跨厂商,备胎收到未知字段会 400 → 冷却)已由「档案能力声明」机制解除——
903
928
  > 字段只发给声明了它的候选,未声明者不注入。思考**档位**(`low/medium/high`)仍然不做。
904
929
 
905
930
  > 首帧等待语义:每候选 60 秒内**一帧都没有**(含推理帧)才切下一候选;思考阶段的
906
931
  > 推理帧算活动并重置窗口,不会误判健康模型失败、也不会重放整段 prompt。
907
932
 
908
- #### 结构化输出(JSON 模式,0.6.8)
933
+ #### 结构化输出(JSON 模式)
909
934
 
910
935
  只要「合法 JSON 对象」用 `json_mode=True`;要约束字段用协议的 `response_format` 原样透传:
911
936
 
@@ -965,7 +990,7 @@ if stats.truncated: # finish_reason 为 length / max_tokens
965
990
  from kit.ai import (
966
991
  ChatService, EmbeddingService, RerankService, ImageService, # 服务类
967
992
  ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest, # 请求模型
968
- RerankHit, StreamStats, ModelBuilder, # 结果类型 / 构建器
993
+ RerankHit, ImageResult, StreamStats, ModelBuilder, # 结果类型 / 构建器
969
994
  )
970
995
 
971
996
  service = ModelBuilder.build_chat() # 老按类型构建:build_chat / build_embedding / build_rerank / build_image
@@ -978,7 +1003,7 @@ await service.close()
978
1003
  | `ChatService` | `ask(messages, *, temperature=None, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats: StreamStats \| None = None, deadline=None) -> str`、`complete(prompt, *, ...) -> str`、`stream(messages, *, ..., stats: StreamStats \| None = None) -> AsyncIterator[str]`(传 `StreamStats()` 即可读收尾统计);`thinking` / `json_mode` / `response_format` 语义与门面一致 |
979
1004
  | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
980
1005
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
981
- | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
1006
+ | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` |
982
1007
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
983
1008
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
984
1009
 
@@ -1230,7 +1255,7 @@ Md2DocKit.convert_md_to_pdf("report.md", mainfont="KaiTi")
1230
1255
 
1231
1256
  | 方法 | 说明 |
1232
1257
  |------|------|
1233
- | `convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体", reference_doc=None) -> str` | Markdown → Word(可插目录、统一字体、图片内嵌) |
1258
+ | `convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体") -> str` | Markdown → Word(可插目录、统一字体、图片内嵌) |
1234
1259
  | `convert_md_to_html(src_path, dest_path=None, *, title=None) -> str` | Markdown → 独立 HTML5 文件(图片输出 `<figure><img>`) |
1235
1260
  | `convert_md_to_pdf(src_path, dest_path=None, *, mainfont="SimSun") -> str` | Markdown → PDF(实验性,内部使用;图片等比限宽居中) |
1236
1261
 
@@ -1278,7 +1303,11 @@ if r.state == "done":
1278
1303
 
1279
1304
  ## kit.docs.marker_kit —— 本地解析:PDF + 图片 → Markdown + Images
1280
1305
 
1281
- 安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大;不含 `[full]`)
1306
+ > ⚠ **0.7.0 起暂停发布**:`marker-pdf` 仍锁 `openai<3`,与 `ai` extra 冲突。本包**不包含**该模块,
1307
+ > 也不提供 `marker` extra。需要此能力请用 `pip install "sdpy-kit[marker]==0.6.10"`。
1308
+ > 以下接口说明仅作参考。
1309
+
1310
+ 0.6.10 的安装方式:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大)
1282
1311
 
1283
1312
  支持面:`.pdf`、`.png`、`.jpg`、`.jpeg`、`.webp`、`.gif`、`.bmp`。docx/pptx/xlsx/html/epub 不在支持面
1284
1313
  (marker 侧实现依赖 weasyprint/GTK),传入会抛 `ValueError`。
@@ -1609,13 +1638,13 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1609
1638
 
1610
1639
  | 层级 | 字段 | 说明 |
1611
1640
  |------|------|------|
1612
- | 档案级 | `endpoint` | 请求路径。**0.6.10 起仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1641
+ | 档案级 | `endpoint` | 请求路径。**仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1613
1642
  | 档案级 | `timeout` | 请求超时秒数(默认 60);SDK 侧为单次调用总超时 |
1614
1643
  | 档案级 | `max_tokens` | 输出上限(chat 用);`AI.chat_ask/chat_complete/chat_stream` 也可按调用点传参,逐级回落:调用点 → 档案 → provider 默认 |
1615
1644
  | 档案级 | `retries` | 5xx 重试次数(默认 2;超时不重试,候选链切换才是有效重试) |
1616
1645
  | 档案级 | `extra_body` | 厂商专有请求体字段透传(如 GLM/Qwen 关闭思考的 `{"thinking": {"type": "disabled"}}`、`{"reasoning_effort": "low"}`);与服务构造的同名键冲突时**以服务的值为准**,故 `model` / `messages` / `stream` 等保留键不可覆盖;改名类需求(如 OpenAI o 系列的 `max_completion_tokens`)无法用它实现。逐调用 `thinking` 与之同键时**以逐调用为准**并打 `WARNING`。改文件后需 `reload_config(reload_file=True)` 生效 |
1617
- | 档案级 | `thinking` | 思考开关的注入声明(0.6.7):`{"arg": "thinking", "off": {"type": "disabled"}, "on": {"type": "enabled"}, "default": false}`。`arg` 是请求体顶层键名(不得占用 `model` / `messages` / `stream` / `max_tokens` / `temperature` 等保留键,配置期与运行期双重校验);`off` / `on` 可为**任意 JSON 值**(双方都是 dict 时浅合并,否则整键替换);`on` 缺省表示不能显式开启;`default` 为档案默认三态(`null` 不干预 / `false` 默认关 / `true` 默认开)。**未声明 = 不支持按调用切换**:逐调用 `thinking` 被忽略并告警(不报错、不影响候选链冷却)。kit 只做翻译,不含任何厂商名 |
1618
- | 档案级 | `structured_output` | 结构化输出的注入声明(0.6.8):`{"arg": "response_format", "json_mode": {"type": "json_object"}}`。`arg` 是请求体顶层键名(不得占用保留键);`json_mode` 是 `json_mode=True` 时写入该键的取值。**未声明 = 用内置 OpenAI 兼容默认**(`response_format: {"type":"json_object"}`,开箱即用);`"structured_output": false` 表示该档案不支持(`json_mode=True` 被忽略 + 告警,不发任何字段)。逐调用 `response_format` 原样透传优先于本声明。kit 只做翻译,不含任何厂商名 |
1646
+ | 档案级 | `thinking` | 思考开关的注入声明:`{"arg": "thinking", "off": {"type": "disabled"}, "on": {"type": "enabled"}, "default": false}`。`arg` 是请求体顶层键名(不得占用 `model` / `messages` / `stream` / `max_tokens` / `temperature` 等保留键,配置期与运行期双重校验);`off` / `on` 可为**任意 JSON 值**(双方都是 dict 时浅合并,否则整键替换);`on` 缺省表示不能显式开启;`default` 为档案默认三态(`null` 不干预 / `false` 默认关 / `true` 默认开)。**未声明 = 不支持按调用切换**:逐调用 `thinking` 被忽略并告警(不报错、不影响候选链冷却)。kit 只做翻译,不含任何厂商名 |
1647
+ | 档案级 | `structured_output` | 结构化输出的注入声明:`{"arg": "response_format", "json_mode": {"type": "json_object"}}`。`arg` 是请求体顶层键名(不得占用保留键);`json_mode` 是 `json_mode=True` 时写入该键的取值。**未声明 = 用内置 OpenAI 兼容默认**(`response_format: {"type":"json_object"}`,开箱即用);`"structured_output": false` 表示该档案不支持(`json_mode=True` 被忽略 + 告警,不发任何字段)。逐调用 `response_format` 原样透传优先于本声明。kit 只做翻译,不含任何厂商名 |
1619
1648
  | 类型级 | `chain_timeout` | 整链预算秒数(与 `default_profile` 同级);门面同步等待上限。未配置时按 `sum(候选 timeout) + 10s` 自适应,也可用环境变量 `KIT_AI_CHAIN_TIMEOUT` 全局覆盖 |
1620
1649
 
1621
1650
  `thinking` 声明两种实测形态(按平台各自写入档案):
@@ -1637,11 +1666,11 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1637
1666
  ```
1638
1667
 
1639
1668
  > **`extra_body` 的用法边界**:`extra_body` 是**档案级**的固有配置,无法按调用点切换。需要
1640
- > 「同一模型,长文开思考、短文关思考」并存时用档案 `thinking` 声明 + 逐调用 `thinking=False/True`
1641
- > (0.6.7 起);仅在无法用开关表达时才考虑复制档案 + `ModelBuilder.build_chat("<档案名>")`
1669
+ > 「同一模型,长文开思考、短文关思考」并存时用档案 `thinking` 声明 + 逐调用 `thinking=False/True`;
1670
+ > 仅在无法用开关表达时才考虑复制档案 + `ModelBuilder.build_chat("<档案名>")`
1642
1671
  > (门面侧 `AI.use("chat", "<档案名>")` 是进程级切换,非并发安全)。
1643
1672
  > 结构化输出同理:档案声明只给 `json_mode=True` 提供取值,按调用点变化的 schema 一律走
1644
- > 逐调用 `response_format`(0.6.8 起)。
1673
+ > 逐调用 `response_format`。
1645
1674
 
1646
1675
  ### 候选链切换语义(两类,勿混淆)
1647
1676
 
@@ -4,7 +4,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
4
4
 
5
5
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
6
6
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
7
- - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
7
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(0.7.0 起暂停发布,需要请用 `sdpy-kit==0.6.10`) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
8
8
 
9
9
  > 要求 Python 3.13+
10
10
 
@@ -24,7 +24,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
24
24
  | `kit.docs.rc_docparser_kit` | `RcDocParserKit` | 离线 docx → Markdown + Images(无模型) | `docx-parse` |
25
25
  | `kit.docs.md2doc_kit` | `Md2DocKit` | Markdown → Word / PDF / HTML(含图片) | `md2doc` |
26
26
  | `kit.docs.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
27
- | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images) | `marker` |
27
+ | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images);**0.7.0 起暂停发布**,需要请用 `sdpy-kit==0.6.10` | — |
28
28
  | `kit.laya_kit` | `LayaKit` | 类型化决策:choice / multi_choice 多选 / score / noul,另含 decide 多字段 / decide_batch 批量 / predict_long 长文本 | `laya` |
29
29
  | `kit.decima_kit` | `DecimaKit` | 决策模型:choice / score / noul / multi_choice(原生 rank);选项乱序不变、置信度可门控;另含 predict / predict_long / brief / choice_batch(方法名与 `LayaKit` 对齐) | `decima` |
30
30
  | `kit.crawl.article_models` | `ArticleInfo` / `ArticleResult` / `SavedOutput` 等 | 文章抓取通用模型(三个 `*ArticleKit` 共用,含 `reason` 分类与非致命 `warnings`) | 无 |
@@ -55,11 +55,10 @@ uv add sdpy-kit
55
55
  | `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
56
56
  | `docx-parse` | RcDocParserKit 离线 docx 解析(rc-docparser → Markdown + images/) | `pip install "sdpy-kit[docx-parse]"` |
57
57
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
58
- | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大)。**与 `[ai]` 互斥**:marker-pdf 锁 `openai<3`,不能与 `openai>=3.44` 同环境安装 | `pip install "sdpy-kit[marker]"` |
59
58
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
60
59
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
61
60
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
62
- | `all` | 常用能力集合,**不含 `marker`**(0.6.11 起,避免与 `[ai]` 的 openai 约束冲突);需要 Marker 请单独装 `[marker]` | `pip install "sdpy-kit[all]"` |
61
+ | `recommended` | 常用能力集合(DB / 缓存 / AI / 文档 / 抓取);不含重依赖(laya / decima)与 Marker(0.7.0 起暂停发布) | `pip install "sdpy-kit[recommended]"` |
63
62
 
64
63
  ## 配置(.env)
65
64
 
@@ -728,7 +727,7 @@ page = qb.paginate(page=1, per_page=20) # {"items", "total", ...}
728
727
 
729
728
  安装:`pip install "sdpy-kit[ai]"`
730
729
 
731
- 传输(0.6.11):chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
730
+ 传输:chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
732
731
  自带 `httpx2`),rerank 走 httpx 自研传输(SDK 无 `/rerank` 资源)。
733
732
  档案里的 `endpoint` **对前三类不再生效**——SDK 固定拼 `/chat/completions`、
734
733
  `/embeddings`、`/images/generations`,仅 rerank 与探活仍使用该字段。
@@ -754,14 +753,29 @@ shutdown() # 应用退出前
754
753
  |------|------|------|
755
754
  | chat | `chat_complete(prompt, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 单轮对话(仅 user 消息) |
756
755
  | chat | `chat_ask(messages, *, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats=None) -> str` | 多轮对话,`messages` 为 `[{"role": "user", "content": "..."}]` |
757
- | 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` / `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` / `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied` |
758
757
  | embedding | `embed_text(text) -> list[float]` | 单文本向量化 |
759
758
  | embedding | `embed_texts(texts) -> list[list[float]]` | 批量向量化 |
760
- | rerank | `rerank(query, documents, top_n=None) -> list[tuple[int, float]]` | 返回 `[(原索引, 相关分)]` 降序列表 |
761
- | image | `generate_image(prompt, *, size=None, n=1) -> str` | 文生图,返回图片 URL |
759
+ | rerank | `rerank(query, documents, top_n=None) -> list[RerankHit]` | 返回 `RerankHit`(`index` / `score` / `document`)降序列表;**`top_n` 是返回条数上限,由 kit 本地裁剪**(服务端返回超量也按分数截断),`top_n=None` 返回全部命中 |
760
+ | image | `generate_image(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` | 文生图,返回 `ImageResult`(`url` / `b64` 至少一个非空) |
762
761
  | 管理 | `use(model_type, profile) -> None` | 切换该类型当前档案(`chat` / `embedding` / `rerank` / `image`) |
763
- | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置与被剔除者) |
764
- | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启 |
762
+ | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置密钥与被剔除者);**类型未在 `ai_models.json` 中配置时返回 `[]`**(不再抛异常),类型名非法仍抛 `AIConfigError` |
763
+ | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启;同时清空思考/结构化输出的告警去重表(修正档案后同一原因会重新提示) |
764
+
765
+ **可用性查询(`available`)**:`available` 返回的是**状态快照**,不是「可用档案列表」。
766
+ 判断某类型是否已配置用 `if not AI.available(t)`,不要靠捕获异常:
767
+
768
+ ```python
769
+ from kit.ai.runner import AI
770
+
771
+ profiles = AI.available("image") # ai_models.json 无 image 段 → []
772
+ if not profiles:
773
+ raise RuntimeError("尚未配置图像生成模型")
774
+ current = next((p for p in profiles if p.current), None)
775
+ ```
776
+
777
+ > 旧写法 `try: AI.available(t) except AIConfigError: ...` 仍可用(异常分支不再触发);
778
+ > `AI.available(t)[0]` 在类型未配置时会抛 `IndexError`,请改用上面的判空写法。
765
779
 
766
780
  ```python
767
781
  from kit.ai.runner import AI
@@ -773,30 +787,42 @@ with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
773
787
 
774
788
  #### 流式收尾统计(思考型模型必备)
775
789
 
776
- 迭代器在流结束后可读六个只读属性(`reasoning_chars` / `content_chars` / `finish_reason` /
777
- `truncated` / `thinking_applied` / `json_mode_applied`),用于区分「模型真的没返回」与「输出预算被思考吃光」——思考型模型的推理
790
+ 迭代器在流结束后可读七个只读属性(`reasoning_chars` / `content_chars` / `finish_reason` /
791
+ `truncated` / `empty_kind` / `thinking_applied` / `json_mode_applied`),用于区分「模型真的没返回」与「输出预算被思考吃光」——思考型模型的推理
778
792
  token 计入 `max_tokens`,长上下文 + 小预算时常出现「返回 200 但正文为空」(同一套字段也可用于非流式 `ask` / `complete`,
779
- 见下文「结构化输出」):
793
+ 见下文「结构化输出」)。
794
+
795
+ `empty_kind` 把「正文为空」分成四类,不再一律归因为预算耗尽:
796
+
797
+ | 取值 | 判定 | 建议处置 |
798
+ |---|---|---|
799
+ | `budget_exhausted` | 被长度截断且有推理 | 调大 `max_tokens`,或对该调用传 `thinking=False` |
800
+ | `reasoning_only` | 有推理、`finish_reason=stop`、正文 0 | 提高预算或换模型 |
801
+ | `no_finish` | 有推理但没收到结束原因(链路中断 / 端点未收尾) | 优先重试,连续出现检查端点/代理超时 |
802
+ | `stopped_empty` | 无推理、`finish_reason=stop`、正文 0 | 检查提示词与模型输出 |
803
+
804
+ 正文非空时 `empty_kind is None`。判定只用「是否有推理 + 结束原因」,不依赖 token 数(kit 目前不采集 `usage`):
780
805
 
781
806
  ```python
782
807
  with AI.chat_stream(messages, max_tokens=450) as stream:
783
808
  body = "".join(stream)
784
- if not body and stream.truncated and stream.reasoning_chars:
785
- raise ValueError(
786
- f"输出预算被思考消耗(推理 {stream.reasoning_chars} 字符),"
787
- "请调大 max_tokens 或对该调用传 thinking=False"
788
- )
809
+ if not body:
810
+ match stream.empty_kind:
811
+ case "budget_exhausted":
812
+ ... # 调大 max_tokens 或 thinking=False
813
+ case "no_finish":
814
+ ... # 优先重试
789
815
  ```
790
816
 
791
817
  - 推理字段兼容 `delta.reasoning_content`(GLM / DeepSeek / Qwen)与 `delta.reasoning`(sensenova):只计数不产出;
792
- - 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` 判定是否被截断;
793
- - 正文为空且(被截断或发生过思考)时 kit 自行打 `WARNING`,含模型与 `max_tokens`;
818
+ - 推理字段名是 kit 内部白名单(当前就是上面两种):新 provider 若用别的字段名,`reasoning_chars` 会恒为 0——此时仍可用 `truncated` / `empty_kind` 判定;
819
+ - 正文为空时 kit 按 `empty_kind` 分级打 `WARNING`(含模型、`max_tokens`、`finish_reason` 与推荐处置),不再统一归因「预算被思考耗尽」;
794
820
  - `truncated` 覆盖 `finish_reason` 为 `length` 或 `max_tokens` 两种口径;
795
821
  - **字符数 ≠ token 数**(实测中文约 2~2.5 字符/token,各家口径不一),不要用 `reasoning_chars` 反推还差多少预算;
796
822
  - provider 不返回 `finish_reason` 时 `finish_reason is None`、`truncated is False`;
797
823
  - 流式过程中读到的是尽力而为的中间值,**结束后才是最终值**;提前 `break` 时为截至中断的累计值。
798
824
 
799
- #### 逐调用思考开关(0.6.7)
825
+ #### 逐调用思考开关
800
826
 
801
827
  同一档案下按调用点切换思考(入库抽取关、章节生成开),不必复制档案:
802
828
 
@@ -815,14 +841,14 @@ kit 不认识任何厂商字段名,请求体字段由档案 `thinking` 声明
815
841
 
816
842
  `thinking_applied`(流式迭代器属性 / `StreamStats` 字段):`None` 未要求干预 / `True` 已按声明注入 / `False` 要求了但被忽略。
817
843
 
818
- > 这是对 0.6.5「不新增 thinking / reasoning_effort 具名字段、只用 `extra_body`」结论的**有意修订**:
819
- > 当时的否决理由(候选链跨厂商,备胎收到未知字段会 400 → 冷却)已由「档案能力声明」机制解除——
844
+ > 设计说明:曾否决「新增 thinking / reasoning_effort 具名字段、只用 `extra_body`」,
845
+ > 该否决理由(候选链跨厂商,备胎收到未知字段会 400 → 冷却)已由「档案能力声明」机制解除——
820
846
  > 字段只发给声明了它的候选,未声明者不注入。思考**档位**(`low/medium/high`)仍然不做。
821
847
 
822
848
  > 首帧等待语义:每候选 60 秒内**一帧都没有**(含推理帧)才切下一候选;思考阶段的
823
849
  > 推理帧算活动并重置窗口,不会误判健康模型失败、也不会重放整段 prompt。
824
850
 
825
- #### 结构化输出(JSON 模式,0.6.8)
851
+ #### 结构化输出(JSON 模式)
826
852
 
827
853
  只要「合法 JSON 对象」用 `json_mode=True`;要约束字段用协议的 `response_format` 原样透传:
828
854
 
@@ -882,7 +908,7 @@ if stats.truncated: # finish_reason 为 length / max_tokens
882
908
  from kit.ai import (
883
909
  ChatService, EmbeddingService, RerankService, ImageService, # 服务类
884
910
  ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest, # 请求模型
885
- RerankHit, StreamStats, ModelBuilder, # 结果类型 / 构建器
911
+ RerankHit, ImageResult, StreamStats, ModelBuilder, # 结果类型 / 构建器
886
912
  )
887
913
 
888
914
  service = ModelBuilder.build_chat() # 老按类型构建:build_chat / build_embedding / build_rerank / build_image
@@ -895,7 +921,7 @@ await service.close()
895
921
  | `ChatService` | `ask(messages, *, temperature=None, max_tokens=None, thinking=None, json_mode=None, response_format=None, stats: StreamStats \| None = None, deadline=None) -> str`、`complete(prompt, *, ...) -> str`、`stream(messages, *, ..., stats: StreamStats \| None = None) -> AsyncIterator[str]`(传 `StreamStats()` 即可读收尾统计);`thinking` / `json_mode` / `response_format` 语义与门面一致 |
896
922
  | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
897
923
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
898
- | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
924
+ | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` |
899
925
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
900
926
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
901
927
 
@@ -1147,7 +1173,7 @@ Md2DocKit.convert_md_to_pdf("report.md", mainfont="KaiTi")
1147
1173
 
1148
1174
  | 方法 | 说明 |
1149
1175
  |------|------|
1150
- | `convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体", reference_doc=None) -> str` | Markdown → Word(可插目录、统一字体、图片内嵌) |
1176
+ | `convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体") -> str` | Markdown → Word(可插目录、统一字体、图片内嵌) |
1151
1177
  | `convert_md_to_html(src_path, dest_path=None, *, title=None) -> str` | Markdown → 独立 HTML5 文件(图片输出 `<figure><img>`) |
1152
1178
  | `convert_md_to_pdf(src_path, dest_path=None, *, mainfont="SimSun") -> str` | Markdown → PDF(实验性,内部使用;图片等比限宽居中) |
1153
1179
 
@@ -1195,7 +1221,11 @@ if r.state == "done":
1195
1221
 
1196
1222
  ## kit.docs.marker_kit —— 本地解析:PDF + 图片 → Markdown + Images
1197
1223
 
1198
- 安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大;不含 `[full]`)
1224
+ > ⚠ **0.7.0 起暂停发布**:`marker-pdf` 仍锁 `openai<3`,与 `ai` extra 冲突。本包**不包含**该模块,
1225
+ > 也不提供 `marker` extra。需要此能力请用 `pip install "sdpy-kit[marker]==0.6.10"`。
1226
+ > 以下接口说明仅作参考。
1227
+
1228
+ 0.6.10 的安装方式:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大)
1199
1229
 
1200
1230
  支持面:`.pdf`、`.png`、`.jpg`、`.jpeg`、`.webp`、`.gif`、`.bmp`。docx/pptx/xlsx/html/epub 不在支持面
1201
1231
  (marker 侧实现依赖 weasyprint/GTK),传入会抛 `ValueError`。
@@ -1526,13 +1556,13 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1526
1556
 
1527
1557
  | 层级 | 字段 | 说明 |
1528
1558
  |------|------|------|
1529
- | 档案级 | `endpoint` | 请求路径。**0.6.10 起仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1559
+ | 档案级 | `endpoint` | 请求路径。**仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1530
1560
  | 档案级 | `timeout` | 请求超时秒数(默认 60);SDK 侧为单次调用总超时 |
1531
1561
  | 档案级 | `max_tokens` | 输出上限(chat 用);`AI.chat_ask/chat_complete/chat_stream` 也可按调用点传参,逐级回落:调用点 → 档案 → provider 默认 |
1532
1562
  | 档案级 | `retries` | 5xx 重试次数(默认 2;超时不重试,候选链切换才是有效重试) |
1533
1563
  | 档案级 | `extra_body` | 厂商专有请求体字段透传(如 GLM/Qwen 关闭思考的 `{"thinking": {"type": "disabled"}}`、`{"reasoning_effort": "low"}`);与服务构造的同名键冲突时**以服务的值为准**,故 `model` / `messages` / `stream` 等保留键不可覆盖;改名类需求(如 OpenAI o 系列的 `max_completion_tokens`)无法用它实现。逐调用 `thinking` 与之同键时**以逐调用为准**并打 `WARNING`。改文件后需 `reload_config(reload_file=True)` 生效 |
1534
- | 档案级 | `thinking` | 思考开关的注入声明(0.6.7):`{"arg": "thinking", "off": {"type": "disabled"}, "on": {"type": "enabled"}, "default": false}`。`arg` 是请求体顶层键名(不得占用 `model` / `messages` / `stream` / `max_tokens` / `temperature` 等保留键,配置期与运行期双重校验);`off` / `on` 可为**任意 JSON 值**(双方都是 dict 时浅合并,否则整键替换);`on` 缺省表示不能显式开启;`default` 为档案默认三态(`null` 不干预 / `false` 默认关 / `true` 默认开)。**未声明 = 不支持按调用切换**:逐调用 `thinking` 被忽略并告警(不报错、不影响候选链冷却)。kit 只做翻译,不含任何厂商名 |
1535
- | 档案级 | `structured_output` | 结构化输出的注入声明(0.6.8):`{"arg": "response_format", "json_mode": {"type": "json_object"}}`。`arg` 是请求体顶层键名(不得占用保留键);`json_mode` 是 `json_mode=True` 时写入该键的取值。**未声明 = 用内置 OpenAI 兼容默认**(`response_format: {"type":"json_object"}`,开箱即用);`"structured_output": false` 表示该档案不支持(`json_mode=True` 被忽略 + 告警,不发任何字段)。逐调用 `response_format` 原样透传优先于本声明。kit 只做翻译,不含任何厂商名 |
1564
+ | 档案级 | `thinking` | 思考开关的注入声明:`{"arg": "thinking", "off": {"type": "disabled"}, "on": {"type": "enabled"}, "default": false}`。`arg` 是请求体顶层键名(不得占用 `model` / `messages` / `stream` / `max_tokens` / `temperature` 等保留键,配置期与运行期双重校验);`off` / `on` 可为**任意 JSON 值**(双方都是 dict 时浅合并,否则整键替换);`on` 缺省表示不能显式开启;`default` 为档案默认三态(`null` 不干预 / `false` 默认关 / `true` 默认开)。**未声明 = 不支持按调用切换**:逐调用 `thinking` 被忽略并告警(不报错、不影响候选链冷却)。kit 只做翻译,不含任何厂商名 |
1565
+ | 档案级 | `structured_output` | 结构化输出的注入声明:`{"arg": "response_format", "json_mode": {"type": "json_object"}}`。`arg` 是请求体顶层键名(不得占用保留键);`json_mode` 是 `json_mode=True` 时写入该键的取值。**未声明 = 用内置 OpenAI 兼容默认**(`response_format: {"type":"json_object"}`,开箱即用);`"structured_output": false` 表示该档案不支持(`json_mode=True` 被忽略 + 告警,不发任何字段)。逐调用 `response_format` 原样透传优先于本声明。kit 只做翻译,不含任何厂商名 |
1536
1566
  | 类型级 | `chain_timeout` | 整链预算秒数(与 `default_profile` 同级);门面同步等待上限。未配置时按 `sum(候选 timeout) + 10s` 自适应,也可用环境变量 `KIT_AI_CHAIN_TIMEOUT` 全局覆盖 |
1537
1567
 
1538
1568
  `thinking` 声明两种实测形态(按平台各自写入档案):
@@ -1554,11 +1584,11 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1554
1584
  ```
1555
1585
 
1556
1586
  > **`extra_body` 的用法边界**:`extra_body` 是**档案级**的固有配置,无法按调用点切换。需要
1557
- > 「同一模型,长文开思考、短文关思考」并存时用档案 `thinking` 声明 + 逐调用 `thinking=False/True`
1558
- > (0.6.7 起);仅在无法用开关表达时才考虑复制档案 + `ModelBuilder.build_chat("<档案名>")`
1587
+ > 「同一模型,长文开思考、短文关思考」并存时用档案 `thinking` 声明 + 逐调用 `thinking=False/True`;
1588
+ > 仅在无法用开关表达时才考虑复制档案 + `ModelBuilder.build_chat("<档案名>")`
1559
1589
  > (门面侧 `AI.use("chat", "<档案名>")` 是进程级切换,非并发安全)。
1560
1590
  > 结构化输出同理:档案声明只给 `json_mode=True` 提供取值,按调用点变化的 schema 一律走
1561
- > 逐调用 `response_format`(0.6.8 起)。
1591
+ > 逐调用 `response_format`。
1562
1592
 
1563
1593
  ### 候选链切换语义(两类,勿混淆)
1564
1594
 
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.6.11"
26
+ __version__ = "0.7.1"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -29,7 +29,7 @@ Author: Clark Chang
29
29
 
30
30
  from kit.ai.chat_service import ChatRequest, ChatService, StreamStats
31
31
  from kit.ai.embedding_service import EmbeddingRequest, EmbeddingService
32
- from kit.ai.image_service import ImageRequest, ImageService
32
+ from kit.ai.image_service import ImageRequest, ImageResult, ImageService
33
33
  from kit.ai.model_builder import ModelBuilder
34
34
  from kit.ai.rerank_service import RerankHit, RerankRequest, RerankService
35
35
 
@@ -39,6 +39,7 @@ __all__ = [
39
39
  "EmbeddingRequest",
40
40
  "EmbeddingService",
41
41
  "ImageRequest",
42
+ "ImageResult",
42
43
  "ImageService",
43
44
  "ModelBuilder",
44
45
  "RerankHit",