tamar-model-client 0.1.27__py3-none-any.whl → 0.1.30__py3-none-any.whl
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.
- tamar_model_client/async_client.py +83 -40
- tamar_model_client/circuit_breaker.py +6 -3
- tamar_model_client/core/__init__.py +5 -1
- tamar_model_client/core/base_client.py +136 -40
- tamar_model_client/core/http_fallback.py +313 -31
- tamar_model_client/core/logging_setup.py +15 -1
- tamar_model_client/core/utils.py +27 -1
- tamar_model_client/error_handler.py +112 -17
- tamar_model_client/json_formatter.py +9 -0
- tamar_model_client/sync_client.py +177 -38
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/METADATA +588 -6
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/RECORD +16 -15
- tests/test_circuit_breaker.py +269 -0
- tests/test_google_azure_final.py +605 -21
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/WHEEL +0 -0
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.30.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ connections fail, supporting both synchronous and asynchronous clients.
|
|
7
7
|
|
8
8
|
import json
|
9
9
|
import logging
|
10
|
-
from typing import Optional, Iterator, AsyncIterator, Dict, Any
|
10
|
+
from typing import Optional, Iterator, AsyncIterator, Dict, Any, List
|
11
11
|
|
12
12
|
from . import generate_request_id, get_protected_logger
|
13
13
|
from ..schemas import ModelRequest, ModelResponse
|
@@ -15,8 +15,70 @@ from ..schemas import ModelRequest, ModelResponse
|
|
15
15
|
logger = get_protected_logger(__name__)
|
16
16
|
|
17
17
|
|
18
|
+
def safe_serialize(obj: Any) -> Any:
|
19
|
+
"""
|
20
|
+
安全地序列化对象,避免 Pydantic ValidatorIterator 序列化问题
|
21
|
+
"""
|
22
|
+
if obj is None:
|
23
|
+
return None
|
24
|
+
|
25
|
+
# 处理基本类型
|
26
|
+
if isinstance(obj, (str, int, float, bool)):
|
27
|
+
return obj
|
28
|
+
|
29
|
+
# 处理列表
|
30
|
+
if isinstance(obj, (list, tuple)):
|
31
|
+
return [safe_serialize(item) for item in obj]
|
32
|
+
|
33
|
+
# 处理字典
|
34
|
+
if isinstance(obj, dict):
|
35
|
+
return {key: safe_serialize(value) for key, value in obj.items()}
|
36
|
+
|
37
|
+
# 处理 Pydantic 模型
|
38
|
+
if hasattr(obj, 'model_dump'):
|
39
|
+
try:
|
40
|
+
return obj.model_dump(exclude_unset=True)
|
41
|
+
except Exception:
|
42
|
+
# 如果 model_dump 失败,尝试手动提取字段
|
43
|
+
try:
|
44
|
+
if hasattr(obj, '__dict__'):
|
45
|
+
return {k: safe_serialize(v) for k, v in obj.__dict__.items()
|
46
|
+
if not k.startswith('_') and not callable(v)}
|
47
|
+
elif hasattr(obj, '__slots__'):
|
48
|
+
return {slot: safe_serialize(getattr(obj, slot, None))
|
49
|
+
for slot in obj.__slots__ if hasattr(obj, slot)}
|
50
|
+
except Exception:
|
51
|
+
pass
|
52
|
+
|
53
|
+
# 处理 Pydantic v1 模型
|
54
|
+
if hasattr(obj, 'dict'):
|
55
|
+
try:
|
56
|
+
return obj.dict(exclude_unset=True)
|
57
|
+
except Exception:
|
58
|
+
pass
|
59
|
+
|
60
|
+
# 处理枚举
|
61
|
+
if hasattr(obj, 'value'):
|
62
|
+
return obj.value
|
63
|
+
|
64
|
+
# 最后的尝试:转换为字符串
|
65
|
+
try:
|
66
|
+
return str(obj)
|
67
|
+
except Exception:
|
68
|
+
return None
|
69
|
+
|
70
|
+
|
18
71
|
class HttpFallbackMixin:
|
19
|
-
"""HTTP fallback functionality for synchronous clients
|
72
|
+
"""HTTP fallback functionality for synchronous clients
|
73
|
+
|
74
|
+
This mixin requires the following attributes from the host class:
|
75
|
+
- http_fallback_url: str - The HTTP fallback URL (from BaseClient._init_resilient_features)
|
76
|
+
- jwt_token: Optional[str] - JWT token for authentication (from BaseClient)
|
77
|
+
|
78
|
+
Usage:
|
79
|
+
This mixin should be used with BaseClient or its subclasses that have
|
80
|
+
initialized the resilient features with _init_resilient_features().
|
81
|
+
"""
|
20
82
|
|
21
83
|
def _ensure_http_client(self) -> None:
|
22
84
|
"""Ensure HTTP client is initialized"""
|
@@ -29,36 +91,42 @@ class HttpFallbackMixin:
|
|
29
91
|
|
30
92
|
# Set default headers
|
31
93
|
self._http_client.headers.update({
|
32
|
-
'Content-Type': 'application/json',
|
33
94
|
'User-Agent': 'TamarModelClient/1.0'
|
34
95
|
})
|
35
96
|
|
36
97
|
def _convert_to_http_format(self, model_request: ModelRequest) -> Dict[str, Any]:
|
37
98
|
"""Convert ModelRequest to HTTP payload format"""
|
99
|
+
# Use safe serialization to avoid Pydantic ValidatorIterator issues
|
38
100
|
payload = {
|
39
|
-
"provider": model_request.provider
|
40
|
-
"model": model_request.model,
|
41
|
-
"user_context": model_request.user_context
|
42
|
-
"stream": model_request.stream
|
101
|
+
"provider": safe_serialize(model_request.provider),
|
102
|
+
"model": safe_serialize(model_request.model),
|
103
|
+
"user_context": safe_serialize(model_request.user_context),
|
104
|
+
"stream": safe_serialize(model_request.stream)
|
43
105
|
}
|
44
106
|
|
45
107
|
# Add provider-specific fields
|
46
108
|
if hasattr(model_request, 'messages') and model_request.messages:
|
47
|
-
payload['messages'] = model_request.messages
|
109
|
+
payload['messages'] = safe_serialize(model_request.messages)
|
48
110
|
if hasattr(model_request, 'contents') and model_request.contents:
|
49
|
-
payload['contents'] = model_request.contents
|
111
|
+
payload['contents'] = safe_serialize(model_request.contents)
|
50
112
|
|
51
113
|
# Add optional fields
|
52
114
|
if model_request.channel:
|
53
|
-
payload['channel'] = model_request.channel
|
115
|
+
payload['channel'] = safe_serialize(model_request.channel)
|
54
116
|
if model_request.invoke_type:
|
55
|
-
payload['invoke_type'] = model_request.invoke_type
|
117
|
+
payload['invoke_type'] = safe_serialize(model_request.invoke_type)
|
56
118
|
|
57
|
-
# Add
|
119
|
+
# Add config parameters safely
|
120
|
+
if hasattr(model_request, 'config') and model_request.config:
|
121
|
+
payload['config'] = safe_serialize(model_request.config)
|
122
|
+
|
123
|
+
# Add extra parameters safely
|
58
124
|
if hasattr(model_request, 'model_extra') and model_request.model_extra:
|
59
|
-
|
60
|
-
|
61
|
-
|
125
|
+
serialized_extra = safe_serialize(model_request.model_extra)
|
126
|
+
if isinstance(serialized_extra, dict):
|
127
|
+
for key, value in serialized_extra.items():
|
128
|
+
if key not in payload:
|
129
|
+
payload[key] = value
|
62
130
|
|
63
131
|
return payload
|
64
132
|
|
@@ -88,12 +156,17 @@ class HttpFallbackMixin:
|
|
88
156
|
data = json.loads(data_str)
|
89
157
|
yield ModelResponse(**data)
|
90
158
|
except json.JSONDecodeError:
|
91
|
-
logger.warning(f"Failed to parse streaming response: {data_str}")
|
159
|
+
logger.warning(f"⚠️ Failed to parse streaming response: {data_str}")
|
92
160
|
|
93
161
|
def _invoke_http_fallback(self, model_request: ModelRequest,
|
94
162
|
timeout: Optional[float] = None,
|
95
|
-
request_id: Optional[str] = None
|
163
|
+
request_id: Optional[str] = None,
|
164
|
+
origin_request_id: Optional[str] = None) -> Any:
|
96
165
|
"""HTTP fallback implementation"""
|
166
|
+
# Check if http_fallback_url is available
|
167
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
168
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
169
|
+
|
97
170
|
self._ensure_http_client()
|
98
171
|
|
99
172
|
# Generate request ID if not provided
|
@@ -104,10 +177,14 @@ class HttpFallbackMixin:
|
|
104
177
|
logger.warning(
|
105
178
|
f"🔻 Using HTTP fallback for request",
|
106
179
|
extra={
|
180
|
+
"log_type": "info",
|
107
181
|
"request_id": request_id,
|
108
|
-
"
|
109
|
-
|
110
|
-
|
182
|
+
"data": {
|
183
|
+
"origin_request_id": origin_request_id,
|
184
|
+
"provider": model_request.provider.value,
|
185
|
+
"model": model_request.model,
|
186
|
+
"fallback_url": self.http_fallback_url
|
187
|
+
}
|
111
188
|
}
|
112
189
|
)
|
113
190
|
|
@@ -117,8 +194,18 @@ class HttpFallbackMixin:
|
|
117
194
|
# Construct URL
|
118
195
|
url = f"{self.http_fallback_url}/v1/invoke"
|
119
196
|
|
120
|
-
# Build headers with authentication
|
121
|
-
headers = {
|
197
|
+
# Build headers with authentication and request tracking
|
198
|
+
headers = {
|
199
|
+
'X-Request-ID': request_id,
|
200
|
+
'Content-Type': 'application/json',
|
201
|
+
'User-Agent': 'TamarModelClient/1.0'
|
202
|
+
}
|
203
|
+
|
204
|
+
# Add origin request ID if provided
|
205
|
+
if origin_request_id:
|
206
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
207
|
+
|
208
|
+
# Add JWT authentication if available
|
122
209
|
if hasattr(self, 'jwt_token') and self.jwt_token:
|
123
210
|
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
124
211
|
|
@@ -138,10 +225,101 @@ class HttpFallbackMixin:
|
|
138
225
|
# Parse response
|
139
226
|
data = response.json()
|
140
227
|
return ModelResponse(**data)
|
228
|
+
|
229
|
+
def _invoke_batch_http_fallback(self, batch_request: 'BatchModelRequest',
|
230
|
+
timeout: Optional[float] = None,
|
231
|
+
request_id: Optional[str] = None,
|
232
|
+
origin_request_id: Optional[str] = None) -> List['BatchModelResponse']:
|
233
|
+
"""HTTP batch fallback implementation"""
|
234
|
+
# Import here to avoid circular import
|
235
|
+
from ..schemas import BatchModelRequest, BatchModelResponse
|
236
|
+
|
237
|
+
# Check if http_fallback_url is available
|
238
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
239
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
240
|
+
|
241
|
+
self._ensure_http_client()
|
242
|
+
|
243
|
+
# Generate request ID if not provided
|
244
|
+
if not request_id:
|
245
|
+
request_id = generate_request_id()
|
246
|
+
|
247
|
+
# Log fallback usage
|
248
|
+
logger.warning(
|
249
|
+
f"🔻 Using HTTP fallback for batch request",
|
250
|
+
extra={
|
251
|
+
"log_type": "info",
|
252
|
+
"request_id": request_id,
|
253
|
+
"data": {
|
254
|
+
"origin_request_id": origin_request_id,
|
255
|
+
"batch_size": len(batch_request.items),
|
256
|
+
"fallback_url": self.http_fallback_url
|
257
|
+
}
|
258
|
+
}
|
259
|
+
)
|
260
|
+
|
261
|
+
# Convert to HTTP format
|
262
|
+
http_payload = {
|
263
|
+
"user_context": batch_request.user_context.model_dump(),
|
264
|
+
"items": []
|
265
|
+
}
|
266
|
+
|
267
|
+
# Convert each item
|
268
|
+
for item in batch_request.items:
|
269
|
+
item_payload = self._convert_to_http_format(item)
|
270
|
+
if hasattr(item, 'custom_id') and item.custom_id:
|
271
|
+
item_payload['custom_id'] = item.custom_id
|
272
|
+
if hasattr(item, 'priority') and item.priority is not None:
|
273
|
+
item_payload['priority'] = item.priority
|
274
|
+
http_payload['items'].append(item_payload)
|
275
|
+
|
276
|
+
# Construct URL
|
277
|
+
url = f"{self.http_fallback_url}/v1/batch-invoke"
|
278
|
+
|
279
|
+
# Build headers with authentication and request tracking
|
280
|
+
headers = {
|
281
|
+
'X-Request-ID': request_id,
|
282
|
+
'Content-Type': 'application/json',
|
283
|
+
'User-Agent': 'TamarModelClient/1.0'
|
284
|
+
}
|
285
|
+
|
286
|
+
# Add origin request ID if provided
|
287
|
+
if origin_request_id:
|
288
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
289
|
+
|
290
|
+
# Add JWT authentication if available
|
291
|
+
if hasattr(self, 'jwt_token') and self.jwt_token:
|
292
|
+
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
293
|
+
|
294
|
+
# Send batch request
|
295
|
+
response = self._http_client.post(
|
296
|
+
url,
|
297
|
+
json=http_payload,
|
298
|
+
timeout=timeout or 120, # Longer timeout for batch requests
|
299
|
+
headers=headers
|
300
|
+
)
|
301
|
+
response.raise_for_status()
|
302
|
+
|
303
|
+
# Parse response
|
304
|
+
data = response.json()
|
305
|
+
results = []
|
306
|
+
for item_data in data.get('results', []):
|
307
|
+
results.append(BatchModelResponse(**item_data))
|
308
|
+
|
309
|
+
return results
|
141
310
|
|
142
311
|
|
143
312
|
class AsyncHttpFallbackMixin:
|
144
|
-
"""HTTP fallback functionality for asynchronous clients
|
313
|
+
"""HTTP fallback functionality for asynchronous clients
|
314
|
+
|
315
|
+
This mixin requires the following attributes from the host class:
|
316
|
+
- http_fallback_url: str - The HTTP fallback URL (from BaseClient._init_resilient_features)
|
317
|
+
- jwt_token: Optional[str] - JWT token for authentication (from BaseClient)
|
318
|
+
|
319
|
+
Usage:
|
320
|
+
This mixin should be used with BaseClient or its subclasses that have
|
321
|
+
initialized the resilient features with _init_resilient_features().
|
322
|
+
"""
|
145
323
|
|
146
324
|
async def _ensure_http_client(self) -> None:
|
147
325
|
"""Ensure async HTTP client is initialized"""
|
@@ -149,7 +327,6 @@ class AsyncHttpFallbackMixin:
|
|
149
327
|
import aiohttp
|
150
328
|
self._http_session = aiohttp.ClientSession(
|
151
329
|
headers={
|
152
|
-
'Content-Type': 'application/json',
|
153
330
|
'User-Agent': 'AsyncTamarModelClient/1.0'
|
154
331
|
}
|
155
332
|
)
|
@@ -188,12 +365,17 @@ class AsyncHttpFallbackMixin:
|
|
188
365
|
data = json.loads(data_str)
|
189
366
|
yield ModelResponse(**data)
|
190
367
|
except json.JSONDecodeError:
|
191
|
-
logger.warning(f"Failed to parse streaming response: {data_str}")
|
368
|
+
logger.warning(f"⚠️ Failed to parse streaming response: {data_str}")
|
192
369
|
|
193
370
|
async def _invoke_http_fallback(self, model_request: ModelRequest,
|
194
371
|
timeout: Optional[float] = None,
|
195
|
-
request_id: Optional[str] = None
|
372
|
+
request_id: Optional[str] = None,
|
373
|
+
origin_request_id: Optional[str] = None) -> Any:
|
196
374
|
"""Async HTTP fallback implementation"""
|
375
|
+
# Check if http_fallback_url is available
|
376
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
377
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
378
|
+
|
197
379
|
await self._ensure_http_client()
|
198
380
|
|
199
381
|
# Generate request ID if not provided
|
@@ -204,21 +386,36 @@ class AsyncHttpFallbackMixin:
|
|
204
386
|
logger.warning(
|
205
387
|
f"🔻 Using HTTP fallback for request",
|
206
388
|
extra={
|
389
|
+
"log_type": "info",
|
207
390
|
"request_id": request_id,
|
208
|
-
"
|
209
|
-
|
210
|
-
|
391
|
+
"data": {
|
392
|
+
"origin_request_id": origin_request_id,
|
393
|
+
"provider": model_request.provider.value,
|
394
|
+
"model": model_request.model,
|
395
|
+
"fallback_url": self.http_fallback_url
|
396
|
+
}
|
211
397
|
}
|
212
398
|
)
|
213
399
|
|
214
400
|
# Convert to HTTP format
|
215
401
|
http_payload = self._convert_to_http_format(model_request)
|
402
|
+
print(http_payload)
|
216
403
|
|
217
404
|
# Construct URL
|
218
405
|
url = f"{self.http_fallback_url}/v1/invoke"
|
219
406
|
|
220
|
-
# Build headers with authentication
|
221
|
-
headers = {
|
407
|
+
# Build headers with authentication and request tracking
|
408
|
+
headers = {
|
409
|
+
'X-Request-ID': request_id,
|
410
|
+
'Content-Type': 'application/json',
|
411
|
+
'User-Agent': 'AsyncTamarModelClient/1.0'
|
412
|
+
}
|
413
|
+
|
414
|
+
# Add origin request ID if provided
|
415
|
+
if origin_request_id:
|
416
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
417
|
+
|
418
|
+
# Add JWT authentication if available
|
222
419
|
if hasattr(self, 'jwt_token') and self.jwt_token:
|
223
420
|
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
224
421
|
|
@@ -242,6 +439,91 @@ class AsyncHttpFallbackMixin:
|
|
242
439
|
data = await response.json()
|
243
440
|
return ModelResponse(**data)
|
244
441
|
|
442
|
+
async def _invoke_batch_http_fallback(self, batch_request: 'BatchModelRequest',
|
443
|
+
timeout: Optional[float] = None,
|
444
|
+
request_id: Optional[str] = None,
|
445
|
+
origin_request_id: Optional[str] = None) -> List['BatchModelResponse']:
|
446
|
+
"""Async HTTP batch fallback implementation"""
|
447
|
+
# Import here to avoid circular import
|
448
|
+
from ..schemas import BatchModelRequest, BatchModelResponse
|
449
|
+
|
450
|
+
# Check if http_fallback_url is available
|
451
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
452
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
453
|
+
|
454
|
+
await self._ensure_http_client()
|
455
|
+
|
456
|
+
# Generate request ID if not provided
|
457
|
+
if not request_id:
|
458
|
+
request_id = generate_request_id()
|
459
|
+
|
460
|
+
# Log fallback usage
|
461
|
+
logger.warning(
|
462
|
+
f"🔻 Using HTTP fallback for batch request",
|
463
|
+
extra={
|
464
|
+
"log_type": "info",
|
465
|
+
"request_id": request_id,
|
466
|
+
"data": {
|
467
|
+
"origin_request_id": origin_request_id,
|
468
|
+
"batch_size": len(batch_request.items),
|
469
|
+
"fallback_url": self.http_fallback_url
|
470
|
+
}
|
471
|
+
}
|
472
|
+
)
|
473
|
+
|
474
|
+
# Convert to HTTP format
|
475
|
+
http_payload = {
|
476
|
+
"user_context": batch_request.user_context.model_dump(),
|
477
|
+
"items": []
|
478
|
+
}
|
479
|
+
|
480
|
+
# Convert each item
|
481
|
+
for item in batch_request.items:
|
482
|
+
item_payload = self._convert_to_http_format(item)
|
483
|
+
if hasattr(item, 'custom_id') and item.custom_id:
|
484
|
+
item_payload['custom_id'] = item.custom_id
|
485
|
+
if hasattr(item, 'priority') and item.priority is not None:
|
486
|
+
item_payload['priority'] = item.priority
|
487
|
+
http_payload['items'].append(item_payload)
|
488
|
+
|
489
|
+
# Construct URL
|
490
|
+
url = f"{self.http_fallback_url}/v1/batch-invoke"
|
491
|
+
|
492
|
+
# Build headers with authentication and request tracking
|
493
|
+
headers = {
|
494
|
+
'X-Request-ID': request_id,
|
495
|
+
'Content-Type': 'application/json',
|
496
|
+
'User-Agent': 'AsyncTamarModelClient/1.0'
|
497
|
+
}
|
498
|
+
|
499
|
+
# Add origin request ID if provided
|
500
|
+
if origin_request_id:
|
501
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
502
|
+
|
503
|
+
# Add JWT authentication if available
|
504
|
+
if hasattr(self, 'jwt_token') and self.jwt_token:
|
505
|
+
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
506
|
+
|
507
|
+
# Send batch request
|
508
|
+
import aiohttp
|
509
|
+
timeout_obj = aiohttp.ClientTimeout(total=timeout or 120) if timeout else None
|
510
|
+
|
511
|
+
async with self._http_session.post(
|
512
|
+
url,
|
513
|
+
json=http_payload,
|
514
|
+
timeout=timeout_obj,
|
515
|
+
headers=headers
|
516
|
+
) as response:
|
517
|
+
response.raise_for_status()
|
518
|
+
|
519
|
+
# Parse response
|
520
|
+
data = await response.json()
|
521
|
+
results = []
|
522
|
+
for item_data in data.get('results', []):
|
523
|
+
results.append(BatchModelResponse(**item_data))
|
524
|
+
|
525
|
+
return results
|
526
|
+
|
245
527
|
async def _cleanup_http_session(self) -> None:
|
246
528
|
"""Clean up HTTP session"""
|
247
529
|
if hasattr(self, '_http_session') and self._http_session:
|
@@ -10,7 +10,7 @@ import threading
|
|
10
10
|
from typing import Optional, Dict
|
11
11
|
|
12
12
|
from ..json_formatter import JSONFormatter
|
13
|
-
from .utils import get_request_id
|
13
|
+
from .utils import get_request_id, get_origin_request_id
|
14
14
|
|
15
15
|
# gRPC 消息长度限制(32位系统兼容)
|
16
16
|
MAX_MESSAGE_LENGTH = 2 ** 31 - 1
|
@@ -45,6 +45,20 @@ class RequestIdFilter(logging.Filter):
|
|
45
45
|
"""
|
46
46
|
# 从 ContextVar 中获取当前的 request_id
|
47
47
|
record.request_id = get_request_id()
|
48
|
+
|
49
|
+
# 添加 origin_request_id 到 data 字段
|
50
|
+
origin_request_id = get_origin_request_id()
|
51
|
+
if origin_request_id:
|
52
|
+
# 确保 data 字段存在且是字典类型
|
53
|
+
if not hasattr(record, 'data'):
|
54
|
+
record.data = {}
|
55
|
+
elif record.data is None:
|
56
|
+
record.data = {}
|
57
|
+
elif isinstance(record.data, dict):
|
58
|
+
# 只有在 data 是字典且没有 origin_request_id 时才添加
|
59
|
+
if 'origin_request_id' not in record.data:
|
60
|
+
record.data['origin_request_id'] = origin_request_id
|
61
|
+
|
48
62
|
return True
|
49
63
|
|
50
64
|
|
tamar_model_client/core/utils.py
CHANGED
@@ -15,6 +15,7 @@ from pydantic import BaseModel
|
|
15
15
|
|
16
16
|
# 使用 contextvars 管理请求ID,支持异步和同步上下文中的请求追踪
|
17
17
|
_request_id: ContextVar[str] = ContextVar('request_id', default='-')
|
18
|
+
_origin_request_id: ContextVar[str] = ContextVar('origin_request_id', default=None)
|
18
19
|
|
19
20
|
|
20
21
|
def is_effective_value(value) -> bool:
|
@@ -168,4 +169,29 @@ def get_request_id() -> str:
|
|
168
169
|
Returns:
|
169
170
|
str: 当前的请求ID或默认值
|
170
171
|
"""
|
171
|
-
return _request_id.get()
|
172
|
+
return _request_id.get()
|
173
|
+
|
174
|
+
|
175
|
+
def set_origin_request_id(origin_request_id: str):
|
176
|
+
"""
|
177
|
+
设置当前上下文的原始请求ID
|
178
|
+
|
179
|
+
在 ContextVar 中设置原始请求ID,使得在整个异步调用链中
|
180
|
+
都能访问到同一个原始请求ID,便于追踪请求来源。
|
181
|
+
|
182
|
+
Args:
|
183
|
+
origin_request_id: 要设置的原始请求ID字符串
|
184
|
+
"""
|
185
|
+
_origin_request_id.set(origin_request_id)
|
186
|
+
|
187
|
+
|
188
|
+
def get_origin_request_id() -> str:
|
189
|
+
"""
|
190
|
+
获取当前上下文的原始请求ID
|
191
|
+
|
192
|
+
从 ContextVar 中获取当前的原始请求ID,如果没有设置则返回 None
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
str: 当前的原始请求ID或 None
|
196
|
+
"""
|
197
|
+
return _origin_request_id.get()
|