coze-coding-dev-sdk 0.5.13__tar.gz → 0.5.15__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 (91) hide show
  1. {coze_coding_dev_sdk-0.5.13/coze_coding_dev_sdk.egg-info → coze_coding_dev_sdk-0.5.15}/PKG-INFO +1 -1
  2. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/__init__.py +17 -0
  3. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/image/client.py +4 -2
  4. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/image/models.py +1 -1
  5. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/__init__.py +19 -0
  6. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/buffer.py +114 -0
  7. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/client.py +96 -0
  8. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/interceptors/__init__.py +4 -0
  9. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/interceptors/boto3_hook.py +127 -0
  10. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/interceptors/httpx_transport.py +97 -0
  11. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/models.py +18 -0
  12. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/singleton.py +24 -0
  13. coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk/report/utils.py +21 -0
  14. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/s3/client.py +11 -0
  15. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15/coze_coding_dev_sdk.egg-info}/PKG-INFO +1 -1
  16. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk.egg-info/SOURCES.txt +9 -0
  17. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/pyproject.toml +2 -2
  18. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/CHANGELOG.md +0 -0
  19. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/LICENSE +0 -0
  20. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/MANIFEST.in +0 -0
  21. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/README.md +0 -0
  22. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/__init__.py +0 -0
  23. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/chat.py +0 -0
  24. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/cli.py +0 -0
  25. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/constants.py +0 -0
  26. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/db.py +0 -0
  27. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/document.py +0 -0
  28. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/embedding.py +0 -0
  29. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/image.py +0 -0
  30. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/knowledge.py +0 -0
  31. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/search.py +0 -0
  32. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/storage.py +0 -0
  33. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/supabase.py +0 -0
  34. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/utils.py +0 -0
  35. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/video.py +0 -0
  36. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/video_edit.py +0 -0
  37. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/cli/voice.py +0 -0
  38. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/core/__init__.py +0 -0
  39. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/core/client.py +0 -0
  40. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/core/config.py +0 -0
  41. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/core/exceptions.py +0 -0
  42. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/database/__init__.py +0 -0
  43. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/database/client.py +0 -0
  44. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/database/migration.py +0 -0
  45. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/__init__.py +0 -0
  46. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/client.py +0 -0
  47. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/docx_generator.py +0 -0
  48. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/models.py +0 -0
  49. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/pdf_generator.py +0 -0
  50. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/pptx_generator.py +0 -0
  51. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/document/xlsx_generator.py +0 -0
  52. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/embedding/__init__.py +0 -0
  53. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/embedding/client.py +0 -0
  54. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/embedding/models.py +0 -0
  55. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/fetch/__init__.py +0 -0
  56. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/fetch/client.py +0 -0
  57. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/fetch/models.py +0 -0
  58. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/image/__init__.py +0 -0
  59. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/knowledge/__init__.py +0 -0
  60. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/knowledge/client.py +0 -0
  61. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/knowledge/models.py +0 -0
  62. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/llm/__init__.py +0 -0
  63. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/llm/client.py +0 -0
  64. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/llm/models.py +0 -0
  65. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/memory/__init__.py +0 -0
  66. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/memory/client.py +0 -0
  67. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/s3/__init__.py +0 -0
  68. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/s3/models.py +0 -0
  69. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/search/__init__.py +0 -0
  70. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/search/client.py +0 -0
  71. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/search/models.py +0 -0
  72. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/supabase/__init__.py +0 -0
  73. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/supabase/client.py +0 -0
  74. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/supabase/models.py +0 -0
  75. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video/__init__.py +0 -0
  76. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video/client.py +0 -0
  77. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video/models.py +0 -0
  78. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video_edit/__init__.py +0 -0
  79. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video_edit/examples.py +0 -0
  80. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video_edit/frame_extractor.py +0 -0
  81. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video_edit/models.py +0 -0
  82. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/video_edit/video_edit.py +0 -0
  83. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/voice/__init__.py +0 -0
  84. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/voice/asr.py +0 -0
  85. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/voice/models.py +0 -0
  86. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk/voice/tts.py +0 -0
  87. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk.egg-info/dependency_links.txt +0 -0
  88. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk.egg-info/entry_points.txt +0 -0
  89. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk.egg-info/requires.txt +0 -0
  90. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/coze_coding_dev_sdk.egg-info/top_level.txt +0 -0
  91. {coze_coding_dev_sdk-0.5.13 → coze_coding_dev_sdk-0.5.15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coze-coding-dev-sdk
3
- Version: 0.5.13
3
+ Version: 0.5.15
4
4
  Summary: Coze Coding Dev SDK - 优雅的多功能 AI SDK,支持图片生成、视频生成、语音合成、语音识别、大语言模型、联网搜索和文本/多模态 Embedding。包含命令行工具 coze-coding-ai,支持 Context 上下文追踪
5
5
  Author-email: Coze Coding Integration Team <support@coze.com>
6
6
  Maintainer-email: Coze Coding Integration Team <support@coze.com>
@@ -76,6 +76,16 @@ from .fetch import (
76
76
  FetchResponse,
77
77
  )
78
78
 
79
+ from .report import (
80
+ LogEntry,
81
+ ReportClient,
82
+ ReportBuffer,
83
+ get_report_buffer,
84
+ InstrumentedTransport,
85
+ InstrumentedAsyncTransport,
86
+ create_boto3_report_hooks,
87
+ )
88
+
79
89
  __version__ = "0.5.0"
80
90
 
81
91
  __all__ = [
@@ -150,4 +160,11 @@ __all__ = [
150
160
  "FetchImage",
151
161
  "FetchRequest",
152
162
  "FetchResponse",
163
+ "LogEntry",
164
+ "ReportClient",
165
+ "ReportBuffer",
166
+ "get_report_buffer",
167
+ "InstrumentedTransport",
168
+ "InstrumentedAsyncTransport",
169
+ "create_boto3_report_hooks",
153
170
  ]
@@ -20,7 +20,6 @@ class ImageGenerationClient(BaseClient):
20
20
  ):
21
21
  super().__init__(config, ctx, custom_headers, verbose)
22
22
  self.base_url = self.config.base_url
23
- self.model = ImageConfig.DEFAULT_MODEL
24
23
 
25
24
  def extract_urls(self, response: ImageGenerationResponse) -> List[str]:
26
25
  urls = []
@@ -47,6 +46,7 @@ class ImageGenerationClient(BaseClient):
47
46
  optimize_prompt_mode: Optional[str] = None,
48
47
  sequential_image_generation: Optional[str] = None,
49
48
  sequential_image_generation_max_images: Optional[int] = None,
49
+ model: str = ImageConfig.DEFAULT_MODEL,
50
50
  ) -> ImageGenerationResponse:
