sdpy-kit 0.6.11__tar.gz → 0.7.0__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.0}/PKG-INFO +34 -32
  2. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/README.md +11 -8
  3. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/__init__.py +2 -1
  5. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/chat_service.py +33 -16
  6. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/failover.py +13 -5
  7. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/image_service.py +18 -6
  8. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/model_client.py +25 -5
  9. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/pool.py +2 -2
  10. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/rerank_service.py +22 -10
  11. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/runner.py +40 -14
  12. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/sdk_client.py +33 -27
  13. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/__init__.py +1 -0
  14. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/pyproject.toml +8 -2
  15. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/.gitignore +0 -0
  16. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/_retry_after.py +0 -0
  17. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/_service_base.py +0 -0
  18. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/embedding_service.py +0 -0
  19. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/exceptions.py +0 -0
  20. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/model_builder.py +0 -0
  21. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/profiles.py +0 -0
  22. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/rerank_transport.py +0 -0
  23. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/ai/types.py +0 -0
  24. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/common_kit.py +0 -0
  25. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/config/__init__.py +0 -0
  26. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/config/kit_config.py +0 -0
  27. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/config/log_config.py +0 -0
  28. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/__init__.py +0 -0
  29. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/article_io.py +0 -0
  30. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/article_models.py +0 -0
  31. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/deepseek_kit.py +0 -0
  32. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/web_article_kit.py +0 -0
  33. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/crawl/wx_article_kit.py +0 -0
  34. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/__init__.py +0 -0
  35. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/audit.py +0 -0
  36. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/base_db_kit.py +0 -0
  37. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/crud.py +0 -0
  38. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/engine.py +0 -0
  39. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/exceptions.py +0 -0
  40. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/models/__init__.py +0 -0
  41. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/models/base.py +0 -0
  42. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/mysql_kit.py +0 -0
  43. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/pg_kit.py +0 -0
  44. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/query.py +0 -0
  45. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/session.py +0 -0
  46. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/sqlite_kit.py +0 -0
  47. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/db/transaction.py +0 -0
  48. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/decima_kit.py +0 -0
  49. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/anydoc_kit.py +0 -0
  50. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/docx_kit.py +0 -0
  51. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/marker_kit.py +0 -0
  52. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/md2doc_kit.py +0 -0
  53. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/mineru_kit.py +0 -0
  54. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/docs/rc_docparser_kit.py +0 -0
  55. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/file_kit.py +0 -0
  56. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/laya_kit.py +0 -0
  57. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/kit/license_kit.py +0 -0
  58. {sdpy_kit-0.6.11 → sdpy_kit-0.7.0}/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.0
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
@@ -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
 
@@ -840,8 +838,8 @@ shutdown() # 应用退出前
840
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` / `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`)降序列表 |
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
844
  | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置与被剔除者) |
847
845
  | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启 |
@@ -965,7 +963,7 @@ if stats.truncated: # finish_reason 为 length / max_tokens
965
963
  from kit.ai import (
966
964
  ChatService, EmbeddingService, RerankService, ImageService, # 服务类
967
965
  ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest, # 请求模型
968
- RerankHit, StreamStats, ModelBuilder, # 结果类型 / 构建器
966
+ RerankHit, ImageResult, StreamStats, ModelBuilder, # 结果类型 / 构建器
969
967
  )
970
968
 
971
969
  service = ModelBuilder.build_chat() # 老按类型构建:build_chat / build_embedding / build_rerank / build_image
@@ -978,7 +976,7 @@ await service.close()
978
976
  | `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
977
  | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
980
978
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
981
- | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
979
+ | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` |
982
980
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
983
981
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
984
982
 
@@ -1278,7 +1276,11 @@ if r.state == "done":
1278
1276
 
1279
1277
  ## kit.docs.marker_kit —— 本地解析:PDF + 图片 → Markdown + Images
1280
1278
 
1281
- 安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大;不含 `[full]`)
1279
+ > ⚠ **0.7.0 起暂停发布**:`marker-pdf` 仍锁 `openai<3`,与 `ai` extra 冲突。本包**不包含**该模块,
1280
+ > 也不提供 `marker` extra。需要此能力请用 `pip install "sdpy-kit[marker]==0.6.10"`。
1281
+ > 以下接口说明仅作参考。
1282
+
1283
+ 0.6.10 的安装方式:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大)
1282
1284
 
1283
1285
  支持面:`.pdf`、`.png`、`.jpg`、`.jpeg`、`.webp`、`.gif`、`.bmp`。docx/pptx/xlsx/html/epub 不在支持面
1284
1286
  (marker 侧实现依赖 weasyprint/GTK),传入会抛 `ValueError`。
@@ -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
 
@@ -757,8 +756,8 @@ shutdown() # 应用退出前
757
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` / `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`)降序列表 |
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
762
  | 管理 | `available(model_type) -> list[ProfileStatus]` | 各档案状态快照(含未配置与被剔除者) |
