Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 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,12 @@ 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.inception.chat.transformation import (
InceptionChatConfig as InceptionChatConfig,
)
Expand Down
4 changes: 4 additions & 0 deletions litellm/_lazy_imports_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@
"MorphChatConfig",
"RAGFlowConfig",
"LambdaAIChatConfig",
"OpenCodeZenChatConfig",
"OpenCodeGoChatConfig",
"InceptionChatConfig",
"HyperbolicChatConfig",
"VercelAIGatewayConfig",
Expand Down Expand Up @@ -1200,6 +1202,8 @@
"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"),
"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 = get_secret_str("OPENCODE_API_KEY") or get_secret_str("OPENCODE_ZEN_API_KEY")
elif endpoint == "https://opencode.ai/zen/v1":
custom_llm_provider = "opencode" # rebind-ok: dispatch chain rebinds this param
dynamic_api_key = get_secret_str("OPENCODE_API_KEY") or get_secret_str("OPENCODE_ZEN_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.
76 changes: 76 additions & 0 deletions litellm/llms/opencode/chat/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
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 typing import ClassVar, Final

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 (
OPENCODE_SESSION_HEADER,
has_opencode_session_header,
resolve_opencode_session_id,
)


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
resolved_api_key: Final = (
api_key or get_secret_str("OPENCODE_API_KEY") or get_secret_str("OPENCODE_ZEN_API_KEY")
)
return resolved_api_base, resolved_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,
)
session_id: Final = (
None if has_opencode_session_header(base_headers) else resolve_opencode_session_id(litellm_params)
)
if session_id is None:
return dict(base_headers)
return {**base_headers, OPENCODE_SESSION_HEADER: session_id}


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"
30 changes: 30 additions & 0 deletions litellm/llms/opencode/common_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Shared helpers for OpenCode's managed inference APIs (Zen and Go).
"""

from collections.abc import Iterable, Mapping
from typing import Final

OPENCODE_SESSION_HEADER: Final = "x-opencode-session"


def resolve_opencode_session_id(litellm_params: Mapping[str, object]) -> str | None:
"""
OpenCode requires a stable per-conversation id on every inference request so it can pin a
conversation to one upstream and keep that upstream's prompt cache warm. LiteLLM keeps no
conversation state of its own, so a caller-supplied session id is the only value that is
genuinely stable across turns; the per-request ids are a last resort that keeps the request
accepted even though it forfeits cache affinity.
"""
metadata: Final = litellm_params.get("metadata")
candidates: Final = (
litellm_params.get("litellm_session_id"),
metadata.get("session_id") if isinstance(metadata, Mapping) else None,
litellm_params.get("litellm_trace_id"),
litellm_params.get("litellm_call_id"),
)
return next((candidate for candidate in candidates if isinstance(candidate, str) and candidate), None)


def has_opencode_session_header(header_names: Iterable[str]) -> bool:
return any(name.lower() == OPENCODE_SESSION_HEADER for name in header_names)
52 changes: 52 additions & 0 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,56 @@ def _complete_groq(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult
)


def _complete_opencode(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
acompletion: Final = ctx.acompletion
api_base: Final = ctx.api_base
api_key: Final = ctx.api_key
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

try:
response: Final = base_llm_http_handler.completion(
model=model,
messages=messages,
headers=headers,
model_response=model_response,
api_key=api_key,
api_base=api_base,
acompletion=acompletion,
logging_obj=logging,
optional_params=optional_params,
litellm_params=litellm_params,
shared_session=shared_session,
timeout=timeout, # pyright: ignore[reportArgumentType] # ctx.timeout allows str/None, as in sibling helpers
client=client,
custom_llm_provider=custom_llm_provider,
encoding=_get_encoding(),
stream=stream,
provider_config=provider_config,
)
except Exception as e:
logging.post_call(
input=messages,
api_key=api_key,
original_response=str(e),
additional_args={"headers": headers},
)
raise e

return response


def _complete_bedrock_mantle(
ctx: _CompletionDispatchContext,
) -> _CompletionDispatchResult:
Expand Down Expand Up @@ -5671,6 +5721,8 @@ def completion(
response = _complete_groq(_dispatch_ctx)
elif custom_llm_provider == "bedrock_mantle":
response = _complete_bedrock_mantle(_dispatch_ctx)
elif custom_llm_provider == "opencode" or custom_llm_provider == "opencode_go":
response = _complete_opencode(_dispatch_ctx)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
elif custom_llm_provider == "a2a":
# A2A (Agent-to-Agent) Protocol
# Resolve agent configuration from registry if model format is "a2a/<agent-name>"
Expand Down
Loading
Loading