Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
22 changes: 22 additions & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,8 @@ def identify(event_details):
v0_models: Set = set()
morph_models: Set = set()
lambda_ai_models: Set = set()
opencode_models: Set = set()
opencode_go_models: Set = set()
inception_models: Set = set()
hyperbolic_models: Set = set()
black_forest_labs_models: Set = set()
Expand Down Expand Up @@ -934,6 +936,10 @@ def _populate_provider_model_sets(model_cost_map: Dict) -> None:
morph_models.add(key)
elif value.get("litellm_provider") == "lambda_ai":
lambda_ai_models.add(key)
elif value.get("litellm_provider") == "opencode":
opencode_models.add(key)
elif value.get("litellm_provider") == "opencode_go":
opencode_go_models.add(key)
elif value.get("litellm_provider") == "inception":
inception_models.add(key)
elif value.get("litellm_provider") == "hyperbolic":
Expand Down Expand Up @@ -1091,6 +1097,8 @@ def add_known_models(model_cost_map: Optional[Dict] = None):
| v0_models
| morph_models
| lambda_ai_models
| opencode_models
| opencode_go_models
| inception_models
| black_forest_labs_models
| recraft_models
Expand Down Expand Up @@ -1200,6 +1208,8 @@ def _build_models_by_provider() -> dict:
"v0": v0_models,
"morph": morph_models,
"lambda_ai": lambda_ai_models,
"opencode": opencode_models,
"opencode_go": opencode_go_models,
"inception": inception_models,
"hyperbolic": hyperbolic_models,
"black_forest_labs": black_forest_labs_models,
Expand Down Expand Up @@ -2064,6 +2074,18 @@ def set_global_gitlab_config(config: Dict[str, Any]) -> None:
from .llms.lambda_ai.chat.transformation import (
LambdaAIChatConfig as LambdaAIChatConfig,
)
from .llms.opencode.chat.transformation import (
OpenCodeGoChatConfig as OpenCodeGoChatConfig,
)
from .llms.opencode.chat.transformation import (
OpenCodeZenChatConfig as OpenCodeZenChatConfig,
)
from .llms.opencode.responses.transformation import (
OpenCodeGoResponsesAPIConfig as OpenCodeGoResponsesAPIConfig,
)
from .llms.opencode.responses.transformation import (
OpenCodeZenResponsesAPIConfig as OpenCodeZenResponsesAPIConfig,
)
from .llms.inception.chat.transformation import (
InceptionChatConfig as InceptionChatConfig,
)
Expand Down
29 changes: 29 additions & 0 deletions litellm/_lazy_imports_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@
"MorphChatConfig",
"RAGFlowConfig",
"LambdaAIChatConfig",
"OpenCodeZenChatConfig",
"OpenCodeGoChatConfig",
"OpenCodeZenMessagesChatConfig",
"OpenCodeGoMessagesChatConfig",
"OpenCodeZenGeminiChatConfig",
"OpenCodeZenResponsesAPIConfig",
"OpenCodeGoResponsesAPIConfig",
"InceptionChatConfig",
"HyperbolicChatConfig",
"VercelAIGatewayConfig",
Expand Down Expand Up @@ -1200,6 +1207,28 @@
"MorphChatConfig": (".llms.morph.chat.transformation", "MorphChatConfig"),
"RAGFlowConfig": (".llms.ragflow.chat.transformation", "RAGFlowConfig"),
"LambdaAIChatConfig": (".llms.lambda_ai.chat.transformation", "LambdaAIChatConfig"),
"OpenCodeZenChatConfig": (".llms.opencode.chat.transformation", "OpenCodeZenChatConfig"),
"OpenCodeGoChatConfig": (".llms.opencode.chat.transformation", "OpenCodeGoChatConfig"),
"OpenCodeZenMessagesChatConfig": (
".llms.opencode.chat.transformation",
"OpenCodeZenMessagesChatConfig",
),
"OpenCodeGoMessagesChatConfig": (
".llms.opencode.chat.transformation",
"OpenCodeGoMessagesChatConfig",
),
"OpenCodeZenGeminiChatConfig": (
".llms.opencode.chat.transformation",
"OpenCodeZenGeminiChatConfig",
),
"OpenCodeZenResponsesAPIConfig": (
".llms.opencode.responses.transformation",
"OpenCodeZenResponsesAPIConfig",
),
"OpenCodeGoResponsesAPIConfig": (
".llms.opencode.responses.transformation",
"OpenCodeGoResponsesAPIConfig",
),
"InceptionChatConfig": (
".llms.inception.chat.transformation",
"InceptionChatConfig",
Expand Down
6 changes: 6 additions & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,8 @@
"oci",
"morph",
"lambda_ai",
"opencode",
"opencode_go",
"inception",
"vercel_ai_gateway",
"wandb",
Expand Down Expand Up @@ -865,6 +867,8 @@
"https://api.cognition.ai/v1",
"https://api.scx.ai/v1",
"https://gigachat.devices.sberbank.ru/api/v1",
"https://opencode.ai/zen/go/v1",
"https://opencode.ai/zen/v1",
]