764
763
  | 管理 | `reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult` | 配置热重载,改密钥 / 档案后无需重启 |
@@ -882,7 +881,7 @@ if stats.truncated: # finish_reason 为 length / max_tokens
882
881
  from kit.ai import (
883
882
  ChatService, EmbeddingService, RerankService, ImageService, # 服务类
884
883
  ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest, # 请求模型
885
- RerankHit, StreamStats, ModelBuilder, # 结果类型 / 构建器
884
+ RerankHit, ImageResult, StreamStats, ModelBuilder, # 结果类型 / 构建器
886
885
  )
887
886
 
888
887
  service = ModelBuilder.build_chat() # 老按类型构建:build_chat / build_embedding / build_rerank / build_image
@@ -895,7 +894,7 @@ await service.close()
895
894
  | `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
895
  | `EmbeddingService` | `embed_text(text) -> list[float]`、`embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]`、`dimension` 属性(**目标维度**,见下) |
897
896
  | `RerankService` | `rerank(query, documents, *, top_n=None) -> list[RerankHit]`(已按 score 降序) |
898
- | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str` |
897
+ | `ImageService` | `generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> ImageResult` |
899
898
  | `ModelBuilder` | `build(model_type, model=None, *, api_key=None)` 及各类型专用 `build_chat / build_embedding / build_rerank / build_image` |
900
899
  | `BaseAIService` | `profile` 属性、`await close()`、支持 `async with` |
901
900
 
@@ -1195,7 +1194,11 @@ if r.state == "done":
1195
1194
 
1196
1195
  ## kit.docs.marker_kit —— 本地解析:PDF + 图片 → Markdown + Images
1197
1196
 
1198
- 安装:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大;不含 `[full]`)
1197
+ > ⚠ **0.7.0 起暂停发布**:`marker-pdf` 仍锁 `openai<3`,与 `ai` extra 冲突。本包**不包含**该模块,
1198
+ > 也不提供 `marker` extra。需要此能力请用 `pip install "sdpy-kit[marker]==0.6.10"`。
1199
+ > 以下接口说明仅作参考。
1200
+
1201
+ 0.6.10 的安装方式:`pip install "sdpy-kit[marker]"`(依赖 `marker-pdf`,含 torch 等 80+ 包,体积较大)
1199
1202
 
1200
1203
  支持面:`.pdf`、`.png`、`.jpg`、`.jpeg`、`.webp`、`.gif`、`.bmp`。docx/pptx/xlsx/html/epub 不在支持面
1201
1204
  (marker 侧实现依赖 weasyprint/GTK),传入会抛 `ValueError`。
@@ -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.0"
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",
@@ -55,37 +55,51 @@ _WARNED: set[tuple[str, str]] = set()
55
55
  _WARNED_LOCK: Final[threading.Lock] = threading.Lock()
56
56
 
57
57
 
58
- def _warn_once(profile_name: str, reason: str, *, kind: str = "思考开关") -> None:
59
- """线程安全的告警去重:``(档案名, 原因)`` 只报一次。"""
58
+ def _warn_once(
59
+ profile_name: str,
60
+ reason: str,
61
+ *,
62
+ kind: str = "思考开关",
63
+ level: int = logging.WARNING,
64
+ ) -> None:
65
+ """线程安全的告警去重:``(档案名, 原因)`` 只报一次(可指定级别)。"""
60
66
  key = (profile_name, reason)
61
67
  with _WARNED_LOCK:
62
68
  if key in _WARNED:
63
69
  return
64
70
  _WARNED.add(key)