51
51
  request_params = {"prompt": prompt}
52
52
  if size is not None:
@@ -71,7 +71,7 @@ class ImageGenerationClient(BaseClient):
71
71
  data = self._request(
72
72
  method="POST",
73
73
  url=f"{self.base_url}/api/v3/images/generations",
74
- json=request.to_api_request(self.model),
74
+ json=request.to_api_request(model),
75
75
  )
76
76
 
77
77
  if "error" in data and data["error"]:
@@ -94,6 +94,7 @@ class ImageGenerationClient(BaseClient):
94
94
  optimize_prompt_mode: Optional[str] = None,
95
95
  sequential_image_generation: Optional[str] = None,
96
96
  sequential_image_generation_max_images: Optional[int] = None,
97
+ model: str = ImageConfig.DEFAULT_MODEL,
97
98
  ) -> ImageGenerationResponse:
98
99
  loop = asyncio.get_event_loop()
99
100
  return await loop.run_in_executor(
@@ -107,4 +108,5 @@ class ImageGenerationClient(BaseClient):
107
108
  optimize_prompt_mode,
108
109
  sequential_image_generation,
109
110
  sequential_image_generation_max_images,
111
+ model,
110
112
  )
@@ -6,7 +6,7 @@ from ..core.exceptions import ValidationError
6
6
 
7
7
 
8
8
  class ImageConfig:
9
- DEFAULT_MODEL = "doubao-seedream-4-5-251128"
9
+ DEFAULT_MODEL = "doubao-seedream-5-0-260128"
10
10
  DEFAULT_SIZE = "2K"
11
11
  DEFAULT_CUSTOM_SIZE = "2048x2048"
12
12
  DEFAULT_WATERMARK = True
@@ -0,0 +1,19 @@
1
+ from .models import LogEntry
2
+ from .utils import truncate, redact_url
3
+ from .client import ReportClient
4
+ from .buffer import ReportBuffer
5
+ from .singleton import get_report_buffer
6
+ from .interceptors.httpx_transport import InstrumentedTransport, InstrumentedAsyncTransport
7
+ from .interceptors.boto3_hook import create_boto3_report_hooks
8
+
9
+ __all__ = [
10
+ "LogEntry",
11
+ "truncate",
12
+ "redact_url",
13
+ "ReportClient",
14
+ "ReportBuffer",
15
+ "get_report_buffer",
16
+ "InstrumentedTransport",
17
+ "InstrumentedAsyncTransport",
18
+ "create_boto3_report_hooks",
19
+ ]
@@ -0,0 +1,114 @@
1
+ import atexit
2
+ import os
3
+ import threading
4
+ import time
5
+ from collections import deque
6
+ from typing import List, Optional
7
+
8
+ from .models import LogEntry
9
+
10
+ BUFFER_SIZE_THRESHOLD = 100
11
+ FLUSH_INTERVAL_SECONDS = 60.0
12
+ MAX_BUFFER_SIZE = 60_000
13
+ MAX_FLUSH_RETRIES = 3
14
+ FLUSH_RETRY_BASE_SECONDS = 1.0
15
+
16
+
17
+ class ReportBuffer:
18
+ def __init__(self, client):
19
+ self._client = client
20
+ self._buffer: deque[LogEntry] = deque(maxlen=MAX_BUFFER_SIZE)
21
+ self._lock = threading.Lock()
22
+ self._project_id: Optional[str] = os.environ.get("COZELOOP_WORKSPACE_ID")
23
+ self._env: Optional[str] = os.environ.get("COZE_PROJECT_ENV")
24
+ self._timer: Optional[threading.Timer] = None
25
+ self._destroyed = False
26
+ self._flushing = False
27
+ self._pending_flush = False
28
+ self._start_timer()
29
+ atexit.register(self.destroy)
30
+
31
+ def _start_timer(self) -> None:
32
+ with self._lock:
33
+ if self._destroyed:
34
+ return
35
+ self._timer = threading.Timer(FLUSH_INTERVAL_SECONDS, self._on_timer)
36
+ self._timer.daemon = True
37
+ self._timer.start()
38
+
39
+ def _on_timer(self) -> None:
40
+ try:
41
+ self.flush()
42
+ except Exception:
43
+ pass
44
+ self._start_timer()
45
+
46
+ def add(self, entry: LogEntry) -> None:
47
+ if self._destroyed:
48
+ return
49
+ try:
50
+ with self._lock:
51
+ entry.project_id = self._project_id
52
+ entry.env = self._env
53
+ self._buffer.append(entry)
54
+ should_flush = len(self._buffer) >= BUFFER_SIZE_THRESHOLD
55
+ if should_flush:
56
+ t = threading.Thread(target=self.flush, daemon=True)
57
+ t.start()
58
+ except Exception:
59
+ pass
60
+
61
+ def flush(self) -> None:
62
+ with self._lock:
63
+ if self._flushing:
64
+ self._pending_flush = True
65
+ return
66
+ if not self._buffer:
67
+ return
68
+ self._flushing = True
69
+
70
+ consecutive_failures = 0
71
+ try:
72
+ while True:
73
+ with self._lock:
74
+ count = min(BUFFER_SIZE_THRESHOLD, len(self._buffer))
75
+ if count == 0:
76
+ break
77
+ entries: List[LogEntry] = [self._buffer.popleft() for _ in range(count)]
78
+
79
+ try:
80
+ self._client.batch_report(entries)
81
+ consecutive_failures = 0
82
+ except Exception as e:
83
+ consecutive_failures += 1
84
+ with self._lock:
85
+ for entry in reversed(entries):
86
+ if len(self._buffer) < MAX_BUFFER_SIZE:
87
+ self._buffer.appendleft(entry)
88
+ if consecutive_failures >= MAX_FLUSH_RETRIES:
89
+ print(f"[report] flush failed {consecutive_failures} times, giving up until next cycle: {e}")
90
+ break
91
+ delay = FLUSH_RETRY_BASE_SECONDS * (2 ** (consecutive_failures - 1))
92
+ print(f"[report] flush failed (attempt {consecutive_failures}/{MAX_FLUSH_RETRIES}), retrying in {delay}s: {e}")
93
+ time.sleep(delay)
94
+ continue
95
+
96
+ with self._lock:
97
+ if not self._pending_flush and len(self._buffer) < BUFFER_SIZE_THRESHOLD:
98
+ break
99
+ self._pending_flush = False
100
+ finally:
101
+ with self._lock:
102
+ self._flushing = False
103
+ self._pending_flush = False
104
+
105
+ def destroy(self) -> None:
106
+ with self._lock:
107
+ self._destroyed = True
108
+ if self._timer:
109
+ self._timer.cancel()
110
+ self._timer = None
111
+ try:
112
+ self.flush()
113
+ except Exception:
114
+ pass
@@ -0,0 +1,96 @@
1
+ import os
2
+ from typing import List, Optional, Dict, Tuple
3
+
4
+ from .models import LogEntry
5
+
6
+ BATCH_REPORT_TIMEOUT_SECONDS = 10
7
+
8
+ _env_loaded = False
9
+ _cached_env: Tuple[str, str] = ("", "")
10
+
11
+
12
+ def _load_report_env() -> Tuple[str, str]:
13
+ """Load base_url and api_key from env, dotenv, or coze_workload_identity."""
14
+ global _env_loaded, _cached_env
15
+ if _env_loaded:
16
+ return _cached_env
17
+
18
+ base_url = os.environ.get("COZE_INTEGRATION_BASE_URL", "")
19
+ api_key = os.environ.get("COZE_WORKLOAD_IDENTITY_API_KEY", "")
20
+
21
+ if base_url and api_key:
22
+ _env_loaded = True
23
+ _cached_env = (base_url, api_key)
24
+ return _cached_env
25
+
26
+ try:
27
+ from dotenv import load_dotenv
28
+ load_dotenv()
29
+ base_url = os.environ.get("COZE_INTEGRATION_BASE_URL", "")
30
+ api_key = os.environ.get("COZE_WORKLOAD_IDENTITY_API_KEY", "")
31
+ if base_url and api_key:
32
+ _env_loaded = True
33
+ _cached_env = (base_url, api_key)
34
+ return _cached_env
35
+ except ImportError:
36
+ pass
37
+
38
+ try:
39
+ from coze_workload_identity import Client
40
+ client = Client()
41
+ env_vars = client.get_project_env_vars()
42
+ client.close()
43
+ for env_var in env_vars:
44
+ if env_var.key == "COZE_INTEGRATION_BASE_URL":
45
+ base_url = env_var.value
46
+ elif env_var.key == "COZE_WORKLOAD_IDENTITY_API_KEY":
47
+ api_key = env_var.value
48
+ except Exception:
49
+ pass
50
+
51
+ _env_loaded = True
52
+ _cached_env = (base_url, api_key)
53
+ return _cached_env
54
+
55
+
56
+ class ReportClient:
57
+ def __init__(self):
58
+ try:
59
+ base_url, api_key = _load_report_env()
60
+ self._base_url = base_url
61
+ self._api_key = api_key
62
+ self._configured = bool(self._base_url and self._api_key)
63
+ if not self._configured:
64
+ missing = []
65
+ if not self._base_url:
66
+ missing.append("COZE_INTEGRATION_BASE_URL")
67
+ if not self._api_key:
68
+ missing.append("COZE_WORKLOAD_IDENTITY_API_KEY")
69
+ print(f"[report] ReportClient not configured: {', '.join(missing)} is missing")
70
+ except Exception as e:
71
+ self._base_url = None
72
+ self._api_key = None
73
+ self._configured = False
74
+ print(f"[report] ReportClient config failed: {e}")
75
+
76
+ def batch_report(self, entries: List[LogEntry]) -> None:
77
+ if not entries:
78
+ return
79
+ if not self._configured:
80
+ print(f"[report] batchReport skipped: ReportClient is not configured. "
81
+ f"Set COZE_INTEGRATION_BASE_URL and COZE_WORKLOAD_IDENTITY_API_KEY "
82
+ f"environment variables. Dropping {len(entries)} entries.")
83
+ return
84
+ # Use `requests` instead of `httpx` intentionally to avoid self-instrumentation:
85
+ # the SDK wraps httpx transports for report interception, so using httpx here
86
+ # would cause the reporting calls themselves to be intercepted recursively.
87
+ import requests
88
+
89
+ url = f"{self._base_url}/v1/memory_report/batch_report"
90
+ headers: Dict[str, str] = {
91
+ "Content-Type": "application/json",
92
+ "x-coze-token": self._api_key,
93
+ }
94
+ payload = {"entries": [e.model_dump() for e in entries]}
95
+ resp = requests.post(url, json=payload, headers=headers, timeout=BATCH_REPORT_TIMEOUT_SECONDS)
96
+ resp.raise_for_status()
@@ -0,0 +1,4 @@
1
+ from .httpx_transport import InstrumentedTransport, InstrumentedAsyncTransport
2
+ from .boto3_hook import create_boto3_report_hooks
3
+
4
+ __all__ = ["InstrumentedTransport", "InstrumentedAsyncTransport", "create_boto3_report_hooks"]
@@ -0,0 +1,127 @@
1
+ import threading
2
+ import time
3
+ from typing import Optional
4
+
5
+ from ..buffer import ReportBuffer
6
+ from ..models import LogEntry
7
+ from ..utils import redact_url, truncate
8
+
9
+
10
+ def create_boto3_report_hooks(buffer: ReportBuffer, source: str = "s3"):
11
+ # We use thread_id to correlate before/after hooks because boto3's event
12
+ # system does not pass a request-level correlation ID between hooks.
13
+ # Limitation: thread ident can be reused in thread pools after a thread dies,
14
+ # and nested boto3 calls on the same thread will overwrite the pending entry.
15
+ # A periodic cleanup discards stale entries older than 5 minutes.
16
+ _pending = {}
17
+ _pending_lock = threading.Lock()
18
+ _STALE_THRESHOLD = 300 # 5 minutes
19
+
20
+ def _cleanup_stale():
21
+ """Remove pending entries older than _STALE_THRESHOLD seconds."""
22
+ now = time.time()
23
+ stale_keys = [k for k, v in _pending.items() if now - v["start_time"] > _STALE_THRESHOLD]
24
+ for k in stale_keys:
25
+ _pending.pop(k, None)
26
+
27
+ def before_hook(event_name, params, **kwargs):
28
+ try:
29
+ thread_id = threading.current_thread().ident
30
+ url_parts = []
31
+ if "url" in params:
32
+ url_parts.append(params["url"])
33
+ elif "endpoint_url" in kwargs:
34
+ url_parts.append(kwargs["endpoint_url"])
35
+
36
+ method = (params.get("method") or params.get("http_method") or "UNKNOWN") if isinstance(params, dict) else "UNKNOWN"
37
+
38
+ request_body: Optional[str] = None
39
+ body = params.get("body") if isinstance(params, dict) else None
40
+ if body:
41
+ try:
42
+ if isinstance(body, bytes):
43
+ request_body = truncate(body.decode("utf-8", errors="replace"))
44
+ elif isinstance(body, str):
45
+ request_body = truncate(body)
46
+ except Exception:
47
+ pass
48
+
49
+ with _pending_lock:
50
+ _cleanup_stale()
51
+ _pending[thread_id] = {
52
+ "start_time": time.time(),
53
+ "method": method,
54
+ "url": url_parts[0] if url_parts else "",
55
+ "request_body": request_body,
56
+ }
57
+ except Exception:
58
+ pass
59
+
60
+ def after_hook(event_name, parsed, **kwargs):
61
+ try:
62
+ thread_id = threading.current_thread().ident
63
+ with _pending_lock:
64
+ pending = _pending.pop(thread_id, None)
65
+ if pending is None:
66
+ return
67
+
68
+ duration_ms = (time.time() - pending["start_time"]) * 1000
69
+ http_response = kwargs.get("http_response")
70
+ status_code: Optional[int] = None
71
+ if http_response is not None:
72
+ status_code = getattr(http_response, "status_code", None)
73
+
74
+ success = 200 <= status_code < 300 if status_code else False
75
+
76
+ response_body: Optional[str] = None
77
+ if parsed:
78
+ try:
79
+ import json
80
+ response_body = truncate(json.dumps(parsed, default=str))
81
+ except Exception:
82
+ pass
83
+
84
+ entry = LogEntry(
85
+ source=source,
86
+ method=pending["method"],
87
+ url=redact_url(pending["url"]),
88
+ request_body=pending.get("request_body"),
89
+ response_body=response_body,
90
+ status_code=status_code,
91
+ error=None,
92
+ duration_ms=duration_ms,
93
+ timestamp=int(pending["start_time"] * 1000),
94
+ success=success,
95
+ )
96
+ buffer.add(entry)
97
+ except Exception:
98
+ pass
99
+
100
+ def error_hook(event_name, exception, **kwargs):
101
+ """Handle failed S3 requests: clean up _pending and log the error."""
102
+ try:
103
+ thread_id = threading.current_thread().ident
104
+ with _pending_lock:
105
+ pending = _pending.pop(thread_id, None)
106
+ if pending is None:
107
+ return
108
+
109
+ duration_ms = (time.time() - pending["start_time"]) * 1000
110
+
111
+ entry = LogEntry(
112
+ source=source,
113
+ method=pending["method"],
114
+ url=redact_url(pending["url"]),
115
+ request_body=pending.get("request_body"),
116
+ response_body=None,
117
+ status_code=None,
118
+ error=str(exception) if exception else "Unknown error",
119
+ duration_ms=duration_ms,
120
+ timestamp=int(pending["start_time"] * 1000),
121
+ success=False,
122
+ )
123
+ buffer.add(entry)
124
+ except Exception:
125
+ pass
126
+
127
+ return before_hook, after_hook, error_hook
@@ -0,0 +1,97 @@
1
+ import time
2
+ from typing import Optional
3
+
4
+ import httpx
5
+
6
+ from ..buffer import ReportBuffer
7
+ from ..models import LogEntry
8
+ from ..utils import redact_url, truncate
9
+
10
+
11
+ def _safe_record(
12
+ buffer: ReportBuffer,
13
+ source: str,
14
+ request: httpx.Request,
15
+ response: Optional[httpx.Response],
16
+ error: Optional[Exception],
17
+ start_time: float,
18
+ ) -> None:
19
+ try:
20
+ duration_ms = (time.time() - start_time) * 1000
21
+ status_code = response.status_code if response else None
22
+ success = 200 <= status_code < 300 if status_code else False
23
+
24
+ request_body: Optional[str] = None
25
+ try:
26
+ raw = request.content
27
+ if raw:
28
+ request_body = truncate(raw.decode("utf-8", errors="replace"))
29
+ except Exception:
30
+ pass
31
+
32
+ response_body: Optional[str] = None
33
+ if response is not None:
34
+ try:
35
+ response_body = truncate(response.text)
36
+ except Exception:
37
+ pass
38
+
39
+ entry = LogEntry(
40
+ source=source,
41
+ method=request.method,
42
+ url=redact_url(str(request.url)),
43
+ request_body=request_body,
44
+ response_body=response_body,
45
+ status_code=status_code,
46
+ error=str(error) if error else None,
47
+ duration_ms=duration_ms,
48
+ timestamp=int(start_time * 1000),
49
+ success=success,
50
+ )
51
+ buffer.add(entry)
52
+ except Exception as e:
53
+ print(f"[report] _safe_record error: {e}")
54
+
55
+
56
+ class InstrumentedTransport(httpx.BaseTransport):
57
+ def __init__(
58
+ self,
59
+ transport: httpx.BaseTransport,
60
+ buffer: ReportBuffer,
61
+ source: str = "supabase",
62
+ ):
63
+ self._transport = transport
64
+ self._buffer = buffer
65
+ self._source = source
66
+
67
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
68
+ start_time = time.time()
69
+ try:
70
+ response = self._transport.handle_request(request)
71
+ _safe_record(self._buffer, self._source, request, response, None, start_time)
72
+ return response
73
+ except Exception as exc:
74
+ _safe_record(self._buffer, self._source, request, None, exc, start_time)
75
+ raise
76
+
77
+
78
+ class InstrumentedAsyncTransport(httpx.AsyncBaseTransport):
79
+ def __init__(
80
+ self,
81
+ transport: httpx.AsyncBaseTransport,
82
+ buffer: ReportBuffer,
83
+ source: str = "supabase",
84
+ ):
85
+ self._transport = transport
86
+ self._buffer = buffer
87
+ self._source = source
88
+
89
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
90
+ start_time = time.time()
91
+ try:
92
+ response = await self._transport.handle_async_request(request)
93
+ _safe_record(self._buffer, self._source, request, response, None, start_time)
94
+ return response
95
+ except Exception as exc:
96
+ _safe_record(self._buffer, self._source, request, None, exc, start_time)
97
+ raise
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class LogEntry(BaseModel):
7
+ source: str
8
+ project_id: Optional[str] = None
9
+ env: Optional[str] = None
10
+ method: str
11
+ url: str
12
+ request_body: Optional[str] = None
13
+ response_body: Optional[str] = None
14
+ status_code: Optional[int] = None
15
+ error: Optional[str] = None
16
+ duration_ms: float
17
+ timestamp: int
18
+ success: bool
@@ -0,0 +1,24 @@
1
+ import threading
2
+ from typing import Optional
3
+
4
+ from .buffer import ReportBuffer
5
+ from .client import ReportClient
6
+
7
+ _buffer: Optional[ReportBuffer] = None
8
+ _lock = threading.Lock()
9
+
10
+
11
+ def get_report_buffer() -> Optional[ReportBuffer]:
12
+ global _buffer
13
+ if _buffer is not None:
14
+ return _buffer
15
+ with _lock:
16
+ if _buffer is not None:
17
+ return _buffer
18
+ try:
19
+ client = ReportClient()
20
+ _buffer = ReportBuffer(client)
21
+ return _buffer
22
+ except Exception as e:
23
+ print(f"[report] failed to create singleton buffer: {e}")
24
+ return None
@@ -0,0 +1,21 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ _SENSITIVE_PARAM_RE = re.compile(
5
+ r'((?:apikey|api_key|access_key|secret_key|access_token|token|secret|password|authorization|credential|X-Amz-Signature|X-Amz-Credential|X-Amz-Security-Token)=)[^&]*',
6
+ re.IGNORECASE,
7
+ )
8
+
9
+ DEFAULT_TRUNCATE_LENGTH = 1024
10
+
11
+
12
+ def truncate(s: Optional[str], max_len: int = DEFAULT_TRUNCATE_LENGTH) -> Optional[str]:
13
+ if s is None:
14
+ return None
15
+ if len(s) <= max_len:
16
+ return s
17
+ return s[:max_len] + "...[truncated]"
18
+
19
+
20
+ def redact_url(url: str) -> str:
21
+ return _SENSITIVE_PARAM_RE.sub(r'\1***', url)
@@ -119,6 +119,17 @@ class S3SyncStorage:
119
119
  logger.error("Error loading COZE_WORKLOAD_IDENTITY_TOKEN: %s", e)
