mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-06-01 22:09:04 +02:00
Add no-thinking model picker variants
This commit is contained in:
@@ -305,6 +305,8 @@ Claude Code 2.1.126 or later reads this proxy's `/v1/models` endpoint when `ANTH
|
|||||||
|
|
||||||
The proxy lists models for configured provider keys and referenced local providers. Picker-safe IDs are routed back to the real provider/model automatically, so no `.env` edit or separate launcher script is needed after startup.
|
The proxy lists models for configured provider keys and referenced local providers. Picker-safe IDs are routed back to the real provider/model automatically, so no `.env` edit or separate launcher script is needed after startup.
|
||||||
|
|
||||||
|
Each provider model also has a `(no thinking)` picker variant. Use it when a model does not support Claude Code thinking or fails with adaptive-thinking requests. It routes to the same upstream model while asking Claude Code to send a non-thinking request.
|
||||||
|
|
||||||
## Optional Integrations
|
## Optional Integrations
|
||||||
|
|
||||||
### Discord And Telegram Bots
|
### Discord And Telegram Bots
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Gateway-safe model id encoding for Claude Code model discovery."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
GATEWAY_MODEL_ID_PREFIX = "anthropic"
|
||||||
|
|
||||||
|
# Claude Code currently treats any model id containing ``claude-3-`` as not
|
||||||
|
# supporting thinking. This intentionally uses that client-side capability
|
||||||
|
# heuristic while keeping the real provider/model ref reversible for routing.
|
||||||
|
NO_THINKING_GATEWAY_MODEL_ID_PREFIX = "claude-3-freecc-no-thinking"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DecodedGatewayModelId:
|
||||||
|
provider_id: str
|
||||||
|
provider_model: str
|
||||||
|
force_thinking_enabled: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def gateway_model_id(provider_model_ref: str) -> str:
|
||||||
|
"""Return the normal Claude Code-discoverable id for a provider/model ref."""
|
||||||
|
return f"{GATEWAY_MODEL_ID_PREFIX}/{provider_model_ref}"
|
||||||
|
|
||||||
|
|
||||||
|
def no_thinking_gateway_model_id(provider_model_ref: str) -> str:
|
||||||
|
"""Return a Claude Code-discoverable id that disables client thinking."""
|
||||||
|
return f"{NO_THINKING_GATEWAY_MODEL_ID_PREFIX}/{provider_model_ref}"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_gateway_model_id(model_name: str) -> DecodedGatewayModelId | None:
|
||||||
|
"""Decode a model id advertised by this gateway, if it is one."""
|
||||||
|
prefix, separator, remainder = model_name.partition("/")
|
||||||
|
if not separator:
|
||||||
|
return None
|
||||||
|
|
||||||
|
force_thinking_enabled: bool | None
|
||||||
|
if prefix == GATEWAY_MODEL_ID_PREFIX:
|
||||||
|
force_thinking_enabled = None
|
||||||
|
elif prefix == NO_THINKING_GATEWAY_MODEL_ID_PREFIX:
|
||||||
|
force_thinking_enabled = False
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider_id, provider_separator, provider_model = remainder.partition("/")
|
||||||
|
if not provider_separator or not provider_model:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return DecodedGatewayModelId(
|
||||||
|
provider_id=provider_id,
|
||||||
|
provider_model=provider_model,
|
||||||
|
force_thinking_enabled=force_thinking_enabled,
|
||||||
|
)
|
||||||
+29
-25
@@ -9,10 +9,9 @@ from loguru import logger
|
|||||||
from config.provider_ids import SUPPORTED_PROVIDER_IDS
|
from config.provider_ids import SUPPORTED_PROVIDER_IDS
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
|
||||||
|
from .gateway_model_ids import decode_gateway_model_id
|
||||||
from .models.anthropic import MessagesRequest, TokenCountRequest
|
from .models.anthropic import MessagesRequest, TokenCountRequest
|
||||||
|
|
||||||
GATEWAY_MODEL_ID_PREFIX = "anthropic"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class ResolvedModel:
|
class ResolvedModel:
|
||||||
@@ -42,16 +41,23 @@ class ModelRouter:
|
|||||||
self._settings = settings
|
self._settings = settings
|
||||||
|
|
||||||
def resolve(self, claude_model_name: str) -> ResolvedModel:
|
def resolve(self, claude_model_name: str) -> ResolvedModel:
|
||||||
direct_provider_id, direct_provider_model = self._direct_provider_model(
|
(
|
||||||
claude_model_name
|
direct_provider_id,
|
||||||
)
|
direct_provider_model,
|
||||||
|
force_thinking_enabled,
|
||||||
|
) = self._direct_provider_model(claude_model_name)
|
||||||
if direct_provider_id is not None and direct_provider_model is not None:
|
if direct_provider_id is not None and direct_provider_model is not None:
|
||||||
thinking_enabled = self._settings.resolve_thinking(direct_provider_model)
|
thinking_enabled = (
|
||||||
|
force_thinking_enabled
|
||||||
|
if force_thinking_enabled is not None
|
||||||
|
else self._settings.resolve_thinking(direct_provider_model)
|
||||||
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"MODEL DIRECT: '{}' -> provider='{}' model='{}'",
|
"MODEL DIRECT: '{}' -> provider='{}' model='{}' thinking={}",
|
||||||
claude_model_name,
|
claude_model_name,
|
||||||
direct_provider_id,
|
direct_provider_id,
|
||||||
direct_provider_model,
|
direct_provider_model,
|
||||||
|
thinking_enabled,
|
||||||
)
|
)
|
||||||
return ResolvedModel(
|
return ResolvedModel(
|
||||||
original_model=claude_model_name,
|
original_model=claude_model_name,
|
||||||
@@ -77,29 +83,27 @@ class ModelRouter:
|
|||||||
thinking_enabled=thinking_enabled,
|
thinking_enabled=thinking_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _direct_provider_model(self, model_name: str) -> tuple[str | None, str | None]:
|
def _direct_provider_model(
|
||||||
provider_id, separator, provider_model = model_name.partition("/")
|
|
||||||
if not separator:
|
|
||||||
return None, None
|
|
||||||
if provider_id == GATEWAY_MODEL_ID_PREFIX:
|
|
||||||
return self._gateway_encoded_provider_model(provider_model)
|
|
||||||
if provider_id not in SUPPORTED_PROVIDER_IDS:
|
|
||||||
return None, None
|
|
||||||
if not provider_model:
|
|
||||||
return None, None
|
|
||||||
return provider_id, provider_model
|
|
||||||
|
|
||||||
def _gateway_encoded_provider_model(
|
|
||||||
self, model_name: str
|
self, model_name: str
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[str | None, str | None, bool | None]:
|
||||||
|
decoded = decode_gateway_model_id(model_name)
|
||||||
|
if decoded is not None:
|
||||||
|
if decoded.provider_id not in SUPPORTED_PROVIDER_IDS:
|
||||||
|
return None, None, None
|
||||||
|
return (
|
||||||
|
decoded.provider_id,
|
||||||
|
decoded.provider_model,
|
||||||
|
decoded.force_thinking_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
provider_id, separator, provider_model = model_name.partition("/")
|
provider_id, separator, provider_model = model_name.partition("/")
|
||||||
if not separator:
|
if not separator:
|
||||||
return None, None
|
return None, None, None
|
||||||
if provider_id not in SUPPORTED_PROVIDER_IDS:
|
if provider_id not in SUPPORTED_PROVIDER_IDS:
|
||||||
return None, None
|
return None, None, None
|
||||||
if not provider_model:
|
if not provider_model:
|
||||||
return None, None
|
return None, None, None
|
||||||
return provider_id, provider_model
|
return provider_id, provider_model, None
|
||||||
|
|
||||||
def resolve_messages_request(
|
def resolve_messages_request(
|
||||||
self, request: MessagesRequest
|
self, request: MessagesRequest
|
||||||
|
|||||||
+24
-19
@@ -9,6 +9,7 @@ from providers.registry import ProviderRegistry
|
|||||||
|
|
||||||
from . import dependencies
|
from . import dependencies
|
||||||
from .dependencies import get_settings, require_api_key
|
from .dependencies import get_settings, require_api_key
|
||||||
|
from .gateway_model_ids import gateway_model_id, no_thinking_gateway_model_id
|
||||||
from .models.anthropic import MessagesRequest, TokenCountRequest
|
from .models.anthropic import MessagesRequest, TokenCountRequest
|
||||||
from .models.responses import ModelResponse, ModelsListResponse
|
from .models.responses import ModelResponse, ModelsListResponse
|
||||||
from .services import ClaudeProxyService
|
from .services import ClaudeProxyService
|
||||||
@@ -16,7 +17,6 @@ from .services import ClaudeProxyService
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
DISCOVERED_MODEL_CREATED_AT = "1970-01-01T00:00:00Z"
|
DISCOVERED_MODEL_CREATED_AT = "1970-01-01T00:00:00Z"
|
||||||
GATEWAY_MODEL_ID_PREFIX = "anthropic"
|
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_CLAUDE_MODELS = [
|
SUPPORTED_CLAUDE_MODELS = [
|
||||||
@@ -77,10 +77,6 @@ def _probe_response(allow: str) -> Response:
|
|||||||
return Response(status_code=204, headers={"Allow": allow})
|
return Response(status_code=204, headers={"Allow": allow})
|
||||||
|
|
||||||
|
|
||||||
def _gateway_model_id(provider_model_ref: str) -> str:
|
|
||||||
return f"{GATEWAY_MODEL_ID_PREFIX}/{provider_model_ref}"
|
|
||||||
|
|
||||||
|
|
||||||
def _discovered_model_response(model_id: str, *, display_name: str) -> ModelResponse:
|
def _discovered_model_response(model_id: str, *, display_name: str) -> ModelResponse:
|
||||||
return ModelResponse(
|
return ModelResponse(
|
||||||
id=model_id,
|
id=model_id,
|
||||||
@@ -98,6 +94,27 @@ def _append_unique_model(
|
|||||||
models.append(model)
|
models.append(model)
|
||||||
|
|
||||||
|
|
||||||
|
def _append_provider_model_variants(
|
||||||
|
models: list[ModelResponse], seen: set[str], provider_model_ref: str
|
||||||
|
) -> None:
|
||||||
|
_append_unique_model(
|
||||||
|
models,
|
||||||
|
seen,
|
||||||
|
_discovered_model_response(
|
||||||
|
gateway_model_id(provider_model_ref),
|
||||||
|
display_name=provider_model_ref,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_append_unique_model(
|
||||||
|
models,
|
||||||
|
seen,
|
||||||
|
_discovered_model_response(
|
||||||
|
no_thinking_gateway_model_id(provider_model_ref),
|
||||||
|
display_name=f"{provider_model_ref} (no thinking)",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_models_list_response(
|
def _build_models_list_response(
|
||||||
settings: Settings, provider_registry: ProviderRegistry | None
|
settings: Settings, provider_registry: ProviderRegistry | None
|
||||||
) -> ModelsListResponse:
|
) -> ModelsListResponse:
|
||||||
@@ -105,23 +122,11 @@ def _build_models_list_response(
|
|||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
|
|
||||||
for ref in settings.configured_chat_model_refs():
|
for ref in settings.configured_chat_model_refs():
|
||||||
_append_unique_model(
|
_append_provider_model_variants(models, seen, ref.model_ref)
|
||||||
models,
|
|
||||||
seen,
|
|
||||||
_discovered_model_response(
|
|
||||||
_gateway_model_id(ref.model_ref), display_name=ref.model_ref
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if provider_registry is not None:
|
if provider_registry is not None:
|
||||||
for model_ref in provider_registry.cached_prefixed_model_refs():
|
for model_ref in provider_registry.cached_prefixed_model_refs():
|
||||||
_append_unique_model(
|
_append_provider_model_variants(models, seen, model_ref)
|
||||||
models,
|
|
||||||
seen,
|
|
||||||
_discovered_model_response(
|
|
||||||
_gateway_model_id(model_ref), display_name=model_ref
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for model in SUPPORTED_CLAUDE_MODELS:
|
for model in SUPPORTED_CLAUDE_MODELS:
|
||||||
_append_unique_model(models, seen, model)
|
_append_unique_model(models, seen, model)
|
||||||
|
|||||||
@@ -34,18 +34,29 @@ def test_models_list_includes_configured_refs_cached_provider_models_and_aliases
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
ids = [item["id"] for item in data["data"]]
|
ids = [item["id"] for item in data["data"]]
|
||||||
|
|
||||||
assert ids[:3] == [
|
assert ids[:6] == [
|
||||||
"anthropic/deepseek/deepseek-chat",
|
"anthropic/deepseek/deepseek-chat",
|
||||||
|
"claude-3-freecc-no-thinking/deepseek/deepseek-chat",
|
||||||
"anthropic/open_router/anthropic/claude-opus",
|
"anthropic/open_router/anthropic/claude-opus",
|
||||||
|
"claude-3-freecc-no-thinking/open_router/anthropic/claude-opus",
|
||||||
"anthropic/open_router/meta/llama-3.3",
|
"anthropic/open_router/meta/llama-3.3",
|
||||||
|
"claude-3-freecc-no-thinking/open_router/meta/llama-3.3",
|
||||||
]
|
]
|
||||||
assert ids.count("anthropic/deepseek/deepseek-chat") == 1
|
assert ids.count("anthropic/deepseek/deepseek-chat") == 1
|
||||||
|
assert ids.count("claude-3-freecc-no-thinking/deepseek/deepseek-chat") == 1
|
||||||
assert ids.count("anthropic/open_router/anthropic/claude-opus") == 1
|
assert ids.count("anthropic/open_router/anthropic/claude-opus") == 1
|
||||||
|
assert (
|
||||||
|
ids.count("claude-3-freecc-no-thinking/open_router/anthropic/claude-opus") == 1
|
||||||
|
)
|
||||||
display_names = {item["id"]: item["display_name"] for item in data["data"]}
|
display_names = {item["id"]: item["display_name"] for item in data["data"]}
|
||||||
assert (
|
assert (
|
||||||
display_names["anthropic/open_router/meta/llama-3.3"]
|
display_names["anthropic/open_router/meta/llama-3.3"]
|
||||||
== "open_router/meta/llama-3.3"
|
== "open_router/meta/llama-3.3"
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
display_names["claude-3-freecc-no-thinking/open_router/meta/llama-3.3"]
|
||||||
|
== "open_router/meta/llama-3.3 (no thinking)"
|
||||||
|
)
|
||||||
assert "claude-sonnet-4-20250514" in ids
|
assert "claude-sonnet-4-20250514" in ids
|
||||||
assert data["first_id"] == ids[0]
|
assert data["first_id"] == ids[0]
|
||||||
assert data["last_id"] == ids[-1]
|
assert data["last_id"] == ids[-1]
|
||||||
@@ -64,8 +75,10 @@ def test_models_list_works_without_provider_registry():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
ids = [item["id"] for item in response.json()["data"]]
|
ids = [item["id"] for item in response.json()["data"]]
|
||||||
assert ids[:2] == [
|
assert ids[:4] == [
|
||||||
"anthropic/deepseek/deepseek-chat",
|
"anthropic/deepseek/deepseek-chat",
|
||||||
|
"claude-3-freecc-no-thinking/deepseek/deepseek-chat",
|
||||||
"anthropic/open_router/anthropic/claude-opus",
|
"anthropic/open_router/anthropic/claude-opus",
|
||||||
|
"claude-3-freecc-no-thinking/open_router/anthropic/claude-opus",
|
||||||
]
|
]
|
||||||
assert "claude-sonnet-4-20250514" in ids
|
assert "claude-sonnet-4-20250514" in ids
|
||||||
|
|||||||
@@ -131,6 +131,27 @@ def test_model_router_routes_gateway_encoded_provider_model_directly(settings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_router_routes_no_thinking_gateway_model_directly(settings):
|
||||||
|
settings.enable_model_thinking = True
|
||||||
|
|
||||||
|
routed = ModelRouter(settings).resolve_messages_request(
|
||||||
|
MessagesRequest(
|
||||||
|
model="claude-3-freecc-no-thinking/nvidia_nim/deepseek-ai/deepseek-v4-pro",
|
||||||
|
max_tokens=100,
|
||||||
|
messages=[Message(role="user", content="hello")],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert routed.request.model == "deepseek-ai/deepseek-v4-pro"
|
||||||
|
assert (
|
||||||
|
routed.resolved.original_model
|
||||||
|
== "claude-3-freecc-no-thinking/nvidia_nim/deepseek-ai/deepseek-v4-pro"
|
||||||
|
)
|
||||||
|
assert routed.resolved.provider_id == "nvidia_nim"
|
||||||
|
assert routed.resolved.provider_model == "deepseek-ai/deepseek-v4-pro"
|
||||||
|
assert routed.resolved.thinking_enabled is False
|
||||||
|
|
||||||
|
|
||||||
def test_model_router_direct_prefixed_model_uses_provider_model_for_thinking(settings):
|
def test_model_router_direct_prefixed_model_uses_provider_model_for_thinking(settings):
|
||||||
settings.enable_model_thinking = False
|
settings.enable_model_thinking = False
|
||||||
settings.enable_opus_thinking = True
|
settings.enable_opus_thinking = True
|
||||||
|
|||||||
Reference in New Issue
Block a user