65
- logger.warning(f"{kind}:{reason}(档案 {profile_name})")
71
+ logger.log(level, f"{kind}:{reason}(档案 {profile_name})")
66
72
 
67
73
 
68
74
  def _resolve_thinking(
69
75
  profile: ModelProfile, thinking: bool | None
70
- ) -> tuple[object, bool | None, str | None]:
76
+ ) -> tuple[object, bool | None, str | None, int]:
71
77
  """解析本次调用的思考开关。
72
78
 
73
79
  逐调用 ``thinking`` 优先,未传时回落档案 ``thinking.default``(三态)。
74
80
 
75
81
  Returns:
76
- ``(注入值 | _UNSET, 是否生效, 告警原因)``。是否生效为 None 表示
77
- 本次未要求干预(请求体不变);False 表示要求了但档案不支持
78
- (不注入、不报错、不影响候选链冷却)。
82
+ ``(注入值 | _UNSET, 是否生效, 提示语, 日志级别)``。
83
+ 是否生效为 None 表示本次未对请求体做思考相关注入(未要求,或档案
84
+ 只声明了 ``off`` 而没声明 ``on``——按厂商默认处理);False 表示
85
+ 要求了但档案完全没有 ``thinking`` 声明(不支持、不注入、不报错、
86
+ 不影响候选链冷却)。
79
87
  """
80
88
  spec = profile.thinking
81
89
  mode = thinking if thinking is not None else (spec.default if spec else None)
82
90
  if mode is None:
83
- return _UNSET, None, None
91
+ return _UNSET, None, None, logging.WARNING
84
92
  if spec is None:
85
- return _UNSET, False, "档案未声明思考开关,本次请求被忽略"
93
+ return _UNSET, False, "档案未声明思考开关,本次请求被忽略", logging.WARNING
86
94
  if mode and spec.on is None:
87
- return _UNSET, False, "档案未声明开启取值,本次请求被忽略"
88
- return (spec.off if mode is False else spec.on), True, None
95
+ # 只声明 off:开启只能走厂商默认(主流模型默认开启),不算「未生效」
96
+ return (
97
+ _UNSET,
98
+ None,
99
+ "档案未声明开启取值,按厂商默认处理",
100
+ logging.INFO,
101
+ )
102
+ return (spec.off if mode is False else spec.on), True, None, logging.WARNING
89
103
 
90
104
 
91
105
  @dataclass
@@ -106,8 +120,9 @@ class StreamStats:
106
120
  content_chars: 已产出正文字符数(等于调用方收到的字符数)
107
121
  finish_reason: 结束原因(stop / length / content_filter / …);
108
122
  provider 未给出时为 None
109
- thinking_applied: 思考开关是否生效——None 本次未要求干预 /
110
- True 已按档案声明注入 / False 要求了但档案不支持(已忽略)
123
+ thinking_applied: 思考开关是否生效——None 本次未对请求体做思考注入
124
+ (未要求,或档案只声明 off 未声明 on,按厂商默认处理)/
125
+ True 已按档案声明注入 / False 要求了但档案完全没有 thinking 声明
111
126
  json_mode_applied: 结构化输出是否生效——None 本次未要求注入 /
112
127
  True 已注入 / False 要求了但档案声明不支持(已忽略)
