Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4310,13 +4310,9 @@ def calculate_and_show_tokens_and_cost(self, messages, completion=None):
cache_write_tokens = getattr(completion.usage, "cache_creation_input_tokens", 0) or 0
self.message_cached_tokens += cache_hit_tokens

if hasattr(completion.usage, "cache_read_input_tokens") or hasattr(
completion.usage, "cache_creation_input_tokens"
):
self.message_tokens_sent += prompt_tokens
self.message_tokens_sent += cache_write_tokens
else:
self.message_tokens_sent += prompt_tokens
# ``prompt_tokens`` is normalized to the full input (including any
# cache read/write) for anthropic usage, so no separate add here.
self.message_tokens_sent += prompt_tokens

else:
prompt_tokens = self.get_active_model().token_count(messages)
Expand Down
1 change: 1 addition & 0 deletions cecli/helpers/conversation/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ def get_messages_dict(
and coder.add_cache_headers
and hasattr(coder, "main_model")
and not coder.main_model.caches_by_default
and not getattr(coder.main_model, "uses_messages_api", False)
):
messages_dict = self._add_cache_control(messages_dict)
return messages_dict
Expand Down
56 changes: 53 additions & 3 deletions cecli/helpers/llms/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
"github_copilot": {"api_base": "https://api.githubcopilot.com", "api_key_env": None},
"meta": {"api_base": "https://api.meta.ai/v1", "api_key_env": "META_API_KEY"},
"chutes": {"api_base": "https://llm.chutes.ai/v1/", "api_key_env": "CHUTES_API_KEY"},
"opencode-go": {
"api_base": "https://opencode.ai/zen/go/v1",
"api_key_env": "OPENCODE_GO_API_KEY",
},
"opencode-zen": {
"api_base": "https://opencode.ai/zen/zen/v1",
"api_key_env": "OPENCODE_ZEN_API_KEY",
},
}


Expand All @@ -40,12 +48,39 @@ def resolve_model_config(model: str) -> Dict[str, Any]:
cfg = get_default_config(model)
llm_block = cfg.get("llm") or {}
api_block = cfg.get("api") or {}
provider = llm_block.get("litellm_provider") or (
model.split("/", 1)[0] if "/" in model else None
)
agent_block = dict(cfg.get("agent") or {})
prefix = model.split("/", 1)[0] if "/" in model else None
provider = llm_block.get("litellm_provider") or prefix
route = model.split("/", 1)[1] if "/" in model else model
is_claude = "claude" in route.lower()

mpm = ModelProviderManager()

# Providers with dedicated routing semantics (authenticated session, AWS
# SigV4 signing, dedicated endpoint templates) win over the metadata
# record's ``litellm_provider`` for every model under their prefix, so a
# ``github_copilot/`` / ``bedrock/`` / ``bedrock_mantle/`` model never
# falls back to a bare (anthropic/openai) record (e.g.
# ``github_copilot/claude-sonnet-4-5`` resolving to the bare
# ``claude-sonnet-4-5`` anthropic entry).
if prefix in ("github_copilot", "bedrock", "bedrock_mantle"):
provider = prefix

# Anthropic models hosted by a third-party provider (openrouter, deepseek,
# ...) authenticate against THAT provider, not anthropic: ``{provider}/claude-*``
# must route through the provider prefix even when the record lookup lands
# on the bare anthropic entry (e.g. ``openrouter/claude-sonnet-5``).
elif prefix and prefix != "anthropic" and is_claude:
provider = prefix

# A configured provider (built-in providers.json entry or a user-defined
# ``model-providers`` entry such as ``bifrost``) wins over the metadata
# record's ``litellm_provider`` for every model under its prefix, so
# ``bifrost/gemini/gemini-2.5-pro`` routes through bifrost (its api_base)
# rather than falling back to the bare ``gemini/gemini-2.5-pro`` record.
elif prefix and provider != prefix and mpm.supports_provider(prefix):
provider = prefix

pcfg = mpm.get_provider_config(provider) or {}

env_base = os.environ.get(f"{provider.upper()}_API_BASE") if provider else None
Expand Down Expand Up @@ -88,6 +123,12 @@ def resolve_model_config(model: str) -> Dict[str, Any]:
# Mantle is an OpenAI-compatible chat wire (SigV4-signed via the chat
# family's signer hook); see providers/bedrock_mantle.py.
family = "chat"
elif is_claude and provider != "anthropic":
# A claude model on a non-anthropic provider uses that provider's
# OpenAI-compatible chat completions wire (openrouter, deepseek,
# azure_ai, ...); only native anthropic (or copilot's anthropic-native
# /v1/messages proxy, handled above) speaks the messages API.
family = "chat"
elif mode == "responses" or "/v1/responses" in endpoints:
family = "responses"
elif provider == "anthropic":
Expand All @@ -100,6 +141,14 @@ def resolve_model_config(model: str) -> Dict[str, Any]:
extra_headers = dict(pcfg.get("default_headers") or {})
extra_body = dict(api_block.get("extra_body") or {})

