Update openai_ondemand_adapter.py
Browse files- openai_ondemand_adapter.py +86 -40
openai_ondemand_adapter.py
CHANGED
|
@@ -440,11 +440,14 @@ def chat_completions():
|
|
| 440 |
logging.warning(f"【请求处理 - Key轮换尝试 {key_retry_count}】HTTP/请求错误。Status: {status_code_from_exc}, Key: {keymgr.display_key(selected_apikey_for_outer_retry) if selected_apikey_for_outer_retry else 'N/A'}, Error: {http_err_outer}")
|
| 441 |
|
| 442 |
if selected_apikey_for_outer_retry:
|
| 443 |
-
if status_code_from_exc == 524:
|
| 444 |
logging.info(f"【KeyManager】Key {keymgr.display_key(selected_apikey_for_outer_retry)} not marked bad due to 524 error.")
|
| 445 |
-
|
|
|
|
| 446 |
keymgr.mark_bad(selected_apikey_for_outer_retry)
|
| 447 |
-
|
|
|
|
|
|
|
| 448 |
if key_retry_count >= max_key_retries:
|
| 449 |
logging.error(f"【请求处理】所有Key轮换尝试均失败。最后错误: {last_exception_for_key_retry}")
|
| 450 |
break
|
|
@@ -455,22 +458,23 @@ def chat_completions():
|
|
| 455 |
except Exception as e_outer:
|
| 456 |
last_exception_for_key_retry = e_outer
|
| 457 |
logging.error(f"【请求处理 - Key轮换尝试 {key_retry_count}】发生意外严重错误: {e_outer}", exc_info=True)
|
| 458 |
-
if selected_apikey_for_outer_retry:
|
| 459 |
keymgr.mark_bad(selected_apikey_for_outer_retry)
|
| 460 |
break
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
| 466 |
|
| 467 |
if is_stream_request:
|
| 468 |
def error_stream_gen():
|
| 469 |
-
yield format_openai_sse_delta({"error": {"message": error_message, "type": "
|
| 470 |
yield "data: [DONE]\n\n"
|
| 471 |
-
return Response(error_stream_gen(), content_type='text/event-stream', status=
|
| 472 |
else:
|
| 473 |
-
return jsonify({"error": error_message, "code":
|
| 474 |
|
| 475 |
return with_valid_key_and_session(attempt_ondemand_request_wrapper)
|
| 476 |
|
|
@@ -494,17 +498,22 @@ def handle_stream_request(initial_apikey, initial_session_id, query_str, endpoin
|
|
| 494 |
logging.info(f"【流式请求-空回复重试 {empty_retry_attempt_num-1}】新Key/Session获取成功: Key={keymgr.display_key(current_apikey_for_attempt)}, Session={current_session_id_for_attempt}")
|
| 495 |
except (ValueError, requests.exceptions.RequestException) as e_key_session:
|
| 496 |
logging.warning(f"【流式请求-空回复重试 {empty_retry_attempt_num-1}】获取新Key/Session失败: {e_key_session}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
if current_apikey_for_attempt and not isinstance(e_key_session, ValueError):
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
status_code_from_exc = e_key_session.response.status_code
|
| 501 |
-
if status_code_from_exc == 524:
|
| 502 |
-
logging.info(f"【KeyManager】Key {keymgr.display_key(current_apikey_for_attempt)} not marked bad for 524 error during key/session acquisition for retry.")
|
| 503 |
else:
|
| 504 |
keymgr.mark_bad(current_apikey_for_attempt)
|
| 505 |
|
| 506 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
yield "data: [DONE]\n\n"
|
| 509 |
return
|
| 510 |
time.sleep(1)
|
|
@@ -543,8 +552,10 @@ def handle_stream_request(initial_apikey, initial_session_id, query_str, endpoin
|
|
| 543 |
raise e_req
|
| 544 |
|
| 545 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
|
| 548 |
yield "data: [DONE]\n\n"
|
| 549 |
return
|
| 550 |
time.sleep(1)
|
|
@@ -560,9 +571,11 @@ def handle_stream_request(initial_apikey, initial_session_id, query_str, endpoin
|
|
| 560 |
|
| 561 |
logging.warning(f"【流式请求】({log_attempt_str}) 返回空内容。")
|
| 562 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 563 |
-
|
|
|
|
|
|
|
| 564 |
yield format_openai_sse_delta({
|
| 565 |
-
"error": {"message":
|
| 566 |
})
|
| 567 |
yield "data: [DONE]\n\n"
|
| 568 |
return
|
|
@@ -570,8 +583,13 @@ def handle_stream_request(initial_apikey, initial_session_id, query_str, endpoin
|
|
| 570 |
logging.info(f"【流式请求】空回复,将在1秒后重试下一个Key。当前总尝试 {empty_retry_attempt_num}/{max_empty_response_retries}")
|
| 571 |
time.sleep(1)
|
| 572 |
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
yield "data: [DONE]\n\n"
|
| 576 |
|
| 577 |
|
|
@@ -596,16 +614,21 @@ def handle_non_stream_request(initial_apikey, initial_session_id, query_str, end
|
|
| 596 |
logging.info(f"【同步请求-空回复重试 {empty_retry_attempt_num-1}】新Key/Session获取成功: Key={keymgr.display_key(current_apikey_for_attempt)}, Session={current_session_id_for_attempt}")
|
| 597 |
except (ValueError, requests.exceptions.RequestException) as e_key_session:
|
| 598 |
logging.warning(f"【同步请求-空回复重试 {empty_retry_attempt_num-1}】获取新Key/Session失败: {e_key_session}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
if current_apikey_for_attempt and not isinstance(e_key_session, ValueError):
|
| 600 |
-
|
| 601 |
-
if hasattr(e_key_session, 'response') and e_key_session.response is not None:
|
| 602 |
-
status_code_from_exc = e_key_session.response.status_code
|
| 603 |
-
if status_code_from_exc == 524:
|
| 604 |
logging.info(f"【KeyManager】Key {keymgr.display_key(current_apikey_for_attempt)} not marked bad for 524 error during key/session acquisition for non-stream retry.")
|
| 605 |
else:
|
| 606 |
keymgr.mark_bad(current_apikey_for_attempt)
|
| 607 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
time.sleep(1)
|
| 610 |
current_apikey_for_attempt = None
|
| 611 |
continue
|
|
@@ -623,7 +646,10 @@ def handle_non_stream_request(initial_apikey, initial_session_id, query_str, end
|
|
| 623 |
response_json = resp.json()
|
| 624 |
if "data" not in response_json or "answer" not in response_json["data"]:
|
| 625 |
logging.error(f"【OnDemand同步错误】响应格式不符合预期 ({log_attempt_str})。Session: {current_session_id_for_attempt}, 响应: {str(response_json)[:500]}")
|
| 626 |
-
|
|
|
|
|
|
|
|
|
|
| 627 |
|
| 628 |
ai_response_content = response_json["data"]["answer"]
|
| 629 |
if ai_response_content is None: ai_response_content = ""
|
|
@@ -640,15 +666,17 @@ def handle_non_stream_request(initial_apikey, initial_session_id, query_str, end
|
|
| 640 |
else:
|
| 641 |
logging.warning(f"【同步请求】({log_attempt_str}) 返回空回复。")
|
| 642 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 643 |
-
|
|
|
|
|
|
|
| 644 |
return jsonify({
|
| 645 |
-
"error":
|
| 646 |
"id": "chatcmpl-" + str(uuid.uuid4())[:12], "object": "chat.completion", "created": int(time.time()),
|
| 647 |
"model": openai_model_name_for_response,
|
| 648 |
-
"choices": [{"index": 0, "message": {"role": "assistant", "content": ""}, "finish_reason": "length"}],
|
| 649 |
"usage": {},
|
| 650 |
-
"code":
|
| 651 |
-
}),
|
| 652 |
logging.info(f"【同步请求】空回复,将在1秒后重试下一个Key。当前总尝试 {empty_retry_attempt_num}/{max_empty_response_retries}")
|
| 653 |
time.sleep(1)
|
| 654 |
|
|
@@ -669,16 +697,34 @@ def handle_non_stream_request(initial_apikey, initial_session_id, query_str, end
|
|
| 669 |
if empty_retry_attempt_num == 1:
|
| 670 |
raise e_req
|
| 671 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 672 |
-
|
| 673 |
-
|
|
|
|
|
|
|
| 674 |
time.sleep(1)
|
| 675 |
continue
|
| 676 |
except (ValueError, KeyError, json.JSONDecodeError) as e_parse:
|
| 677 |
-
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
-
|
| 681 |
-
|
|
|
|
|
|
|
|
|
|
| 682 |
|
| 683 |
|
| 684 |
@app.route("/v1/models", methods=["GET"])
|
|
|
|
| 440 |
logging.warning(f"【请求处理 - Key轮换尝试 {key_retry_count}】HTTP/请求错误。Status: {status_code_from_exc}, Key: {keymgr.display_key(selected_apikey_for_outer_retry) if selected_apikey_for_outer_retry else 'N/A'}, Error: {http_err_outer}")
|
| 441 |
|
| 442 |
if selected_apikey_for_outer_retry:
|
| 443 |
+
if status_code_from_exc == 524: # HTTP 524: A Timeout Occurred (Cloudflare)
|
| 444 |
logging.info(f"【KeyManager】Key {keymgr.display_key(selected_apikey_for_outer_retry)} not marked bad due to 524 error.")
|
| 445 |
+
# For other client/server errors that might indicate a key issue or persistent service issue with this key
|
| 446 |
+
elif status_code_from_exc and (400 <= status_code_from_exc < 500 or status_code_from_exc in [500, 502, 503]): # excluding 524
|
| 447 |
keymgr.mark_bad(selected_apikey_for_outer_retry)
|
| 448 |
+
elif not status_code_from_exc : # Network errors without a status code (e.g., connection refused, DNS failure)
|
| 449 |
+
keymgr.mark_bad(selected_apikey_for_outer_retry)
|
| 450 |
+
|
| 451 |
if key_retry_count >= max_key_retries:
|
| 452 |
logging.error(f"【请求处理】所有Key轮换尝试均失败。最后错误: {last_exception_for_key_retry}")
|
| 453 |
break
|
|
|
|
| 458 |
except Exception as e_outer:
|
| 459 |
last_exception_for_key_retry = e_outer
|
| 460 |
logging.error(f"【请求处理 - Key轮换尝试 {key_retry_count}】发生意外严重错误: {e_outer}", exc_info=True)
|
| 461 |
+
if selected_apikey_for_outer_retry: # Mark key bad on any other unexpected exception during setup
|
| 462 |
keymgr.mark_bad(selected_apikey_for_outer_retry)
|
| 463 |
break
|
| 464 |
|
| 465 |
+
# This block is reached if all key_retry_count attempts in with_valid_key_and_session fail
|
| 466 |
+
error_message = "重试次数过多,请检查上下文长度! 或联系管理员!" # User requested message
|
| 467 |
+
error_code_str = "max_retries_check_context_contact_admin" # Custom code for this scenario
|
| 468 |
+
|
| 469 |
+
logging.error(f"【请求处理】所有Key/Session获取尝试失败。最终错误: {error_message} Last underlying exception: {last_exception_for_key_retry}")
|
| 470 |
|
| 471 |
if is_stream_request:
|
| 472 |
def error_stream_gen():
|
| 473 |
+
yield format_openai_sse_delta({"error": {"message": error_message, "type": "proxy_setup_error_max_retries", "code": error_code_str}})
|
| 474 |
yield "data: [DONE]\n\n"
|
| 475 |
+
return Response(error_stream_gen(), content_type='text/event-stream', status=500) # Status 500 as requested
|
| 476 |
else:
|
| 477 |
+
return jsonify({"error": error_message, "code": error_code_str}), 500 # Status 500 as requested
|
| 478 |
|
| 479 |
return with_valid_key_and_session(attempt_ondemand_request_wrapper)
|
| 480 |
|
|
|
|
| 498 |
logging.info(f"【流式请求-空回复重试 {empty_retry_attempt_num-1}】新Key/Session获取成功: Key={keymgr.display_key(current_apikey_for_attempt)}, Session={current_session_id_for_attempt}")
|
| 499 |
except (ValueError, requests.exceptions.RequestException) as e_key_session:
|
| 500 |
logging.warning(f"【流式请求-空回复重试 {empty_retry_attempt_num-1}】获取新Key/Session失败: {e_key_session}")
|
| 501 |
+
status_code_from_exc_retry_setup = None
|
| 502 |
+
if hasattr(e_key_session, 'response') and e_key_session.response is not None:
|
| 503 |
+
status_code_from_exc_retry_setup = e_key_session.response.status_code
|
| 504 |
+
|
| 505 |
if current_apikey_for_attempt and not isinstance(e_key_session, ValueError):
|
| 506 |
+
if status_code_from_exc_retry_setup == 524:
|
| 507 |
+
logging.info(f"【KeyManager】Key {keymgr.display_key(current_apikey_for_attempt)} not marked bad for 524 error during key/session acquisition for stream retry.")
|
|
|
|
|
|
|
|
|
|
| 508 |
else:
|
| 509 |
keymgr.mark_bad(current_apikey_for_attempt)
|
| 510 |
|
| 511 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 512 |
+
# Final failure to get key/session for the last empty response retry
|
| 513 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 514 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 515 |
+
logging.error(f"【流式请求】无法为最终空回复重试获取新Key/Session。错误: {e_key_session}")
|
| 516 |
+
yield format_openai_sse_delta({"error": {"message": final_error_message, "type": "proxy_final_retry_setup_failed", "code": final_error_code}})
|
| 517 |
yield "data: [DONE]\n\n"
|
| 518 |
return
|
| 519 |
time.sleep(1)
|
|
|
|
| 552 |
raise e_req
|
| 553 |
|
| 554 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 555 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 556 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 557 |
+
logging.error(f"【流式请求】在最后一次空回复重试时发生请求错误: {e_req}")
|
| 558 |
+
yield format_openai_sse_delta({"error": {"message": final_error_message, "type": "proxy_final_retry_request_failed", "code": final_error_code}})
|
| 559 |
yield "data: [DONE]\n\n"
|
| 560 |
return
|
| 561 |
time.sleep(1)
|
|
|
|
| 571 |
|
| 572 |
logging.warning(f"【流式请求】({log_attempt_str}) 返回空内容。")
|
| 573 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 574 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 575 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 576 |
+
logging.error(f"【流式请求】达到最大空回复重试次数 ({max_empty_response_retries})。将返回指定错误。")
|
| 577 |
yield format_openai_sse_delta({
|
| 578 |
+
"error": {"message": final_error_message, "type": "max_retries_exceeded_empty_response", "code": final_error_code}
|
| 579 |
})
|
| 580 |
yield "data: [DONE]\n\n"
|
| 581 |
return
|
|
|
|
| 583 |
logging.info(f"【流式请求】空回复,将在1秒后重试下一个Key。当前总尝试 {empty_retry_attempt_num}/{max_empty_response_retries}")
|
| 584 |
time.sleep(1)
|
| 585 |
|
| 586 |
+
# Fallback if loop finishes unexpectedly (shouldn't happen with current logic)
|
| 587 |
+
# This case means all max_empty_response_retries were exhausted, and the last one was also empty.
|
| 588 |
+
# The specific error for this is handled inside the loop. This is a safeguard.
|
| 589 |
+
final_fallback_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 590 |
+
final_fallback_error_code = "max_retries_check_context_contact_admin_fallback"
|
| 591 |
+
logging.error(f"【流式请求】意外退出空回复重试循环。返回最终错误。")
|
| 592 |
+
yield format_openai_sse_delta({"error": {"message": final_fallback_error_message, "type": "internal_proxy_error_unexpected_exit", "code": final_fallback_error_code}})
|
| 593 |
yield "data: [DONE]\n\n"
|
| 594 |
|
| 595 |
|
|
|
|
| 614 |
logging.info(f"【同步请求-空回复重试 {empty_retry_attempt_num-1}】新Key/Session获取成功: Key={keymgr.display_key(current_apikey_for_attempt)}, Session={current_session_id_for_attempt}")
|
| 615 |
except (ValueError, requests.exceptions.RequestException) as e_key_session:
|
| 616 |
logging.warning(f"【同步请求-空回复重试 {empty_retry_attempt_num-1}】获取新Key/Session失败: {e_key_session}")
|
| 617 |
+
status_code_from_exc_retry_setup_ns = None
|
| 618 |
+
if hasattr(e_key_session, 'response') and e_key_session.response is not None:
|
| 619 |
+
status_code_from_exc_retry_setup_ns = e_key_session.response.status_code
|
| 620 |
+
|
| 621 |
if current_apikey_for_attempt and not isinstance(e_key_session, ValueError):
|
| 622 |
+
if status_code_from_exc_retry_setup_ns == 524:
|
|
|
|
|
|
|
|
|
|
| 623 |
logging.info(f"【KeyManager】Key {keymgr.display_key(current_apikey_for_attempt)} not marked bad for 524 error during key/session acquisition for non-stream retry.")
|
| 624 |
else:
|
| 625 |
keymgr.mark_bad(current_apikey_for_attempt)
|
| 626 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 627 |
+
# Final failure to get key/session for the last empty response retry
|
| 628 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 629 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 630 |
+
logging.error(f"【同步请求】无法为最终空回复重试获取新Key/Session。错误: {e_key_session}")
|
| 631 |
+
return jsonify({"error": final_error_message, "code": final_error_code}), 500 # Status 500
|
| 632 |
time.sleep(1)
|
| 633 |
current_apikey_for_attempt = None
|
| 634 |
continue
|
|
|
|
| 646 |
response_json = resp.json()
|
| 647 |
if "data" not in response_json or "answer" not in response_json["data"]:
|
| 648 |
logging.error(f"【OnDemand同步错误】响应格式不符合预期 ({log_attempt_str})。Session: {current_session_id_for_attempt}, 响应: {str(response_json)[:500]}")
|
| 649 |
+
# This is an API format error, not an empty response.
|
| 650 |
+
# If this happens on the first attempt, it will be re-raised to with_valid_key_and_session
|
| 651 |
+
# If on a retry for empty response, it's a new kind of failure for that attempt.
|
| 652 |
+
raise ValueError(f"OnDemand API sync response missing 'data.answer' field on attempt {empty_retry_attempt_num}.")
|
| 653 |
|
| 654 |
ai_response_content = response_json["data"]["answer"]
|
| 655 |
if ai_response_content is None: ai_response_content = ""
|
|
|
|
| 666 |
else:
|
| 667 |
logging.warning(f"【同步请求】({log_attempt_str}) 返回空回复。")
|
| 668 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 669 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 670 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 671 |
+
logging.error(f"【同步请求】达到最大空回复重试次数 ({max_empty_response_retries})。将返回指定错误。")
|
| 672 |
return jsonify({
|
| 673 |
+
"error": final_error_message,
|
| 674 |
"id": "chatcmpl-" + str(uuid.uuid4())[:12], "object": "chat.completion", "created": int(time.time()),
|
| 675 |
"model": openai_model_name_for_response,
|
| 676 |
+
"choices": [{"index": 0, "message": {"role": "assistant", "content": ""}, "finish_reason": "length"}],
|
| 677 |
"usage": {},
|
| 678 |
+
"code": final_error_code
|
| 679 |
+
}), 500 # Status 500 as requested
|
| 680 |
logging.info(f"【同步请求】空回复,将在1秒后重试下一个Key。当前总尝试 {empty_retry_attempt_num}/{max_empty_response_retries}")
|
| 681 |
time.sleep(1)
|
| 682 |
|
|
|
|
| 697 |
if empty_retry_attempt_num == 1:
|
| 698 |
raise e_req
|
| 699 |
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 700 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 701 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 702 |
+
logging.error(f"【同步请求】在最后一次空回复重试时发生请求错误: {e_req}")
|
| 703 |
+
return jsonify({"error": final_error_message, "code":final_error_code, "details": str(e_req)}), 500 # Status 500
|
| 704 |
time.sleep(1)
|
| 705 |
continue
|
| 706 |
except (ValueError, KeyError, json.JSONDecodeError) as e_parse:
|
| 707 |
+
# This catches the ValueError from "data.answer" missing, or JSON decode errors
|
| 708 |
+
logging.error(f"【同步请求】({log_attempt_str}) 处理响应或格式时出错: {e_parse}", exc_info=True)
|
| 709 |
+
if empty_retry_attempt_num == 1:
|
| 710 |
+
# If format error on first attempt, re-raise to be caught by with_valid_key_and_session
|
| 711 |
+
# This implies a more fundamental issue than just an empty response.
|
| 712 |
+
raise requests.exceptions.RequestException(f"Response format error on first attempt: {e_parse}") from e_parse
|
| 713 |
+
|
| 714 |
+
# If it's a format error during an empty-response retry, it's problematic.
|
| 715 |
+
if empty_retry_attempt_num >= max_empty_response_retries:
|
| 716 |
+
final_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 717 |
+
final_error_code = "max_retries_check_context_contact_admin"
|
| 718 |
+
logging.error(f"【同步请求】在最后一次空回复重试时发生响应解析错误: {e_parse}")
|
| 719 |
+
return jsonify({"error": final_error_message, "code": final_error_code, "details": f"Parse error: {str(e_parse)}"}), 500 # Status 500
|
| 720 |
+
time.sleep(1)
|
| 721 |
+
continue # Try next key for empty response retry
|
| 722 |
|
| 723 |
+
# Fallback if loop finishes unexpectedly (e.g. all retries were empty and the last one didn't hit the specific return)
|
| 724 |
+
final_fallback_error_message = "重试次数过多,请检查上下文长度! 或联系管理员!"
|
| 725 |
+
final_fallback_error_code = "max_retries_check_context_contact_admin_fallback"
|
| 726 |
+
logging.error(f"【同步请求】意外退出空回复重试循环。返回最终错误。")
|
| 727 |
+
return jsonify({"error": final_fallback_error_message, "code": final_fallback_error_code}), 500 # Status 500
|
| 728 |
|
| 729 |
|
| 730 |
@app.route("/v1/models", methods=["GET"])
|