113
128
  """
@@ -206,11 +221,13 @@ class ChatService(SdkServiceBase):
206
221
  if key not in _RESERVED_PAYLOAD_KEYS
207
222
  }
208
223
 
209
- thinking_value, thinking_applied, thinking_reason = _resolve_thinking(
210
- self._profile, request.thinking
224
+ thinking_value, thinking_applied, thinking_reason, thinking_level = (
225
+ _resolve_thinking(self._profile, request.thinking)
211
226
  )
212
227
  if thinking_reason is not None:
213
- _warn_once(self._profile.name, thinking_reason)
228
+ _warn_once(
229
+ self._profile.name, thinking_reason, level=thinking_level
230
+ )
214
231
  if thinking_value is not _UNSET:
215
232
  spec = self._profile.thinking
216
233
  if spec is None: # 类型收窄:applied 为 True 时 spec 必非 None
@@ -40,7 +40,7 @@ from typing import Any, Final, TypeVar, cast
40
40
  from kit.ai.chat_service import ChatService, StreamStats
41
41
  from kit.ai.embedding_service import EmbeddingService
42
42
  from kit.ai.exceptions import AIAggregateError, AIError, AIRequestError, AITimeoutError
43
- from kit.ai.image_service import ImageService
43
+ from kit.ai.image_service import ImageResult, ImageService
44
44
  from kit.ai.model_builder import Service
45
45
  from kit.ai.pool import AIServicePool
46
46
  from kit.ai.rerank_service import RerankHit, RerankService
@@ -422,11 +422,19 @@ class FailoverImageService(_FailoverBase):
422
422
  *,
423
423
  size: str | None = None,
424
424
  n: int = 1,
425
+ return_base64: bool = False,
426
+ ratio: str | None = None,
425
427
  deadline: float | None = None,
426
- ) -> str:
427
- """文生图,返回图片 URL(链式故障切换)。"""
428
+ ) -> ImageResult:
429
+ """文生图,返回 :class:`ImageResult`(链式故障切换)。"""
428
430
 
429
- async def _run(service: Service) -> str:
430
- return await cast(ImageService, service).generate(prompt, size=size, n=n)
431
+ async def _run(service: Service) -> ImageResult:
432
+ return await cast(ImageService, service).generate(
433
+ prompt,
434
+ size=size,
435
+ n=n,
436
+ return_base64=return_base64,
437
+ ratio=ratio,
438
+ )
431
439
 
432
440
  return await self._attempt("图像生成", _run, deadline=deadline)
@@ -26,7 +26,19 @@ from typing import ClassVar
26
26
 
27
27
  from kit.ai.sdk_client import SdkServiceBase
28
28
 
29
- __all__ = ["ImageRequest", "ImageService"]
29
+ __all__ = ["ImageRequest", "ImageResult", "ImageService"]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ImageResult:
34
+ """图像生成结果。
35
+
36
+ ``url`` 与 ``b64`` 至少一个非空;两者都有时都填充(不同厂商回退行为不同)。
37
+ 需要二进制时:有 ``b64`` 直接解码,否则下载 ``url``。
38
+ """
39
+
40
+ url: str | None
41
+ b64: str | None
30
42
 
31
43
 
32
44
  @dataclass(frozen=True)
@@ -50,8 +62,7 @@ class ImageService(SdkServiceBase):
50
62
  """图像生成服务(全异步)。
51
63
 
52
64
  ``n > 1`` 时仅返回第一张图片,调用方如需多图请自行扩展。
53
- 返回 ``str`` 语义:默认返回图片 URL;``return_base64=True`` 时
54
- 返回 Base64 数据,调用方按返回值是否以 ``http`` 开头区分形式。
65
+ 返回 :class:`ImageResult`(``url`` / ``b64`` 至少一个非空)。
55
66
  """
56
67
 
57
68
  _LABEL: ClassVar[str] = "图像生成模型"
@@ -64,8 +75,8 @@ class ImageService(SdkServiceBase):
64
75
  n: int = 1,
65
76
  return_base64: bool = False,
66
77
  ratio: str | None = None,
67
- ) -> str:
68
- """文生图,返回图片 URL 或 Base64 数据。"""
78
+ ) -> ImageResult:
79
+ """文生图,返回 :class:`ImageResult`。"""
69
80
  extra: dict[str, object] = {}
70
81
  if return_base64:
71
82
  extra["return_base64"] = True
@@ -81,9 +92,10 @@ class ImageService(SdkServiceBase):
81
92
  }
82
93
  )
83
94
  with self._log_call():
84
- return await self._client.image(
95
+ url, b64 = await self._client.image(
85
96
  prompt,
86
97
  size=size,
87
98
  n=n,
88
99
  extra_body=extra or None,
89
100
  )
101
+ return ImageResult(url=url, b64=b64)
@@ -18,23 +18,31 @@ Author: Clark Chang
18
18
  """
19
19
 
20
20
  # date: 2026-11-04
21
- """模型档案轻探测客户端(0.6.10 起仅保留探活能力)。
21
+ """模型档案轻探测客户端(0.7.0 起更名 ``ProbeClient``)。
22
22
 
23
23
  破坏性变更:``post_json`` / ``stream_json`` 已删除——请求发送改由
24
24
  :class:`kit.ai.sdk_client.SdkClient` 承担,rerank 走