Expand Down Expand Up @@ -922,6 +926,8 @@
"helicone",
"morph",
"lambda_ai",
"opencode",
"opencode_go",
"inception",
"hyperbolic",
"vercel_ai_gateway",
Expand Down
16 changes: 16 additions & 0 deletions litellm/litellm_core_utils/get_llm_provider_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ def get_llm_provider(
elif endpoint == "https://api.lambda.ai/v1":
custom_llm_provider = "lambda_ai"
dynamic_api_key = get_secret_str("LAMBDA_API_KEY")
elif endpoint == "https://opencode.ai/zen/go/v1":
custom_llm_provider = "opencode_go" # rebind-ok: dispatch chain rebinds this param
dynamic_api_key = api_key or get_secret_str("OPENCODE_API_KEY")
elif endpoint == "https://opencode.ai/zen/v1":
custom_llm_provider = "opencode" # rebind-ok: dispatch chain rebinds this param
dynamic_api_key = api_key or get_secret_str("OPENCODE_API_KEY")
elif endpoint == "https://api.inceptionlabs.ai/v1":
custom_llm_provider = "inception"
dynamic_api_key = get_secret_str("INCEPTION_API_KEY")
Expand Down Expand Up @@ -829,6 +835,16 @@ def _get_openai_compatible_provider_info(
api_base,
dynamic_api_key,
) = litellm.LambdaAIChatConfig()._get_openai_compatible_provider_info(api_base, api_key)
elif custom_llm_provider == "opencode":
(
api_base, # rebind-ok: resolution chain returns these params
dynamic_api_key, # rebind-ok: resolution chain returns these params
) = litellm.OpenCodeZenChatConfig()._get_openai_compatible_provider_info(api_base, api_key)
elif custom_llm_provider == "opencode_go":
(
api_base, # rebind-ok: resolution chain returns these params
dynamic_api_key, # rebind-ok: resolution chain returns these params
) = litellm.OpenCodeGoChatConfig()._get_openai_compatible_provider_info(api_base, api_key)
elif custom_llm_provider == "inception":
(
api_base,
Expand Down
Empty file.
Empty file.
209 changes: 209 additions & 0 deletions litellm/llms/opencode/chat/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
Translate from OpenAI's `/v1/chat/completions` to OpenCode Zen's and OpenCode Go's.

Both surfaces are OpenAI-compatible, so only the default base URL, the credential lookup and
the required `x-opencode-session` header differ from stock OpenAI.
"""

from collections.abc import Mapping, Sequence
from itertools import chain
from typing import ClassVar, Final

from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.gemini.chat.transformation import GoogleAIStudioGeminiConfig
from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues

from ..common_utils import resolve_opencode_api_key, with_opencode_session_header


class OpenCodeChatConfig(OpenAILikeChatConfig):
_provider: ClassVar[str]
_default_api_base: ClassVar[str]
_api_base_env_var: ClassVar[str]

@property
def custom_llm_provider(self) -> str | None:
return self._provider

def _get_openai_compatible_provider_info(
self, api_base: str | None, api_key: str | None
) -> tuple[str | None, str | None]:
resolved_api_base: Final = api_base or get_secret_str(self._api_base_env_var) or self._default_api_base
return resolved_api_base, resolve_opencode_api_key(api_key)

def validate_environment(
self,
headers: Mapping[str, object],
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict: # mutable-ok: return type is fixed by BaseConfig.validate_environment, whose callers mutate it
base_headers: Final[Mapping[str, object]] = super().validate_environment(
headers=dict(headers), # mutable-ok: BaseConfig.validate_environment only accepts mutable dicts
model=model,
messages=list(messages), # mutable-ok: BaseConfig.validate_environment only accepts a mutable list
optional_params=dict(optional_params), # mutable-ok: as above
litellm_params=dict(litellm_params), # mutable-ok: as above
api_key=api_key,
api_base=api_base,
)
return with_opencode_session_header(base_headers, litellm_params)


class OpenCodeZenChatConfig(OpenCodeChatConfig):
_provider: ClassVar[str] = "opencode"
_default_api_base: ClassVar[str] = "https://opencode.ai/zen/v1"
_api_base_env_var: ClassVar[str] = "OPENCODE_API_BASE"


class OpenCodeGoChatConfig(OpenCodeChatConfig):
_provider: ClassVar[str] = "opencode_go"
_default_api_base: ClassVar[str] = "https://opencode.ai/zen/go/v1"
_api_base_env_var: ClassVar[str] = "OPENCODE_GO_API_BASE"


class OpenCodeMessagesChatConfig(AnthropicConfig):
"""
OpenCode serves part of its catalogue (Claude, Qwen, MiniMax) on an Anthropic-shaped
`/messages` endpoint that authenticates with `x-api-key` rather than a bearer token.
"""

_provider: ClassVar[str]
_default_api_base: ClassVar[str]
_api_base_env_var: ClassVar[str]

@property
def custom_llm_provider(self) -> str | None:
return self._provider

def get_complete_url(
self,
api_base: str | None,
api_key: str | None,
model: str,
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
stream: bool | None = None,
) -> str:
resolved_api_base: Final = (
api_base or get_secret_str(self._api_base_env_var) or self._default_api_base
).rstrip("/")
if resolved_api_base.endswith("/messages"):
return resolved_api_base
return f"{resolved_api_base}/messages"

def validate_environment(
self,
headers: Mapping[str, object],
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict: # mutable-ok: return type is fixed by BaseConfig.validate_environment
base_headers: Final[Mapping[str, object]] = super().validate_environment(
headers=dict(headers), # mutable-ok: AnthropicConfig.validate_environment needs a mutable dict
model=model,
messages=list(messages), # mutable-ok: as above
optional_params=dict(optional_params), # mutable-ok: as above
litellm_params=dict(litellm_params), # mutable-ok: as above
api_key=resolve_opencode_api_key(api_key),
api_base=api_base,
)
return with_opencode_session_header(base_headers, litellm_params)


class OpenCodeZenMessagesChatConfig(OpenCodeMessagesChatConfig):
_provider: ClassVar[str] = "opencode"
_default_api_base: ClassVar[str] = "https://opencode.ai/zen/v1"
_api_base_env_var: ClassVar[str] = "OPENCODE_API_BASE"


class OpenCodeGoMessagesChatConfig(OpenCodeMessagesChatConfig):
_provider: ClassVar[str] = "opencode_go"
_default_api_base: ClassVar[str] = "https://opencode.ai/zen/go/v1"
_api_base_env_var: ClassVar[str] = "OPENCODE_GO_API_BASE"


class OpenCodeZenGeminiChatConfig(GoogleAIStudioGeminiConfig):
"""
OpenCode Zen serves Google models on Gemini's own `models/<id>:generateContent` shape,
hosted under the Zen base URL and authenticated with a bearer token.
"""

_default_api_base: ClassVar[str] = "https://opencode.ai/zen/v1"
_api_base_env_var: ClassVar[str] = "OPENCODE_API_BASE"

@property
def custom_llm_provider(self) -> str | None:
return "opencode"

def get_complete_url(
self,
api_base: str | None,
api_key: str | None,
model: str,
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
stream: bool | None = None,
) -> str:
resolved_api_base: Final = (
api_base or get_secret_str(self._api_base_env_var) or self._default_api_base
).rstrip("/")
if stream:
return f"{resolved_api_base}/models/{model}:streamGenerateContent?alt=sse"
return f"{resolved_api_base}/models/{model}:generateContent"

def transform_request(
self,
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
headers: Mapping[str, object],
) -> dict: # mutable-ok: return type is fixed by BaseConfig.transform_request
"""
VertexGeminiConfig.transform_request raises NotImplementedError because Vertex builds its
body in a bespoke handler, so reuse the shared Gemini body builder that handler calls.
"""
from litellm.llms.vertex_ai.gemini.transformation import _transform_request_body

cached_content: Final = optional_params.get("cached_content")
return _transform_request_body(
messages=list(messages), # mutable-ok: the Gemini body builder takes a mutable list
model=model,
optional_params=dict(optional_params), # mutable-ok: as above
custom_llm_provider="gemini",
litellm_params=dict(litellm_params), # mutable-ok: as above
cached_content=cached_content if isinstance(cached_content, str) else None,
)

def validate_environment(
self,
headers: Mapping[str, object] | None,
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
api_key: str | dict | None = None, # mutable-ok: signature is fixed by VertexGeminiConfig
api_base: str | None = None,
) -> dict: # mutable-ok: return type is fixed by BaseConfig.validate_environment
"""
OpenCode authenticates this endpoint with Google's `x-goog-api-key`; a bearer token,
an `x-api-key` and a `?key=` query parameter all come back as `Missing API key`.
"""
resolved_key: Final = resolve_opencode_api_key(api_key if isinstance(api_key, str) else None)
base_headers: Final[Mapping[str, object]] = dict(
chain(
(("Content-Type", "application/json"),),
headers.items() if headers else (),
(("x-goog-api-key", resolved_key),),
)
)
return with_opencode_session_header(base_headers, litellm_params)
Loading
Loading