tamar-model-client 0.1.16__tar.gz → 0.1.18__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.
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/PKG-INFO +1 -1
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/setup.py +1 -1
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/__init__.py +2 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/async_client.py +227 -32
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/enums/invoke.py +2 -1
- tamar_model_client-0.1.18/tamar_model_client/json_formatter.py +26 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/schemas/inputs.py +54 -123
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/sync_client.py +211 -25
- tamar_model_client-0.1.18/tamar_model_client/utils.py +118 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client.egg-info/PKG-INFO +1 -1
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client.egg-info/SOURCES.txt +2 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/README.md +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/setup.cfg +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/auth.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/enums/__init__.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/enums/channel.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/enums/providers.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/exceptions.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/generated/__init__.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/generated/model_service_pb2.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/generated/model_service_pb2_grpc.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/schemas/__init__.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client/schemas/outputs.py +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client.egg-info/dependency_links.txt +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client.egg-info/requires.txt +0 -0
- {tamar_model_client-0.1.16 → tamar_model_client-0.1.18}/tamar_model_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
from .sync_client import TamarModelClient
|
2
2
|
from .async_client import AsyncTamarModelClient
|
3
3
|
from .exceptions import ModelManagerClientError, ConnectionError, ValidationError
|
4
|
+
from .json_formatter import JSONFormatter
|
4
5
|
|
5
6
|
__all__ = [
|
6
7
|
"TamarModelClient",
|
@@ -8,4 +9,5 @@ __all__ = [
|
|
8
9
|
"ModelManagerClientError",
|
9
10
|
"ConnectionError",
|
10
11
|
"ValidationError",
|
12
|
+
"JSONFormatter",
|
11
13
|
]
|
@@ -4,6 +4,7 @@ import base64
|
|
4
4
|
import json
|
5
5
|
import logging
|
6
6
|
import os
|
7
|
+
import time
|
7
8
|
import uuid
|
8
9
|
from contextvars import ContextVar
|
9
10
|
|
@@ -19,7 +20,8 @@ from .exceptions import ConnectionError
|
|
19
20
|
from .schemas import ModelRequest, ModelResponse, BatchModelRequest, BatchModelResponse
|
20
21
|
from .generated import model_service_pb2, model_service_pb2_grpc
|
21
22
|
from .schemas.inputs import GoogleGenAiInput, OpenAIResponsesInput, OpenAIChatCompletionsInput, \
|
22
|
-
GoogleVertexAIImagesInput, OpenAIImagesInput
|
23
|
+
GoogleVertexAIImagesInput, OpenAIImagesInput, OpenAIImagesEditInput
|
24
|
+
from .json_formatter import JSONFormatter
|
23
25
|
|
24
26
|
logger = logging.getLogger(__name__)
|
25
27
|
|
@@ -40,8 +42,8 @@ if not logger.hasHandlers():
|
|
40
42
|
# 创建日志处理器,输出到控制台
|
41
43
|
console_handler = logging.StreamHandler()
|
42
44
|
|
43
|
-
#
|
44
|
-
formatter =
|
45
|
+
# 使用 JSON 格式化器
|
46
|
+
formatter = JSONFormatter()
|
45
47
|
console_handler.setFormatter(formatter)
|
46
48
|
|
47
49
|
# 为当前记录器添加处理器
|
@@ -181,26 +183,31 @@ class AsyncTamarModelClient:
|
|
181
183
|
# 对于取消的情况进行指数退避重试
|
182
184
|
if isinstance(e, grpc.aio.AioRpcError) and e.code() == grpc.StatusCode.CANCELLED:
|
183
185
|
retry_count += 1
|
184
|
-
logger.
|
186
|
+
logger.info(f"❌ RPC cancelled, retrying {retry_count}/{self.max_retries}...",
|
187
|
+
extra={"log_type": "info", "data": {"retry_count": retry_count, "max_retries": self.max_retries, "error_code": "CANCELLED"}})
|
185
188
|
if retry_count < self.max_retries:
|
186
189
|
delay = self.retry_delay * (2 ** (retry_count - 1))
|
187
190
|
await asyncio.sleep(delay)
|
188
191
|
else:
|
189
|
-
logger.error("❌ Max retry reached for CANCELLED"
|
192
|
+
logger.error("❌ Max retry reached for CANCELLED",
|
193
|
+
extra={"log_type": "info", "data": {"error_code": "CANCELLED", "max_retries_reached": True}})
|
190
194
|
raise
|
191
195
|
# 针对其他 RPC 错误类型,如暂时的连接问题、服务器超时等
|
192
196
|
elif isinstance(e, grpc.RpcError) and e.code() in {grpc.StatusCode.UNAVAILABLE,
|
193
197
|
grpc.StatusCode.DEADLINE_EXCEEDED}:
|
194
198
|
retry_count += 1
|
195
|
-
logger.
|
199
|
+
logger.info(f"❌ gRPC error {e.code()}, retrying {retry_count}/{self.max_retries}...",
|
200
|
+
extra={"log_type": "info", "data": {"retry_count": retry_count, "max_retries": self.max_retries, "error_code": str(e.code())}})
|
196
201
|
if retry_count < self.max_retries:
|
197
202
|
delay = self.retry_delay * (2 ** (retry_count - 1))
|
198
203
|
await asyncio.sleep(delay)
|
199
204
|
else:
|
200
|
-
logger.error(f"❌ Max retry reached for {e.code()}"
|
205
|
+
logger.error(f"❌ Max retry reached for {e.code()}",
|
206
|
+
extra={"log_type": "info", "data": {"error_code": str(e.code()), "max_retries_reached": True}})
|
201
207
|
raise
|
202
208
|
else:
|
203
|
-
logger.error(f"❌ Non-retryable gRPC error: {e}", exc_info=True
|
209
|
+
logger.error(f"❌ Non-retryable gRPC error: {e}", exc_info=True,
|
210
|
+
extra={"log_type": "info", "data": {"error_code": str(e.code()) if hasattr(e, 'code') else None, "retryable": False}})
|
204
211
|
raise
|
205
212
|
|
206
213
|
async def _retry_request_stream(self, func, *args, **kwargs):
|
@@ -212,26 +219,31 @@ class AsyncTamarModelClient:
|
|
212
219
|
# 对于取消的情况进行指数退避重试
|
213
220
|
if isinstance(e, grpc.aio.AioRpcError) and e.code() == grpc.StatusCode.CANCELLED:
|
214
221
|
retry_count += 1
|
215
|
-
logger.
|
222
|
+
logger.info(f"❌ RPC cancelled, retrying {retry_count}/{self.max_retries}...",
|
223
|
+
extra={"log_type": "info", "data": {"retry_count": retry_count, "max_retries": self.max_retries, "error_code": "CANCELLED"}})
|
216
224
|
if retry_count < self.max_retries:
|
217
225
|
delay = self.retry_delay * (2 ** (retry_count - 1))
|
218
226
|
await asyncio.sleep(delay)
|
219
227
|
else:
|
220
|
-
logger.error("❌ Max retry reached for CANCELLED"
|
228
|
+
logger.error("❌ Max retry reached for CANCELLED",
|
229
|
+
extra={"log_type": "info", "data": {"error_code": "CANCELLED", "max_retries_reached": True}})
|
221
230
|
raise
|
222
231
|
# 针对其他 RPC 错误类型,如暂时的连接问题、服务器超时等
|
223
232
|
elif isinstance(e, grpc.RpcError) and e.code() in {grpc.StatusCode.UNAVAILABLE,
|
224
233
|
grpc.StatusCode.DEADLINE_EXCEEDED}:
|
225
234
|
retry_count += 1
|
226
|
-
logger.
|
235
|
+
logger.info(f"❌ gRPC error {e.code()}, retrying {retry_count}/{self.max_retries}...",
|
236
|
+
extra={"log_type": "info", "data": {"retry_count": retry_count, "max_retries": self.max_retries, "error_code": str(e.code())}})
|
227
237
|
if retry_count < self.max_retries:
|
228
238
|
delay = self.retry_delay * (2 ** (retry_count - 1))
|
229
239
|
await asyncio.sleep(delay)
|
230
240
|
else:
|
231
|
-
logger.error(f"❌ Max retry reached for {e.code()}"
|
241
|
+
logger.error(f"❌ Max retry reached for {e.code()}",
|
242
|
+
extra={"log_type": "info", "data": {"error_code": str(e.code()), "max_retries_reached": True}})
|
232
243
|
raise
|
233
244
|
else:
|
234
|
-
logger.error(f"❌ Non-retryable gRPC error: {e}", exc_info=True
|
245
|
+
logger.error(f"❌ Non-retryable gRPC error: {e}", exc_info=True,
|
246
|
+
extra={"log_type": "info", "data": {"error_code": str(e.code()) if hasattr(e, 'code') else None, "retryable": False}})
|
235
247
|
raise
|
236
248
|
|
237
249
|
def _build_auth_metadata(self, request_id: str) -> list:
|
@@ -266,32 +278,40 @@ class AsyncTamarModelClient:
|
|
266
278
|
credentials,
|
267
279
|
options=options
|
268
280
|
)
|
269
|
-
logger.info("🔐 Using secure gRPC channel (TLS enabled)"
|
281
|
+
logger.info("🔐 Using secure gRPC channel (TLS enabled)",
|
282
|
+
extra={"log_type": "info", "data": {"tls_enabled": True, "server_address": self.server_address}})
|
270
283
|
else:
|
271
284
|
self.channel = grpc.aio.insecure_channel(
|
272
285
|
self.server_address,
|
273
286
|
options=options
|
274
287
|
)
|
275
|
-
logger.info("🔓 Using insecure gRPC channel (TLS disabled)"
|
288
|
+
logger.info("🔓 Using insecure gRPC channel (TLS disabled)",
|
289
|
+
extra={"log_type": "info", "data": {"tls_enabled": False, "server_address": self.server_address}})
|
276
290
|
await self.channel.channel_ready()
|
277
291
|
self.stub = model_service_pb2_grpc.ModelServiceStub(self.channel)
|
278
|
-
logger.info(f"✅ gRPC channel initialized to {self.server_address}"
|
292
|
+
logger.info(f"✅ gRPC channel initialized to {self.server_address}",
|
293
|
+
extra={"log_type": "info", "data": {"status": "success", "server_address": self.server_address}})
|
279
294
|
return
|
280
295
|
except grpc.FutureTimeoutError as e:
|
281
|
-
logger.error(f"❌ gRPC channel initialization timed out: {str(e)}", exc_info=True
|
296
|
+
logger.error(f"❌ gRPC channel initialization timed out: {str(e)}", exc_info=True,
|
297
|
+
extra={"log_type": "info", "data": {"error_type": "timeout", "server_address": self.server_address}})
|
282
298
|
except grpc.RpcError as e:
|
283
|
-
logger.error(f"❌ gRPC channel initialization failed: {str(e)}", exc_info=True
|
299
|
+
logger.error(f"❌ gRPC channel initialization failed: {str(e)}", exc_info=True,
|
300
|
+
extra={"log_type": "info", "data": {"error_type": "rpc_error", "server_address": self.server_address}})
|
284
301
|
except Exception as e:
|
285
|
-
logger.error(f"❌ Unexpected error during channel initialization: {str(e)}", exc_info=True
|
302
|
+
logger.error(f"❌ Unexpected error during channel initialization: {str(e)}", exc_info=True,
|
303
|
+
extra={"log_type": "info", "data": {"error_type": "unexpected", "server_address": self.server_address}})
|
286
304
|
|
287
305
|
retry_count += 1
|
288
306
|
if retry_count > self.max_retries:
|
289
|
-
logger.error(f"❌ Failed to initialize gRPC channel after {self.max_retries} retries.", exc_info=True
|
307
|
+
logger.error(f"❌ Failed to initialize gRPC channel after {self.max_retries} retries.", exc_info=True,
|
308
|
+
extra={"log_type": "info", "data": {"max_retries_reached": True, "server_address": self.server_address}})
|
290
309
|
raise ConnectionError(f"❌ Failed to initialize gRPC channel after {self.max_retries} retries.")
|
291
310
|
|
292
311
|
# 指数退避:延迟时间 = retry_delay * (2 ^ (retry_count - 1))
|
293
312
|
delay = self.retry_delay * (2 ** (retry_count - 1))
|
294
|
-
logger.info(f"🚀 Retrying connection (attempt {retry_count}/{self.max_retries}) after {delay:.2f}s delay..."
|
313
|
+
logger.info(f"🚀 Retrying connection (attempt {retry_count}/{self.max_retries}) after {delay:.2f}s delay...",
|
314
|
+
extra={"log_type": "info", "data": {"retry_count": retry_count, "max_retries": self.max_retries, "delay": delay}})
|
295
315
|
await asyncio.sleep(delay)
|
296
316
|
|
297
317
|
async def _stream(self, request, metadata, invoke_timeout) -> AsyncIterator[ModelResponse]:
|
@@ -303,6 +323,66 @@ class AsyncTamarModelClient:
|
|
303
323
|
raw_response=json.loads(response.raw_response) if response.raw_response else None,
|
304
324
|
request_id=response.request_id if response.request_id else None,
|
305
325
|
)
|
326
|
+
|
327
|
+
async def _stream_with_logging(self, request, metadata, invoke_timeout, start_time, model_request) -> AsyncIterator[ModelResponse]:
|
328
|
+
"""流式响应的包装器,用于记录完整的响应日志"""
|
329
|
+
total_content = ""
|
330
|
+
final_usage = None
|
331
|
+
error_occurred = None
|
332
|
+
chunk_count = 0
|
333
|
+
|
334
|
+
try:
|
335
|
+
async for response in self._stream(request, metadata, invoke_timeout):
|
336
|
+
chunk_count += 1
|
337
|
+
if response.content:
|
338
|
+
total_content += response.content
|
339
|
+
if response.usage:
|
340
|
+
final_usage = response.usage
|
341
|
+
if response.error:
|
342
|
+
error_occurred = response.error
|
343
|
+
yield response
|
344
|
+
|
345
|
+
# 流式响应完成,记录成功日志
|
346
|
+
duration = time.time() - start_time
|
347
|
+
logger.info(
|
348
|
+
f"✅ Stream completed successfully | chunks: {chunk_count}",
|
349
|
+
extra={
|
350
|
+
"log_type": "response",
|
351
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
352
|
+
"duration": duration,
|
353
|
+
"data": {
|
354
|
+
"provider": model_request.provider.value,
|
355
|
+
"invoke_type": model_request.invoke_type.value,
|
356
|
+
"model": model_request.model,
|
357
|
+
"stream": True,
|
358
|
+
"chunks_count": chunk_count,
|
359
|
+
"total_length": len(total_content),
|
360
|
+
"usage": final_usage
|
361
|
+
}
|
362
|
+
}
|
363
|
+
)
|
364
|
+
except Exception as e:
|
365
|
+
# 流式响应出错,记录错误日志
|
366
|
+
duration = time.time() - start_time
|
367
|
+
logger.error(
|
368
|
+
f"❌ Stream failed after {chunk_count} chunks: {str(e)}",
|
369
|
+
exc_info=True,
|
370
|
+
extra={
|
371
|
+
"log_type": "response",
|
372
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
373
|
+
"duration": duration,
|
374
|
+
"data": {
|
375
|
+
"provider": model_request.provider.value,
|
376
|
+
"invoke_type": model_request.invoke_type.value,
|
377
|
+
"model": model_request.model,
|
378
|
+
"stream": True,
|
379
|
+
"chunks_count": chunk_count,
|
380
|
+
"error_type": type(e).__name__,
|
381
|
+
"partial_content_length": len(total_content)
|
382
|
+
}
|
383
|
+
}
|
384
|
+
)
|
385
|
+
raise
|
306
386
|
|
307
387
|
async def _invoke_request(self, request, metadata, invoke_timeout):
|
308
388
|
async for response in self.stub.Invoke(request, metadata=metadata, timeout=invoke_timeout):
|
@@ -345,8 +425,22 @@ class AsyncTamarModelClient:
|
|
345
425
|
metadata = self._build_auth_metadata(request_id) # 将 request_id 加入到请求头
|
346
426
|
|
347
427
|
# 记录开始日志
|
428
|
+
start_time = time.time()
|
348
429
|
logger.info(
|
349
|
-
f"🔵 Request Start | request_id: {request_id} | provider: {model_request.provider} | invoke_type: {model_request.invoke_type}
|
430
|
+
f"🔵 Request Start | request_id: {request_id} | provider: {model_request.provider} | invoke_type: {model_request.invoke_type}",
|
431
|
+
extra={
|
432
|
+
"log_type": "request",
|
433
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
434
|
+
"data": {
|
435
|
+
"provider": model_request.provider.value,
|
436
|
+
"invoke_type": model_request.invoke_type.value,
|
437
|
+
"model": model_request.model,
|
438
|
+
"stream": model_request.stream,
|
439
|
+
"org_id": model_request.user_context.org_id,
|
440
|
+
"user_id": model_request.user_context.user_id,
|
441
|
+
"client_type": model_request.user_context.client_type
|
442
|
+
}
|
443
|
+
})
|
350
444
|
|
351
445
|
# 动态根据 provider/invoke_type 决定使用哪个 input 字段
|
352
446
|
try:
|
@@ -363,6 +457,8 @@ class AsyncTamarModelClient:
|
|
363
457
|
allowed_fields = OpenAIChatCompletionsInput.model_fields.keys()
|
364
458
|
case ((ProviderType.OPENAI | ProviderType.AZURE), InvokeType.IMAGE_GENERATION):
|
365
459
|
allowed_fields = OpenAIImagesInput.model_fields.keys()
|
460
|
+
case ((ProviderType.OPENAI | ProviderType.AZURE), InvokeType.IMAGE_EDIT_GENERATION):
|
461
|
+
allowed_fields = OpenAIImagesEditInput.model_fields.keys()
|
366
462
|
case _:
|
367
463
|
raise ValueError(
|
368
464
|
f"Unsupported provider/invoke_type combination: {model_request.provider} + {model_request.invoke_type}")
|
@@ -402,16 +498,63 @@ class AsyncTamarModelClient:
|
|
402
498
|
try:
|
403
499
|
invoke_timeout = timeout or self.default_invoke_timeout
|
404
500
|
if model_request.stream:
|
405
|
-
|
501
|
+
# 对于流式响应,使用带日志记录的包装器
|
502
|
+
stream_generator = await self._retry_request_stream(self._stream, request, metadata, invoke_timeout)
|
503
|
+
return self._stream_with_logging(request, metadata, invoke_timeout, start_time, model_request)
|
406
504
|
else:
|
407
|
-
|
505
|
+
result = await self._retry_request(self._invoke_request, request, metadata, invoke_timeout)
|
506
|
+
|
507
|
+
# 记录非流式响应的成功日志
|
508
|
+
duration = time.time() - start_time
|
509
|
+
logger.info(
|
510
|
+
f"✅ Request completed successfully",
|
511
|
+
extra={
|
512
|
+
"log_type": "response",
|
513
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
514
|
+
"duration": duration,
|
515
|
+
"data": {
|
516
|
+
"provider": model_request.provider.value,
|
517
|
+
"invoke_type": model_request.invoke_type.value,
|
518
|
+
"model": model_request.model,
|
519
|
+
"stream": False,
|
520
|
+
"content_length": len(result.content) if result.content else 0,
|
521
|
+
"usage": result.usage
|
522
|
+
}
|
523
|
+
}
|
524
|
+
)
|
525
|
+
return result
|
408
526
|
except grpc.RpcError as e:
|
527
|
+
duration = time.time() - start_time
|
409
528
|
error_message = f"❌ Invoke gRPC failed: {str(e)}"
|
410
|
-
logger.error(error_message, exc_info=True
|
529
|
+
logger.error(error_message, exc_info=True,
|
530
|
+
extra={
|
531
|
+
"log_type": "response",
|
532
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
533
|
+
"duration": duration,
|
534
|
+
"data": {
|
535
|
+
"error_type": "grpc_error",
|
536
|
+
"error_code": str(e.code()) if hasattr(e, 'code') else None,
|
537
|
+
"provider": model_request.provider.value,
|
538
|
+
"invoke_type": model_request.invoke_type.value,
|
539
|
+
"model": model_request.model
|
540
|
+
}
|
541
|
+
})
|
411
542
|
raise e
|
412
543
|
except Exception as e:
|
544
|
+
duration = time.time() - start_time
|
413
545
|
error_message = f"❌ Invoke other error: {str(e)}"
|
414
|
-
logger.error(error_message, exc_info=True
|
546
|
+
logger.error(error_message, exc_info=True,
|
547
|
+
extra={
|
548
|
+
"log_type": "response",
|
549
|
+
"uri": f"/invoke/{model_request.provider.value}/{model_request.invoke_type.value}",
|
550
|
+
"duration": duration,
|
551
|
+
"data": {
|
552
|
+
"error_type": "other_error",
|
553
|
+
"provider": model_request.provider.value,
|
554
|
+
"invoke_type": model_request.invoke_type.value,
|
555
|
+
"model": model_request.model
|
556
|
+
}
|
557
|
+
})
|
415
558
|
raise e
|
416
559
|
|
417
560
|
async def invoke_batch(self, batch_request_model: BatchModelRequest, timeout: Optional[float] = None,
|
@@ -442,8 +585,19 @@ class AsyncTamarModelClient:
|
|
442
585
|
metadata = self._build_auth_metadata(request_id) # 将 request_id 加入到请求头
|
443
586
|
|
444
587
|
# 记录开始日志
|
588
|
+
start_time = time.time()
|
445
589
|
logger.info(
|
446
|
-
f"🔵 Batch Request Start | request_id: {request_id} | batch_size: {len(batch_request_model.items)}
|
590
|
+
f"🔵 Batch Request Start | request_id: {request_id} | batch_size: {len(batch_request_model.items)}",
|
591
|
+
extra={
|
592
|
+
"log_type": "request",
|
593
|
+
"uri": "/batch_invoke",
|
594
|
+
"data": {
|
595
|
+
"batch_size": len(batch_request_model.items),
|
596
|
+
"org_id": batch_request_model.user_context.org_id,
|
597
|
+
"user_id": batch_request_model.user_context.user_id,
|
598
|
+
"client_type": batch_request_model.user_context.client_type
|
599
|
+
}
|
600
|
+
})
|
447
601
|
|
448
602
|
# 构造批量请求
|
449
603
|
items = []
|
@@ -461,6 +615,8 @@ class AsyncTamarModelClient:
|
|
461
615
|
allowed_fields = OpenAIChatCompletionsInput.model_fields.keys()
|
462
616
|
case ((ProviderType.OPENAI | ProviderType.AZURE), InvokeType.IMAGE_GENERATION):
|
463
617
|
allowed_fields = OpenAIImagesInput.model_fields.keys()
|
618
|
+
case ((ProviderType.OPENAI | ProviderType.AZURE), InvokeType.IMAGE_EDIT_GENERATION):
|
619
|
+
allowed_fields = OpenAIImagesEditInput.model_fields.keys()
|
464
620
|
case _:
|
465
621
|
raise ValueError(
|
466
622
|
f"Unsupported provider/invoke_type combination: {model_request_item.provider} + {model_request_item.invoke_type}")
|
@@ -516,17 +672,54 @@ class AsyncTamarModelClient:
|
|
516
672
|
error=res_item.error or None,
|
517
673
|
custom_id=res_item.custom_id if res_item.custom_id else None
|
518
674
|
))
|
519
|
-
|
675
|
+
batch_response = BatchModelResponse(
|
520
676
|
request_id=response.request_id if response.request_id else None,
|
521
677
|
responses=result
|
522
678
|
)
|
679
|
+
|
680
|
+
# 记录成功日志
|
681
|
+
duration = time.time() - start_time
|
682
|
+
logger.info(
|
683
|
+
f"✅ Batch request completed successfully",
|
684
|
+
extra={
|
685
|
+
"log_type": "response",
|
686
|
+
"uri": "/batch_invoke",
|
687
|
+
"duration": duration,
|
688
|
+
"data": {
|
689
|
+
"batch_size": len(batch_request_model.items),
|
690
|
+
"responses_count": len(result)
|
691
|
+
}
|
692
|
+
}
|
693
|
+
)
|
694
|
+
return batch_response
|
523
695
|
except grpc.RpcError as e:
|
696
|
+
duration = time.time() - start_time
|
524
697
|
error_message = f"❌ BatchInvoke gRPC failed: {str(e)}"
|
525
|
-
logger.error(error_message, exc_info=True
|
698
|
+
logger.error(error_message, exc_info=True,
|
699
|
+
extra={
|
700
|
+
"log_type": "response",
|
701
|
+
"uri": "/batch_invoke",
|
702
|
+
"duration": duration,
|
703
|
+
"data": {
|
704
|
+
"error_type": "grpc_error",
|
705
|
+
"error_code": str(e.code()) if hasattr(e, 'code') else None,
|
706
|
+
"batch_size": len(batch_request_model.items)
|
707
|
+
}
|
708
|
+
})
|
526
709
|
raise e
|
527
710
|
except Exception as e:
|
711
|
+
duration = time.time() - start_time
|
528
712
|
error_message = f"❌ BatchInvoke other error: {str(e)}"
|
529
|
-
logger.error(error_message, exc_info=True
|
713
|
+
logger.error(error_message, exc_info=True,
|
714
|
+
extra={
|
715
|
+
"log_type": "response",
|
716
|
+
"uri": "/batch_invoke",
|
717
|
+
"duration": duration,
|
718
|
+
"data": {
|
719
|
+
"error_type": "other_error",
|
720
|
+
"batch_size": len(batch_request_model.items)
|
721
|
+
}
|
722
|
+
})
|
530
723
|
raise e
|
531
724
|
|
532
725
|
async def close(self):
|
@@ -534,7 +727,8 @@ class AsyncTamarModelClient:
|
|
534
727
|
if self.channel and not self._closed:
|
535
728
|
await self.channel.close()
|
536
729
|
self._closed = True
|
537
|
-
logger.info("✅ gRPC channel closed"
|
730
|
+
logger.info("✅ gRPC channel closed",
|
731
|
+
extra={"log_type": "info", "data": {"status": "success"}})
|
538
732
|
|
539
733
|
def _safe_sync_close(self):
|
540
734
|
"""进程退出时自动关闭 channel(事件循环处理兼容)"""
|
@@ -546,7 +740,8 @@ class AsyncTamarModelClient:
|
|
546
740
|
else:
|
547
741
|
loop.run_until_complete(self.close())
|
548
742
|
except Exception as e:
|
549
|
-
logger.
|
743
|
+
logger.info(f"❌ gRPC channel close failed at exit: {e}",
|
744
|
+
extra={"log_type": "info", "data": {"status": "failed", "error": str(e)}})
|
550
745
|
|
551
746
|
async def __aenter__(self):
|
552
747
|
"""支持 async with 自动初始化连接"""
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
|
6
|
+
class JSONFormatter(logging.Formatter):
|
7
|
+
def format(self, record):
|
8
|
+
# log_type 只能是 request、response 或 info
|
9
|
+
log_type = getattr(record, "log_type", "info")
|
10
|
+
if log_type not in ["request", "response", "info"]:
|
11
|
+
log_type = "info"
|
12
|
+
|
13
|
+
log_data = {
|
14
|
+
"timestamp": datetime.fromtimestamp(record.created).isoformat(),
|
15
|
+
"level": record.levelname,
|
16
|
+
"type": log_type,
|
17
|
+
"uri": getattr(record, "uri", None),
|
18
|
+
"request_id": getattr(record, "request_id", None),
|
19
|
+
"data": getattr(record, "data", None),
|
20
|
+
"message": record.getMessage(),
|
21
|
+
"duration": getattr(record, "duration", None),
|
22
|
+
}
|
23
|
+
# 增加 trace 支持
|
24
|
+
if hasattr(record, "trace"):
|
25
|
+
log_data["trace"] = getattr(record, "trace")
|
26
|
+
return json.dumps(log_data, ensure_ascii=False)
|