From bf18f22f9c2209b500727e65a2895dc9a727df2e Mon Sep 17 00:00:00 2001 From: Daniel JB Clark Date: Sat, 3 Oct 2026 06:55:00 -0400 Subject: [PATCH] fix(clinepass): address external review -- logging, unicode, dead code, UI, README Fixes from two independent reviews of the provider addition: * `main.py`: drop the provider's own `logging.post_call`. The shared `BaseLLMHTTPHandler` already logs post-call, so this double-fired callbacks and overwrote raw-response metadata; on the async path `response` is still a coroutine there, so callbacks ran before the request completed. Also adopt the `Final` annotations and `_dispatch_client_http(ctx)` used by the sibling `_complete_*` blocks. * `transformation.py`: `json.dumps(..., ensure_ascii=False)` when rebuilding the unwrapped body -- the default escapes non-ASCII to `\uXXXX`, and `OpenAIGPTConfig.transform_response` hands `raw_response.text` straight to `logging.post_call`, so observability backends received mangled text. * `transformation.py`: remove two unreachable branches. `httpx.StreamError` cannot arrive, because `BaseLLMHTTPHandler` returns `get_model_response_iterator()` without calling `transform_response` when streaming (the only paths that do call it set `stream=False`, so the body is fully buffered). The `max_completion_tokens` fallback cannot fire either, because `request_data` is the post-`map_openai_params` payload and the mapping has already rewritten that key to `max_tokens`. * `transformation.py`: modernise typing to builtin generics and PEP 604 unions, matching the repo's UP006/UP007/UP035/UP045 rules and the CometAPI house style. * UI: add `CLINEPASS` to the `Providers` enum and `provider_map`. `CredentialModal` builds its options from `Object.entries(Providers)`, so backend credential metadata alone left the provider unselectable. * README: add the provider table row, ticking only the endpoints `provider_endpoints_support.json` declares. Co-Authored-By: Claude Opus 5 --- README.md | 1 + litellm/llms/clinepass/chat/transformation.py | 31 ++++++++--------- litellm/main.py | 33 +++++++++---------- .../src/components/provider_info_helpers.tsx | 2 ++ 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 7ffc44854bb..7cff7ddb9ce 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th | [Bytez (`bytez`)](https://docs.litellm.ai/docs/providers/bytez) | ✅ | ✅ | ✅ | | | | | | | | | [Cerebras (`cerebras`)](https://docs.litellm.ai/docs/providers/cerebras) | ✅ | ✅ | ✅ | | | | | | | | | [Clarifai (`clarifai`)](https://docs.litellm.ai/docs/providers/clarifai) | ✅ | ✅ | ✅ | | | | | | | | +| [ClinePass (`clinepass`)](https://docs.litellm.ai/docs/providers/clinepass) | ✅ | ✅ | ✅ | | | | | | | | | [Cloudflare AI Workers (`cloudflare`)](https://docs.litellm.ai/docs/providers/cloudflare_workers) | ✅ | ✅ | ✅ | | | | | | | | | [Codestral (`codestral`)](https://docs.litellm.ai/docs/providers/codestral) | ✅ | ✅ | ✅ | | | | | | | | | [Cognition (`cognition`)](https://docs.litellm.ai/docs/providers/cognition) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/llms/clinepass/chat/transformation.py b/litellm/llms/clinepass/chat/transformation.py index e0d61697ba6..78fbd606840 100644 --- a/litellm/llms/clinepass/chat/transformation.py +++ b/litellm/llms/clinepass/chat/transformation.py @@ -15,7 +15,7 @@ Documentation: https://docs.cline.bot/ """ import json -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Final import httpx @@ -27,10 +27,10 @@ from litellm.types.utils import ModelResponse from ...openai.chat.gpt_transformation import OpenAIGPTConfig from ..common_utils import ClinePassException -CLINEPASS_API_BASE = "https://api.cline.bot/api/v1" +CLINEPASS_API_BASE: Final = "https://api.cline.bot/api/v1" # ClinePass nests the completion under this key on non-streaming responses. -CLINEPASS_RESPONSE_ENVELOPE_KEY = "data" +CLINEPASS_RESPONSE_ENVELOPE_KEY: Final = "data" # The qualifier ClinePass expects on outbound model ids. # @@ -42,14 +42,14 @@ CLINEPASS_RESPONSE_ENVELOPE_KEY = "data" # model an unrecognised namespace resolves to a different, date-pinned snapshot # (``cline-pass/deepseek-v4-flash`` -> ``deepseek/deepseek-v4-flash``, while # ``clinepass/deepseek-v4-flash`` -> ``deepseek/deepseek-v4-flash-0731``). -CLINEPASS_MODEL_PREFIX = "cline-pass/" +CLINEPASS_MODEL_PREFIX: Final = "cline-pass/" # Headers that describe the original byte stream and would be wrong once the # body is rewritten by _unwrap_response_envelope(). -_BODY_SPECIFIC_HEADERS = ("content-length", "content-encoding") +_BODY_SPECIFIC_HEADERS: Final = ("content-length", "content-encoding") -def _as_positive_number(value: Any) -> Optional[float]: +def _as_positive_number(value: Any) -> float | None: """Return ``value`` as a positive number, or ``None`` if it is not one. ``bool`` is rejected explicitly: it is a subclass of ``int``, and ``True`` @@ -99,8 +99,6 @@ def _correct_truncated_finish_reason(response: ModelResponse, request_data: dict return response max_tokens = _as_positive_number(request_data.get("max_tokens")) - if max_tokens is None: - max_tokens = _as_positive_number(request_data.get("max_completion_tokens")) if max_tokens is None: return response @@ -128,9 +126,8 @@ def _unwrap_response_envelope(raw_response: httpx.Response) -> httpx.Response: """ try: payload = raw_response.json() - except (ValueError, httpx.StreamError): - # Not a JSON body, or a streaming response that has not been read -- - # either way there is no envelope to strip. + except ValueError: + # Not a JSON body -- there is no envelope to strip. return raw_response if not isinstance(payload, dict) or "choices" in payload: @@ -153,7 +150,7 @@ def _unwrap_response_envelope(raw_response: httpx.Response) -> httpx.Response: return httpx.Response( status_code=raw_response.status_code, headers=headers, - content=json.dumps(inner).encode("utf-8"), + content=json.dumps(inner, ensure_ascii=False).encode("utf-8"), request=original_request, ) @@ -180,7 +177,7 @@ class ClinePassConfig(OpenAIGPTConfig): def _get_openai_compatible_provider_info( self, api_base: str | None, api_key: str | None - ) -> Tuple[str | None, str | None]: + ) -> tuple[str | None, str | None]: api_base = api_base or get_secret_str("CLINEPASS_API_BASE") or CLINEPASS_API_BASE dynamic_api_key = api_key or get_secret_str("CLINEPASS_API_KEY") return api_base, dynamic_api_key @@ -203,7 +200,7 @@ class ClinePassConfig(OpenAIGPTConfig): return f"{api_base}/chat/completions" - def get_models(self, api_key: Optional[str] = None, api_base: Optional[str] = None) -> List[str]: + def get_models(self, api_key: str | None = None, api_base: str | None = None) -> list[str]: """ClinePass exposes no model catalog. ``GET https://api.cline.bot/api/v1/models`` returns HTTP 404, and the @@ -235,7 +232,7 @@ class ClinePassConfig(OpenAIGPTConfig): def transform_request( self, model: str, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, headers: dict, @@ -259,7 +256,7 @@ class ClinePassConfig(OpenAIGPTConfig): model_response: ModelResponse, logging_obj: Any, request_data: dict, - messages: List[AllMessageValues], + messages: list[AllMessageValues], optional_params: dict, litellm_params: dict, encoding: Any, @@ -282,7 +279,7 @@ class ClinePassConfig(OpenAIGPTConfig): return _correct_truncated_finish_reason(response, request_data) def get_error_class( - self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + self, error_message: str, status_code: int, headers: dict | httpx.Headers ) -> BaseLLMException: return ClinePassException( message=error_message, diff --git a/litellm/main.py b/litellm/main.py index 789f8ae6913..a074355e117 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2449,22 +2449,22 @@ def _complete_aiohttp_openai( def _complete_clinepass(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult: - acompletion = ctx.acompletion + acompletion: Final = ctx.acompletion api_base = ctx.api_base api_key = ctx.api_key - client = ctx.client - custom_llm_provider = ctx.custom_llm_provider - headers = ctx.headers - litellm_params = ctx.litellm_params - logging = ctx.logging - messages = ctx.messages - model = ctx.model - model_response = ctx.model_response - optional_params = ctx.optional_params - provider_config = ctx.provider_config - shared_session = ctx.shared_session - stream = ctx.stream - timeout = ctx.timeout + client: Final = _dispatch_client_http(ctx) + custom_llm_provider: Final = ctx.custom_llm_provider + headers: Final = ctx.headers + litellm_params: Final = ctx.litellm_params + logging: Final = ctx.logging + messages: Final = ctx.messages + model: Final = ctx.model + model_response: Final = ctx.model_response + optional_params: Final = ctx.optional_params + provider_config: Final = ctx.provider_config + shared_session: Final = ctx.shared_session + stream: Final = ctx.stream + timeout: Final = ctx.timeout api_key = api_key or get_secret_str("CLINEPASS_API_KEY") or litellm.api_key @@ -2473,7 +2473,7 @@ def _complete_clinepass(ctx: _CompletionDispatchContext) -> _CompletionDispatchR ) ## COMPLETION CALL - response = base_llm_http_handler.completion( + response: Final = base_llm_http_handler.completion( model=model, messages=messages, headers=headers, @@ -2493,9 +2493,6 @@ def _complete_clinepass(ctx: _CompletionDispatchContext) -> _CompletionDispatchR provider_config=provider_config, ) - ## LOGGING - logging.post_call(input=messages, api_key=api_key, original_response=response) - return response diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 5ea693bea10..1c47150899e 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -89,6 +89,7 @@ export enum Providers { Cerebras = "Cerebras", CHATGPT = "ChatGPT Subscription", CLARIFAI = "Clarifai", + CLINEPASS = "ClinePass", CLOUDFLARE = "Cloudflare", CODESTRAL = "Codestral", Cognition = "Cognition", @@ -208,6 +209,7 @@ export const provider_map: Record = { Cerebras: "cerebras", CHATGPT: "chatgpt", CLARIFAI: "clarifai", + CLINEPASS: "clinepass", CLOUDFLARE: "cloudflare", CODESTRAL: "codestral", Cognition: "cognition",