25
25
  :class:`kit.ai.rerank_transport.RerankTransport`。本类只保留
26
26
  ``list_models()`` / ``close()``,供业务侧做「密钥是否有效」的连通性测试。
27
+
28
+ ``ModelClient`` 作为弃用别名保留一个版本(访问时触发 ``DeprecationWarning``)。
27
29
  """
28
30
 
29
- from kit.ai.sdk_client import SdkClient
31
+ import warnings
32
+
30
33
  from kit.ai.profiles import ModelProfile
34
+ from kit.ai.sdk_client import SdkClient
31
35
  from kit.ai.types import JSONObject
32
36
 
33
- __all__ = ["ModelClient"]
37
+ __all__ = ["ProbeClient"]
34
38
 
35
39
 
36
- class ModelClient:
37
- """单档案连通性探测客户端(全异步)。"""
40
+ class ProbeClient:
41
+ """单档案连通性探测客户端(全异步)。
42
+
43
+ 0.7.0 起由 ``ModelClient`` 改名:它只做 ``list_models`` 探活,
44
+ 真正的传输在 :class:`kit.ai.sdk_client.SdkClient`。
45
+ """
38
46
 
39
47
  def __init__(self, profile: ModelProfile) -> None:
40
48
  self._profile: ModelProfile = profile
@@ -57,3 +65,15 @@ class ModelClient:
57
65
 
58
66
  async def close(self) -> None:
59
67
  await self._client.close()
68
+
69
+
70
+ def __getattr__(name: str) -> object:
71
+ """0.7.0 起 ``ModelClient`` 更名为 ``ProbeClient``:保留别名并告警。"""
72
+ if name == "ModelClient":
73
+ warnings.warn(
74
+ "ModelClient 已更名为 ProbeClient(0.7.0),请改用 ProbeClient",
75
+ DeprecationWarning,
76
+ stacklevel=2,
77
+ )
78
+ return ProbeClient
79
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -45,7 +45,7 @@ from kit.ai.embedding_service import EmbeddingService
45
45
  from kit.ai.exceptions import AIConfigError, AIError, AIRequestError
46
46
  from kit.ai.image_service import ImageService
47
47
  from kit.ai.model_builder import Service
48
- from kit.ai.model_client import ModelClient
48
+ from kit.ai.model_client import ProbeClient
49
49
  from kit.ai.profiles import (
50
50
  ModelProfile,
51
51
  ModelRegistry,
@@ -85,7 +85,7 @@ async def _probe_profile(profile: ModelProfile) -> None:
85
85
 
86
86
  独立模块级函数便于测试替换。探测客户端用毕即关。
87
87
  """
88
- client = ModelClient(profile)
88
+ client = ProbeClient(profile)
89
89
  try:
90
90
  await client.list_models()
91
91
  finally:
@@ -43,10 +43,15 @@ class RerankRequest:
43
43
 
44
44
  @dataclass(frozen=True)
45
45
  class RerankHit:
46
- """重排序单条命中结果。"""
46
+ """重排序单条命中结果。
47
+
48
+ ``index`` 保留是为了让调用方映射回自己的原始对象(如 dict 列表);
49
+ ``document`` 是命中原文,只要文本时免去二次索引。
50
+ """
47
51
 
48
52
  index: int
49
53
  score: float
54
+ document: str
50
55
 
51
56
 
52
57
  class RerankService(LoggingServiceBase):
@@ -68,7 +73,9 @@ class RerankService(LoggingServiceBase):
68
73
  payload["top_n"] = request.top_n
69
74
  return cast(JSONObject, payload)
70
75
 
71
- def _parse_response(self, data: JSONObject) -> list[RerankHit]:
76
+ def _parse_response(
77
+ self, data: JSONObject, documents: list[str]
78
+ ) -> list[RerankHit]:
72
79
  items = safe_get(data, "results")
73
80
  if not isinstance(items, list):
74
81
  raise AIResponseError(f"重排序响应 results 必须是数组: {str(data)[:200]}")
@@ -80,7 +87,18 @@ class RerankService(LoggingServiceBase):
80
87
  score = item.get("relevance_score")
81
88
  if not isinstance(index, int) or not isinstance(score, (int, float)):
82
89
  raise AIResponseError(f"重排序响应字段异常: {str(item)[:200]}")