# Effective cache flags for the domain adapters. Anthropic messages-API
# models never cache "by default": prompt caching only happens when the
# request carries explicit ``cache_control`` breakpoints, so the messages
# domain injects them (agent.cache_control True + caches_by_default False).
if family == "messages":
agent_block["cache_control"] = True
agent_block["caches_by_default"] = False

return {
"model": model,
"provider": provider,
Expand All @@ -112,6 +161,7 @@ def resolve_model_config(model: str) -> Dict[str, Any]:
"extra_query": dict(pcfg.get("extra_query") or {}),
"api_block": api_block,
"llm_block": llm_block,
"agent_block": agent_block,
}


Expand Down
50 changes: 47 additions & 3 deletions cecli/helpers/llms/domains/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
handles the three wild shapes (reasoning_content / reasoning /
reasoning_details). Reasoning tokens reported without streamed text are marked
redacted (``Message.reasoning_redacted``).

At request time the ``OPENAI_API_BASE`` / ``OPENAI_API_KEY`` env vars
(docs/llms/openai-compat.md) can redirect the wire to any OpenAI-compatible
endpoint: ``OPENAI_API_BASE`` wins over the resolved base and
``OPENAI_API_KEY`` replaces the provider key when the base is a well-formed
http(s) URL.
"""

from __future__ import annotations

import json
from typing import Any, AsyncIterator, Dict, List, Optional
import os
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
from urllib.parse import urlparse

from ..runtime import VERIFY_SSL, make_client
from ..types import (
Expand Down Expand Up @@ -94,12 +102,18 @@ async def chat_complete(
headers: Dict[str, str],
kwargs: Dict[str, Any],
) -> CompletionResponse:
url = f"{resolved['api_base']}/chat/completions"
env = _openai_env_override()
base = env[0] if env else resolved["api_base"]
url = f"{base}/chat/completions"
payload = chat_payload(resolved, messages, tools, False, kwargs)
hdrs = {"Content-Type": "application/json", **headers}
body: Optional[bytes] = None
signer = resolved.get("_signer")

if env and env[1]:
key = env[1]
hdrs["Authorization"] = f"Bearer {key}"

if signer:
url, hdrs, body = signer(url, payload, hdrs, key)

Expand Down Expand Up @@ -127,12 +141,18 @@ async def chat_stream(
headers: Dict[str, str],
kwargs: Dict[str, Any],
) -> AsyncIterator[CompletionChunk]:
url = f"{resolved['api_base']}/chat/completions"
env = _openai_env_override()
base = env[0] if env else resolved["api_base"]
url = f"{base}/chat/completions"
payload = chat_payload(resolved, messages, tools, True, kwargs)
hdrs = {"Content-Type": "application/json", **headers}
body: Optional[bytes] = None
signer = resolved.get("_signer")

if env and env[1]:
key = env[1]
hdrs["Authorization"] = f"Bearer {key}"

if signer:
url, hdrs, body = signer(url, payload, hdrs, key)

Expand Down Expand Up @@ -364,6 +384,30 @@ def _map_reasoning_content(msg: Dict[str, Any]) -> str:
return "\n".join(texts)


def _openai_env_override() -> Optional[Tuple[str, Optional[str]]]:
"""Return (api_base, api_key) from the OPENAI_API_* env vars when the base
is a well-formed http(s) URL (docs/llms/openai-compat.md).

Lets a custom OpenAI-compatible endpoint take over the chat completions
wire at request time: the env base wins over the resolved provider base and
OPENAI_API_KEY replaces the provider's own key for that request. A base
that is not a well-formed URL (no http(s) scheme / host, e.g.
``localhost:8000``) is ignored so unrelated OPENAI_API_* leftovers cannot
hijack requests.
"""
raw = os.environ.get("OPENAI_API_BASE")

if not raw:
return None

parsed = urlparse(raw.strip())

if parsed.scheme not in ("http", "https") or not parsed.netloc:
return None

return (raw.strip().rstrip("/"), os.environ.get("OPENAI_API_KEY") or None)


__all__ = [
"chat_payload",
"chat_complete",
Expand Down
Loading
Loading