120
120
 
121
121
  client.meta.events.register("before-call.s3", _inject_header)
122
+ try:
123
+ from ..report import get_report_buffer, create_boto3_report_hooks
124
+
125
+ report_buffer = get_report_buffer()
126
+ if report_buffer:
127
+ before_hook, after_hook, error_hook = create_boto3_report_hooks(report_buffer, source="s3")
128
+ client.meta.events.register("before-call.s3", before_hook)
129
+ client.meta.events.register("after-call.s3", after_hook)
130
+ client.meta.events.register("after-call-error.s3", error_hook)
131
+ except Exception:
132
+ pass
122
133
  self._client = client
123
134
  return self._client
124
135
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coze-coding-dev-sdk
3
- Version: 0.5.13
3
+ Version: 0.5.15
4
4
  Summary: Coze Coding Dev SDK - 优雅的多功能 AI SDK,支持图片生成、视频生成、语音合成、语音识别、大语言模型、联网搜索和文本/多模态 Embedding。包含命令行工具 coze-coding-ai,支持 Context 上下文追踪
5
5
  Author-email: Coze Coding Integration Team <support@coze.com>
6
6
  Maintainer-email: Coze Coding Integration Team <support@coze.com>
@@ -57,6 +57,15 @@ coze_coding_dev_sdk/llm/client.py
57
57
  coze_coding_dev_sdk/llm/models.py
