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 <noreply@anthropic.com>
This commit is contained in:
Daniel JB Clark 2026-10-03 06:55:00 -04:00
parent b9d159fcc9
commit bf18f22f9c
No known key found for this signature in database
4 changed files with 32 additions and 35 deletions

View file

@ -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) | ✅ | ✅ | ✅ | | | | | | | |

View file

@ -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,

View file

@ -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

View file

@ -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<string, string> = {
Cerebras: "cerebras",
CHATGPT: "chatgpt",
CLARIFAI: "clarifai",
CLINEPASS: "clinepass",
CLOUDFLARE: "cloudflare",
CODESTRAL: "codestral",
Cognition: "cognition",