83
- hits.append(RerankHit(index=index, score=float(score)))
90
+ # 先校验越界再构造:否则 documents[index] 会抛 IndexError
91
+ if index < 0 or index >= len(documents):
92
+ raise AIResponseError(
93
+ f"重排序 index 越界: {index} (文档数 {len(documents)})"
94
+ )
95
+ hits.append(
96
+ RerankHit(
97
+ index=index,
98
+ score=float(score),
99
+ document=documents[index],
100
+ )
101
+ )
84
102
  hits.sort(key=lambda h: h.score, reverse=True)
85
103
  return hits
86
104
 
@@ -103,13 +121,7 @@ class RerankService(LoggingServiceBase):
103
121
  payload = self._build_payload(RerankRequest(query, documents, top_n=top_n))
104
122
  with self._log_call():
105
123
  data = await self._client.post_json(payload, deadline=deadline)
106
- hits = self._parse_response(data)
107
- for hit in hits:
108
- if hit.index < 0 or hit.index >= len(documents):
109
- raise AIResponseError(
110
- f"重排序 index 越界: {hit.index} (文档数 {len(documents)})"
111
- )
112
- return hits
124
+ return self._parse_response(data, documents)
113
125
 
114
126
  async def close(self) -> None:
115
127
  await self._client.close()
@@ -57,7 +57,7 @@ from __future__ import annotations
57
57
  而非经本门面阻塞事件循环线程。
58
58
  - ``chat_stream`` 提供同步流式迭代(每次取下一帧),适合同步线程内逐 token 消费。
59
59
  - ``AI.use`` / ``AI.available`` 为进程内内存态(不持久化),进程重启后
