-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Feat: Opencode Go Subcription as Provider #8179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5ff796e
af88dcb
a5a74da
b6a5ade
45a5e3e
340d4e6
7fbfa80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -515,6 +515,41 @@ def _apply_provider_specific_extra_body_overrides( | |
| extra_body.pop("think", None) | ||
| extra_body["reasoning_effort"] = "none" | ||
|
|
||
| def _requires_tool_call_reasoning_content( | ||
| self, | ||
| payloads: dict, | ||
|
Comment on lines
+518
to
+520
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The Since Suggested implementation: def _requires_tool_call_reasoning_content(
self,
extra_body: dict[str, Any],
) -> bool:You will also need to:
|
||
| extra_body: dict[str, Any], | ||
| ) -> bool: | ||
| thinking = extra_body.get("thinking") | ||
| if isinstance(thinking, dict) and thinking.get("type") == "disabled": | ||
| return False | ||
|
|
||
| value = self.provider_config.get("force_tool_call_reasoning_content", False) | ||
| if isinstance(value, str): | ||
| return value.strip().lower() in {"1", "true", "yes", "on"} | ||
| return bool(value) | ||
|
|
||
| def _ensure_tool_call_reasoning_content( | ||
| self, | ||
| payloads: dict, | ||
| extra_body: dict[str, Any], | ||
| ) -> None: | ||
| if not self._requires_tool_call_reasoning_content(payloads, extra_body): | ||
| return | ||
|
|
||
| messages = payloads.get("messages") | ||
| if not isinstance(messages, list): | ||
| return | ||
|
|
||
| for message in messages: | ||
| if not isinstance(message, dict): | ||
| continue | ||
| if message.get("role") != "assistant" or not message.get("tool_calls"): | ||
| continue | ||
| reasoning_content = message.get("reasoning_content") | ||
| if not isinstance(reasoning_content, str) or not reasoning_content.strip(): | ||
| message["reasoning_content"] = " " | ||
|
|
||
| async def get_models(self): | ||
| try: | ||
| models_str = [] | ||
|
|
@@ -591,6 +626,7 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: | |
|
|
||
| model = payloads.get("model", "").lower() | ||
|
|
||
| self._ensure_tool_call_reasoning_content(payloads, extra_body) | ||
| self._sanitize_assistant_messages(payloads) | ||
|
|
||
| completion = await self.client.chat.completions.create( | ||
|
|
@@ -643,6 +679,7 @@ async def _query_stream( | |
| del payloads[key] | ||
| self._apply_provider_specific_extra_body_overrides(extra_body) | ||
|
|
||
| self._ensure_tool_call_reasoning_content(payloads, extra_body) | ||
| self._sanitize_assistant_messages(payloads) | ||
|
|
||
| stream = await self.client.chat.completions.create( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| from collections.abc import AsyncGenerator | ||
| from typing import Literal | ||
|
|
||
| from astrbot.api.provider import Provider | ||
| from astrbot.core.agent.message import ContentPart, Message | ||
| from astrbot.core.agent.tool import ToolSet | ||
| from astrbot.core.provider.entities import LLMResponse, ToolCallsResult | ||
|
|
||
| from ..register import register_provider_adapter | ||
| from .openai_source import ProviderOpenAIOfficial | ||
|
|
||
| OPENCODE_GO_API_BASE = "https://opencode.ai/zen/go/v1" | ||
| OPENCODE_GO_MODEL_PREFIX = "opencode-go/" | ||
| OPENCODE_GO_DEFAULT_MODEL = "kimi-k2.6" | ||
| OPENCODE_GO_MESSAGES_ONLY_MODELS = {"minimax-m2.5", "minimax-m2.7"} | ||
|
|
||
|
|
||
| @register_provider_adapter( | ||
| "opencode_go_chat_completion", | ||
| "OpenCode Go Subscription Provider Adapter", | ||
| ) | ||
| class ProviderOpenCodeGo(Provider): | ||
| def __init__(self, provider_config: dict, provider_settings: dict) -> None: | ||
| super().__init__(provider_config, provider_settings) | ||
| self.api_base = provider_config.get("api_base", OPENCODE_GO_API_BASE).rstrip( | ||
| "/" | ||
| ) | ||
| self.timeout = provider_config.get("timeout", 120) | ||
| if isinstance(self.timeout, str): | ||
| self.timeout = int(self.timeout) | ||
|
|
||
| model = self._to_api_model( | ||
| provider_config.get("model", OPENCODE_GO_DEFAULT_MODEL) | ||
| ) | ||
| self.set_model(model) | ||
|
|
||
| self.openai_provider = ProviderOpenAIOfficial( | ||
| self._build_delegate_config(model=model), | ||
| provider_settings, | ||
| ) | ||
|
|
||
| def _build_delegate_config(self, *, model: str) -> dict: | ||
| config = dict(self.provider_config) | ||
| config["api_base"] = self.api_base | ||
| config["model"] = model | ||
| config["force_tool_call_reasoning_content"] = True | ||
| return config | ||
|
|
||
| @classmethod | ||
| def _to_api_model(cls, model: str | None) -> str: | ||
| resolved_model = (model or OPENCODE_GO_DEFAULT_MODEL).strip() | ||
| if resolved_model.startswith(OPENCODE_GO_MODEL_PREFIX): | ||
| return resolved_model.removeprefix(OPENCODE_GO_MODEL_PREFIX) | ||
| return resolved_model | ||
|
|
||
| @classmethod | ||
| def _to_provider_model(cls, model: str) -> str: | ||
| api_model = cls._to_api_model(model) | ||
| return f"{OPENCODE_GO_MODEL_PREFIX}{api_model}" | ||
|
|
||
| @classmethod | ||
| def _ensure_chat_completions_model(cls, model: str | None) -> str: | ||
| api_model = cls._to_api_model(model) | ||
| if api_model in OPENCODE_GO_MESSAGES_ONLY_MODELS: | ||
| raise ValueError( | ||
| f"OpenCode Go model {OPENCODE_GO_MODEL_PREFIX}{api_model} uses " | ||
| "/v1/messages. This adapter currently supports " | ||
| "/v1/chat/completions models only." | ||
| ) | ||
| return api_model | ||
|
|
||
| def _resolve_model(self, model: str | None = None) -> str: | ||
| return self._ensure_chat_completions_model(model or self.get_model()) | ||
|
|
||
| def get_current_key(self) -> str: | ||
| return self.openai_provider.get_current_key() | ||
|
|
||
| def get_keys(self) -> list[str]: | ||
| return self.openai_provider.get_keys() | ||
|
|
||
| def set_key(self, key: str) -> None: | ||
| self.openai_provider.set_key(key) | ||
|
|
||
| async def get_models(self) -> list[str]: | ||
| models = await self.openai_provider.get_models() | ||
| provider_models: list[str] = [] | ||
| for model in models: | ||
| api_model = self._to_api_model(model) | ||
| if not api_model or api_model in OPENCODE_GO_MESSAGES_ONLY_MODELS: | ||
| continue | ||
| provider_models.append(f"{OPENCODE_GO_MODEL_PREFIX}{api_model}") | ||
| return sorted(provider_models) | ||
|
|
||
| async def text_chat( | ||
| self, | ||
| prompt: str | None = None, | ||
| session_id: str | None = None, | ||
| image_urls: list[str] | None = None, | ||
| audio_urls: list[str] | None = None, | ||
| func_tool: ToolSet | None = None, | ||
| contexts: list[Message] | list[dict] | None = None, | ||
| system_prompt: str | None = None, | ||
| tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None, | ||
| model: str | None = None, | ||
| extra_user_content_parts: list[ContentPart] | None = None, | ||
| tool_choice: Literal["auto", "required"] = "auto", | ||
| **kwargs, | ||
| ) -> LLMResponse: | ||
| return await self.openai_provider.text_chat( | ||
| prompt=prompt, | ||
| session_id=session_id, | ||
| image_urls=image_urls, | ||
| audio_urls=audio_urls, | ||
| func_tool=func_tool, | ||
| contexts=contexts, | ||
| system_prompt=system_prompt, | ||
| tool_calls_result=tool_calls_result, | ||
| model=self._resolve_model(model), | ||
| extra_user_content_parts=extra_user_content_parts, | ||
| tool_choice=tool_choice, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| async def text_chat_stream( | ||
| self, | ||
| prompt: str | None = None, | ||
| session_id: str | None = None, | ||
| image_urls: list[str] | None = None, | ||
| audio_urls: list[str] | None = None, | ||
| func_tool: ToolSet | None = None, | ||
| contexts: list[Message] | list[dict] | None = None, | ||
| system_prompt: str | None = None, | ||
| tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None, | ||
| model: str | None = None, | ||
| tool_choice: Literal["auto", "required"] = "auto", | ||
| **kwargs, | ||
| ) -> AsyncGenerator[LLMResponse, None]: | ||
| async for response in self.openai_provider.text_chat_stream( | ||
| prompt=prompt, | ||
| session_id=session_id, | ||
| image_urls=image_urls, | ||
| audio_urls=audio_urls, | ||
| func_tool=func_tool, | ||
| contexts=contexts, | ||
| system_prompt=system_prompt, | ||
| tool_calls_result=tool_calls_result, | ||
| model=self._resolve_model(model), | ||
| tool_choice=tool_choice, | ||
| **kwargs, | ||
| ): | ||
| yield response | ||
|
Comment on lines
+94
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
例如,可以创建一个 def _resolve_model(self, model: str | None) -> str:
requested_model = model or self.get_model()
return self._ensure_chat_completions_model(requested_model)此外,新功能的实现(如该 Provider 的核心逻辑)应当伴随相应的单元测试以确保稳定性。 References
|
||
|
|
||
| async def terminate(self) -> None: | ||
| await self.openai_provider.terminate() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new configuration field
force_tool_call_reasoning_contentincludes hardcoded Chinese strings fordescriptionandhint. According to the project's standards (see the warning comment at line 3015), these fields have been internationalized. Please ensure that the corresponding keys and translations are added to the following resource files:dashboard/src/i18n/locales/en-US/features/config-metadata.jsondashboard/src/i18n/locales/zh-CN/features/config-metadata.jsonReferences