58
58
  coze_coding_dev_sdk/memory/__init__.py
59
59
  coze_coding_dev_sdk/memory/client.py
60
+ coze_coding_dev_sdk/report/__init__.py
61
+ coze_coding_dev_sdk/report/buffer.py
62
+ coze_coding_dev_sdk/report/client.py
63
+ coze_coding_dev_sdk/report/models.py
64
+ coze_coding_dev_sdk/report/singleton.py
65
+ coze_coding_dev_sdk/report/utils.py
66
+ coze_coding_dev_sdk/report/interceptors/__init__.py
67
+ coze_coding_dev_sdk/report/interceptors/boto3_hook.py
68
+ coze_coding_dev_sdk/report/interceptors/httpx_transport.py
60
69
  coze_coding_dev_sdk/s3/__init__.py
61
70
  coze_coding_dev_sdk/s3/client.py
62
71
  coze_coding_dev_sdk/s3/models.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coze-coding-dev-sdk"
7
- version = "0.5.13"
7
+ version = "0.5.15"
8
8
  description = "Coze Coding Dev SDK - 优雅的多功能 AI SDK,支持图片生成、视频生成、语音合成、语音识别、大语言模型、联网搜索和文本/多模态 Embedding。包含命令行工具 coze-coding-ai,支持 Context 上下文追踪"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -98,7 +98,7 @@ dev = [
98
98
  ]