60
- 回到 env(``AI_{TYPE}_PROFILES``)/ JSON 声明顺序。
60
+ 回到 JSON 声明顺序。
61
61
  """
62
62
 
63
63
  import asyncio
@@ -81,6 +81,7 @@ from kit.ai.failover import (
81
81
  from kit.ai.model_builder import Service
82
82
  from kit.ai.pool import AIServicePool
83
83
  from kit.ai.profiles import ProfileStatus, get_model_registry
84
+ from kit.ai.image_service import ImageResult
84
85
  from kit.ai.rerank_service import RerankHit
85
86
  from kit.config.kit_config import get_settings
86
87
  from kit.config.log_config import get_default_logger
@@ -545,27 +546,38 @@ async def _rerank(
545
546
  top_n: int | None,
546
547
  *,
547
548
  deadline: float | None = None,
548
- ) -> list[tuple[int, float]]:
549
+ ) -> list[RerankHit]:
550
+ """对候选文档重排序,返回 ``RerankHit`` 降序列表。"""
549
551
  if not documents:
550
552
  return []
551
553
  service: FailoverRerankService = cast(
552
554
  FailoverRerankService,
553
555
  await _background._get_service(_RERANK), # noqa: SLF001
554
556
  )
555
- hits: list[RerankHit] = await service.rerank(
556
- query, documents, top_n=top_n, deadline=deadline
557
- )
558
- return [(hit.index, hit.score) for hit in hits]
557
+ return await service.rerank(query, documents, top_n=top_n, deadline=deadline)
559
558
 
560
559
 
561
560
  async def _generate_image(
562
- prompt: str, *, size: str | None, n: int, deadline: float | None = None
563
- ) -> str:
561
+ prompt: str,
562
+ *,
563
+ size: str | None,
564
+ n: int,
565
+ return_base64: bool = False,
566
+ ratio: str | None = None,
567
+ deadline: float | None = None,
568
+ ) -> ImageResult:
564
569
  service: FailoverImageService = cast(
565
570
  FailoverImageService,
566
571
  await _background._get_service(_IMAGE), # noqa: SLF001
567
572
  )
568
- return await service.generate(prompt, size=size, n=n, deadline=deadline)
573
+ return await service.generate(
574
+ prompt,
575
+ size=size,
576
+ n=n,
577
+ return_base64=return_base64,
578
+ ratio=ratio,
579
+ deadline=deadline,
580
+ )
569
581
 
570
582
 
571
583
  class AI:
@@ -668,8 +680,8 @@ class AI:
668
680
  @staticmethod
669
681
  def rerank(
670
682
  query: str, documents: list[str], top_n: int | None = None
671
- ) -> list[tuple[int, float]]:
672
- """对候选文档重排序,返回 [(原索引, 相关分)] 降序列表。"""
683
+ ) -> list[RerankHit]:
684
+ """对候选文档重排序,返回 ``RerankHit`` 降序列表(原索引、分数、原文)。"""
673
685
  timeout = _facade_timeout(_RERANK)
674
686
  deadline = time.monotonic() + timeout
675
687
  return _background.run(
@@ -678,12 +690,26 @@ class AI:
678
690
  )
679
691
 
680
692
  @staticmethod
681
- def generate_image(prompt: str, *, size: str | None = None, n: int = 1) -> str:
682
- """文生图,返回生成的图片 URL。"""
693
+ def generate_image(
694
+ prompt: str,
695
+ *,
696
+ size: str | None = None,
697
+ n: int = 1,
698
+ return_base64: bool = False,
699
+ ratio: str | None = None,
700
+ ) -> ImageResult:
701
+ """文生图,返回 :class:`ImageResult`(``url`` / ``b64`` 至少一个非空)。"""
683
702
  timeout = _facade_timeout(_IMAGE)
684
703
  deadline = time.monotonic() + timeout
685
704
  return _background.run(
686
- lambda: _generate_image(prompt, size=size, n=n, deadline=deadline),
705
+ lambda: _generate_image(
706
+ prompt,
707
+ size=size,
708
+ n=n,
709
+ return_base64=return_base64,
710
+ ratio=ratio,
711
+ deadline=deadline,
712
+ ),
687
713
  timeout=timeout,
688
714
  )
689
715
 
@@ -262,21 +262,26 @@ class SdkClient:
262
262
  except openai.OpenAIError as exc:
263
263
  raise self._map_error(exc) from exc
264
264
  try:
265
- async for chunk in stream:
266
- choices = getattr(chunk, "choices", None)
267
- if not choices:
268
- continue
269
- choice = choices[0]
270
- delta = getattr(choice, "delta", None)
271
- content = getattr(delta, "content", None) if delta is not None else None
272
- reasoning = _reasoning_of(delta) if delta is not None else ""
273
- finish = getattr(choice, "finish_reason", None)
274
- if content or reasoning or finish:
275
- yield (
276
- content if isinstance(content, str) else None,
277
- reasoning or None,
278
- finish if isinstance(finish, str) and finish else None,
265
+ # 用 async with 托管流:业务侧提前 break/取消时也能关闭连接,
266
+ # 否则只能等 GC,长跑进程会堆积半开连接
267
+ async with stream:
268
+ async for chunk in stream:
269
+ choices = getattr(chunk, "choices", None)
270
+ if not choices:
271
+ continue
272
+ choice = choices[0]
273
+ delta = getattr(choice, "delta", None)
274
+ content = (
275
+ getattr(delta, "content", None) if delta is not None else None
279
276
  )
277
+ reasoning = _reasoning_of(delta) if delta is not None else ""
278
+ finish = getattr(choice, "finish_reason", None)
279
+ if content or reasoning or finish:
280
+ yield (
281
+ content if isinstance(content, str) else None,
282
+ reasoning or None,
283
+ finish if isinstance(finish, str) and finish else None,
284
+ )
280
285
  except openai.OpenAIError as exc:
281
286
  raise self._map_error(exc) from exc
282
287
 
@@ -312,8 +317,8 @@ class SdkClient:
312
317
  n: int = 1,
313
318
  extra_body: JSONObject | None = None,
314
319
  deadline: float | None = None,
315
- ) -> str:
316
- """文生图,返回 URL 或 Base64(按返回值是否以 http 开头区分)。"""
320
+ ) -> tuple[str | None, str | None]:
321
+ """文生图,返回 ``(url, b64)``:两者可能同时有值,至少一个非空。"""
317
322
  params: dict[str, Any] = {
318
323
  "model": self._profile.model,
319
324
  "prompt": prompt,
@@ -333,23 +338,24 @@ class SdkClient:
333
338
  if not items:
334
339
  raise AIResponseError("图像生成响应缺少 data")
335
340
  first = items[0]
336
- url = getattr(first, "url", None)
337
- if isinstance(url, str) and url:
338
- return url
339
- b64 = getattr(first, "b64_json", None)
340
- if isinstance(b64, str) and b64:
341
- return b64
342
- raise AIResponseError("图像生成响应缺少 url/b64_json")
341
+ raw_url = getattr(first, "url", None)
342
+ raw_b64 = getattr(first, "b64_json", None)
343
+ url = raw_url if isinstance(raw_url, str) and raw_url else None
344
+ b64 = raw_b64 if isinstance(raw_b64, str) and raw_b64 else None
345
+ if url is None and b64 is None:
346
+ raise AIResponseError("图像生成响应缺少 url/b64_json")
347
+ return url, b64
343
348
 
344
349
  async def list_models(self) -> JSONObject:
345
350
  """轻探测 ``GET {base_url}/models``:仅验证密钥有效性。
