|
4 | 4 | # src/rotator_library/providers/iflow_provider.py |
5 | 5 |
|
6 | 6 | import copy |
| 7 | +import hmac |
7 | 8 | import hashlib |
8 | 9 | import json |
9 | 10 | import time |
|
69 | 70 | "response_format", |
70 | 71 | } |
71 | 72 |
|
| 73 | +IFLOW_USER_AGENT = "iFlow-Cli" |
| 74 | +IFLOW_HEADER_SESSION_ID = "session-id" |
| 75 | +IFLOW_HEADER_TIMESTAMP = "x-iflow-timestamp" |
| 76 | +IFLOW_HEADER_SIGNATURE = "x-iflow-signature" |
| 77 | + |
72 | 78 | # ============================================================================= |
73 | 79 | # THINKING MODE CONFIGURATION |
74 | 80 | # ============================================================================= |
@@ -512,6 +518,39 @@ def _build_request_payload( |
512 | 518 |
|
513 | 519 | return payload |
514 | 520 |
|
| 521 | + def _create_iflow_signature( |
| 522 | + self, user_agent: str, session_id: str, timestamp_ms: int, api_key: str |
| 523 | + ) -> str: |
| 524 | + """Generate iFlow HMAC-SHA256 signature: userAgent:sessionId:timestamp.""" |
| 525 | + if not api_key: |
| 526 | + return "" |
| 527 | + |
| 528 | + payload = f"{user_agent}:{session_id}:{timestamp_ms}" |
| 529 | + return hmac.new( |
| 530 | + api_key.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256 |
| 531 | + ).hexdigest() |
| 532 | + |
| 533 | + def _build_iflow_headers(self, api_key: str, stream: bool) -> Dict[str, str]: |
| 534 | + """Build iFlow request headers, including signed auth headers.""" |
| 535 | + session_id = f"session-{uuid.uuid4()}" |
| 536 | + timestamp_ms = int(time.time() * 1000) |
| 537 | + signature = self._create_iflow_signature( |
| 538 | + IFLOW_USER_AGENT, session_id, timestamp_ms, api_key |
| 539 | + ) |
| 540 | + |
| 541 | + headers = { |
| 542 | + "Authorization": f"Bearer {api_key}", |
| 543 | + "Content-Type": "application/json", |
| 544 | + "User-Agent": IFLOW_USER_AGENT, |
| 545 | + IFLOW_HEADER_SESSION_ID: session_id, |
| 546 | + IFLOW_HEADER_TIMESTAMP: str(timestamp_ms), |
| 547 | + "Accept": "text/event-stream" if stream else "application/json", |
| 548 | + } |
| 549 | + if signature: |
| 550 | + headers[IFLOW_HEADER_SIGNATURE] = signature |
| 551 | + |
| 552 | + return headers |
| 553 | + |
515 | 554 | def _extract_finish_reason_from_chunk(self, chunk: Dict[str, Any]) -> Optional[str]: |
516 | 555 | """ |
517 | 556 | Extract finish_reason from a raw iFlow chunk by searching all possible locations. |
@@ -922,12 +961,10 @@ async def make_request(): |
922 | 961 | model_name, kwargs, **kwargs_with_stripped_model |
923 | 962 | ) |
924 | 963 |
|
925 | | - headers = { |
926 | | - "Authorization": f"Bearer {api_key}", # Uses api_key from user info |
927 | | - "Content-Type": "application/json", |
928 | | - "Accept": "text/event-stream", |
929 | | - "User-Agent": "iFlow-Cli", |
930 | | - } |
| 964 | + headers = self._build_iflow_headers( |
| 965 | + api_key=api_key, |
| 966 | + stream=bool(payload.get("stream")), |
| 967 | + ) |
931 | 968 |
|
932 | 969 | url = f"{api_base.rstrip('/')}/chat/completions" |
933 | 970 |
|
@@ -983,6 +1020,11 @@ async def stream_handler(response_stream, attempt=1): |
983 | 1020 |
|
984 | 1021 | # Handle other errors |
985 | 1022 | else: |
| 1023 | + if not error_text: |
| 1024 | + content_type = response.headers.get("content-type", "") |
| 1025 | + error_text = ( |
| 1026 | + f"(empty response body, content-type={content_type})" |
| 1027 | + ) |
986 | 1028 | error_msg = ( |
987 | 1029 | f"iFlow HTTP {response.status_code} error: {error_text}" |
988 | 1030 | ) |
|
0 commit comments