99
99
 
100
100
  [tool.setuptools]
101
- packages = ["coze_coding_dev_sdk", "coze_coding_dev_sdk.core", "coze_coding_dev_sdk.image", "coze_coding_dev_sdk.video", "coze_coding_dev_sdk.voice", "coze_coding_dev_sdk.llm", "coze_coding_dev_sdk.search", "coze_coding_dev_sdk.cli", "coze_coding_dev_sdk.database", "coze_coding_dev_sdk.memory", "coze_coding_dev_sdk.s3", "coze_coding_dev_sdk.embedding", "coze_coding_dev_sdk.supabase"]
101
+ packages = ["coze_coding_dev_sdk", "coze_coding_dev_sdk.core", "coze_coding_dev_sdk.image", "coze_coding_dev_sdk.video", "coze_coding_dev_sdk.voice", "coze_coding_dev_sdk.llm", "coze_coding_dev_sdk.search", "coze_coding_dev_sdk.cli", "coze_coding_dev_sdk.database", "coze_coding_dev_sdk.memory", "coze_coding_dev_sdk.s3", "coze_coding_dev_sdk.embedding", "coze_coding_dev_sdk.supabase", "coze_coding_dev_sdk.report", "coze_coding_dev_sdk.report.interceptors"]
102
102
 
103
103
  [tool.setuptools.package-data]
104
104
  coze_coding_dev_sdk = ["py.typed"]