346
351
 
347
352
  独立 10 秒超时;返回值只表达成功(失败抛异常),统一返回空对象。
348
353
  """
349
- await self._request(
350
- lambda: self._client.models.list(timeout=_LIST_MODELS_TIMEOUT),
351
- deadline=None,
352
- )
354
+ # 探活不重试:失败即返回,避免「10s 超时 × (retries+1)」把配置页拖住
355
+ try:
356
+ await self._client.models.list(timeout=_LIST_MODELS_TIMEOUT)
357
+ except openai.OpenAIError as exc:
358
+ raise self._map_error(exc) from exc
353
359
  return {}
354
360
 
355
361
  async def close(self) -> None:
@@ -26,6 +26,7 @@ Author: Clark Chang
26
26
  - docx_kit: DocxKit Word 模板动态填充(docxtpl,extra: docx)
27
27
  - mineru_kit: MinerUKit MinerU 文档解析 API(requests,extra: mineru)
28
28
  - marker_kit: MarkerKit PDF + 图片本地解析(marker-pdf,extra: marker)
29
+ ⚠ 0.7.0 起**暂停发布**:不进 wheel、不可安装;需要请用 sdpy-kit==0.6.10
29
30
  - rc_docparser_kit: RcDocParserKit 离线 docx 解析(rc-docparser,extra: docx-parse)
30
31
 
31
32
  本 __init__ 不导入任何成员:五个模块均携带重型可选依赖,保持
@@ -74,6 +74,8 @@ ai = [
74
74
  md2doc = [
75
75
  "pimd[pdfa]>=2.2.5",
76
76
  "markdown-it-py>=4.2.0",
77
+ # 0.7.0 起 marker 摘除后解除 <11 封顶,显式抬到最新
78
+ "pillow>=13.0.0",
77
79
  ]
78
80
  anydoc = [
79
81
  "firecrawl-anydoc>=0.2.4",
@@ -82,7 +84,6 @@ anydoc = [
82
84
  docx = ["docxtpl>=0.20.2"]
83
85
  docx-parse = ["rc-docparser>=0.2.2"]
84
86
  mineru = ["requests>=2.34.2"]
85
- marker = ["marker-pdf>=2.0.0"]
86
87
  laya = [
87
88
  "laya>=0.4.22",
88
89
  "huggingface-hub>=1.5,<2",
@@ -95,7 +96,9 @@ decima = [
95
96
  "huggingface-hub>=1.5,<2",
96
97
  ]
97
98
  web-article = ["trafilatura>=2.3.1"]
98
- all = [
99
+ # 0.7.0 起 all 更名为 recommended:只含「轻量、互不冲突、覆盖主场景」的 extras。
100
+ # 重依赖(laya / decima)与暂停发布的 marker 单独声明。
101
+ recommended = [
99
102
  "psycopg2-binary>=2.9.13",
100
103
  "pymysql>=1.2.3",
101
104
  "numpy>=2.5.4",
@@ -122,6 +125,9 @@ path = "kit/__init__.py"
122
125
 
123
126
  [tool.hatch.build.targets.wheel]
124
127
  packages = ["kit"]
128
+ # 0.7.0 起暂停发布 MarkerKit(marker-pdf 仍锁 openai<3):源码保留在仓库,
129
+ # 但不进 wheel。恢复时删除本行并恢复 marker extra。
130
+ exclude = ["kit/docs/marker_kit.py"]
125
131
 
126
132
  [tool.hatch.build.targets.sdist]
127
133
  only-include = ["kit"]
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