mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat: support kimi coding plan by temporary solution
This commit is contained in:
@@ -102,18 +102,18 @@ Reference: [China Quick Start](https://docs.bigmodel.cn/cn/coding-plan/quick-sta
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"bot_type": "openai",
|
"bot_type": "moonshot",
|
||||||
"model": "kimi-for-coding",
|
"model": "kimi-for-coding",
|
||||||
"open_ai_api_base": "https://api.kimi.com/coding/v1",
|
"moonshot_base_url": "https://api.kimi.com/coding/v1",
|
||||||
"open_ai_api_key": "YOUR_API_KEY"
|
"moonshot_api_key": "YOUR_API_KEY"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `model` | `kimi-for-coding` |
|
| `model` | Use `kimi-for-coding` for auto-updating model, or specify a model such as `kimi-k2.6` |
|
||||||
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
|
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
|
||||||
| `open_ai_api_key` | Coding Plan specific key (not shared with pay-as-you-go) |
|
| `moonshot_api_key` | Coding Plan specific key (not shared with pay-as-you-go) |
|
||||||
|
|
||||||
Reference: [Key & Docs](https://www.kimi.com/code/docs/)
|
Reference: [Key & Docs](https://www.kimi.com/code/docs/)
|
||||||
|
|
||||||
|
|||||||
@@ -102,18 +102,18 @@ description: Coding Planモデルの設定
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"bot_type": "openai",
|
"bot_type": "moonshot",
|
||||||
"model": "kimi-for-coding",
|
"model": "kimi-for-coding",
|
||||||
"open_ai_api_base": "https://api.kimi.com/coding/v1",
|
"moonshot_base_url": "https://api.kimi.com/coding/v1",
|
||||||
"open_ai_api_key": "YOUR_API_KEY"
|
"moonshot_api_key": "YOUR_API_KEY"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| パラメータ | 説明 |
|
| パラメータ | 説明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `model` | `kimi-for-coding` |
|
| `model` | `kimi-for-coding`で自動更新モデル、または`kimi-k2.6`などのモデルを指定 |
|
||||||
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
|
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
|
||||||
| `open_ai_api_key` | Coding Plan専用キー(従量課金とは共有不可) |
|
| `moonshot_api_key` | Coding Plan専用キー(従量課金とは共有不可) |
|
||||||
|
|
||||||
参考: [キー & ドキュメント](https://www.kimi.com/code/docs/)
|
参考: [キー & ドキュメント](https://www.kimi.com/code/docs/)
|
||||||
|
|
||||||
|
|||||||
@@ -99,27 +99,6 @@ description: Coding Plan 模式模型配置
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kimi
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"bot_type": "openai",
|
|
||||||
"model": "kimi-for-coding",
|
|
||||||
"open_ai_api_base": "https://api.kimi.com/coding/v1",
|
|
||||||
"open_ai_api_key": "YOUR_API_KEY"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 参数 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `model` | `kimi-for-coding` |
|
|
||||||
| `open_ai_api_base` | `https://api.kimi.com/coding/v1` |
|
|
||||||
| `open_ai_api_key` | Coding Plan 专用 Key(与按量计费接口不通用) |
|
|
||||||
|
|
||||||
官方文档:[Key 获取](https://www.kimi.com/code/docs/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 火山引擎
|
## 火山引擎
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -138,3 +117,24 @@ description: Coding Plan 模式模型配置
|
|||||||
| `open_ai_api_key` | API Key 与普通接口通用 |
|
| `open_ai_api_key` | API Key 与普通接口通用 |
|
||||||
|
|
||||||
官方文档:[快速开始](https://www.volcengine.com/docs/82379/1928261?lang=zh)
|
官方文档:[快速开始](https://www.volcengine.com/docs/82379/1928261?lang=zh)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kimi
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bot_type": "moonshot",
|
||||||
|
"model": "kimi-for-coding",
|
||||||
|
"moonshot_base_url": "https://api.kimi.com/coding/v1",
|
||||||
|
"moonshot_api_key": "YOUR_API_KEY"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `model` | 填写 `kimi-for-coding` 会自动更新模型,或指定模型例如 `kimi-k2.6` |
|
||||||
|
| `moonshot_base_url` | `https://api.kimi.com/coding/v1` |
|
||||||
|
| `moonshot_api_key` | Coding Plan 专用 Key(与按量计费接口不通用) |
|
||||||
|
|
||||||
|
官方文档:[Key 获取](https://www.kimi.com/code/docs/)
|
||||||
|
|||||||
@@ -39,6 +39,29 @@ class MoonshotBot(Bot):
|
|||||||
url = url.rsplit("/chat/completions", 1)[0]
|
url = url.rsplit("/chat/completions", 1)[0]
|
||||||
return url.rstrip("/")
|
return url.rstrip("/")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _is_kimi_coding_plan(self) -> bool:
|
||||||
|
"""Detect Kimi Coding Plan by model name or API base URL."""
|
||||||
|
model = str(conf().get("model", ""))
|
||||||
|
base = str(conf().get("moonshot_base_url", ""))
|
||||||
|
return model == "kimi-for-coding" or "api.kimi.com/coding" in base
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _model_supports_thinking(model_name: str) -> bool:
|
||||||
|
"""Return True if the model supports the ``thinking`` request parameter."""
|
||||||
|
m = model_name.lower()
|
||||||
|
return m.startswith("kimi-k2") or m.startswith("kimi-k1.5")
|
||||||
|
|
||||||
|
def _build_headers(self) -> dict:
|
||||||
|
"""Build HTTP headers, adding Coding-Agent User-Agent for Kimi Coding Plan."""
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
}
|
||||||
|
if self._is_kimi_coding_plan:
|
||||||
|
headers["User-Agent"] = "claude-cli/2.1.39"
|
||||||
|
return headers
|
||||||
|
|
||||||
def reply(self, query, context=None):
|
def reply(self, query, context=None):
|
||||||
# acquire reply content
|
# acquire reply content
|
||||||
if context.type == ContextType.TEXT:
|
if context.type == ContextType.TEXT:
|
||||||
@@ -97,19 +120,15 @@ class MoonshotBot(Bot):
|
|||||||
:return: {}
|
:return: {}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = self._build_headers()
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + self.api_key
|
|
||||||
}
|
|
||||||
# Fallback to default args (e.g. when called by session title
|
# Fallback to default args (e.g. when called by session title
|
||||||
# generation which passes only the session). Always copy to avoid
|
# generation which passes only the session). Always copy to avoid
|
||||||
# mutating the shared self.args across calls.
|
# mutating the shared self.args across calls.
|
||||||
body = dict(args) if args else dict(self.args)
|
body = dict(args) if args else dict(self.args)
|
||||||
body["messages"] = session.messages
|
body["messages"] = session.messages
|
||||||
# K2.x series enforces fixed temperature/top_p; passing custom values
|
|
||||||
# makes the API reject the request. Strip them for kimi-k2 family.
|
|
||||||
model_name = str(body.get("model", ""))
|
model_name = str(body.get("model", ""))
|
||||||
if model_name.startswith("kimi-k2"):
|
# K2.x / Coding Plan enforce fixed temperature/top_p; strip them.
|
||||||
|
if model_name.startswith("kimi-k2") or model_name == "kimi-for-coding":
|
||||||
body.pop("temperature", None)
|
body.pop("temperature", None)
|
||||||
body.pop("top_p", None)
|
body.pop("top_p", None)
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
@@ -174,10 +193,7 @@ class MoonshotBot(Bot):
|
|||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
headers = {
|
headers = self._build_headers()
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
resp = requests.post(f"{self.base_url}/chat/completions",
|
resp = requests.post(f"{self.base_url}/chat/completions",
|
||||||
headers=headers, json=payload, timeout=60)
|
headers=headers, json=payload, timeout=60)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
@@ -258,7 +274,12 @@ class MoonshotBot(Bot):
|
|||||||
request_body["tools"] = converted_tools
|
request_body["tools"] = converted_tools
|
||||||
request_body["tool_choice"] = "auto"
|
request_body["tool_choice"] = "auto"
|
||||||
|
|
||||||
request_body["thinking"] = kwargs.get("thinking", {"type": "enabled"})
|
# Kimi Coding Plan has built-in reasoning and ignores the thinking param.
|
||||||
|
# For regular Kimi models, only K2/K1.5 series support the thinking param.
|
||||||
|
# Respect the enable_thinking config passed from agent_bridge.
|
||||||
|
if not self._is_kimi_coding_plan and self._model_supports_thinking(model):
|
||||||
|
thinking = kwargs.get("thinking", {"type": "enabled"})
|
||||||
|
request_body["thinking"] = thinking
|
||||||
|
|
||||||
logger.debug(f"[MOONSHOT] API call: model={model}, "
|
logger.debug(f"[MOONSHOT] API call: model={model}, "
|
||||||
f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}")
|
f"tools={len(converted_tools) if converted_tools else 0}, stream={stream}")
|
||||||
@@ -282,10 +303,7 @@ class MoonshotBot(Bot):
|
|||||||
def _handle_stream_response(self, request_body: dict):
|
def _handle_stream_response(self, request_body: dict):
|
||||||
"""Handle streaming SSE response from Moonshot API and yield OpenAI-format chunks."""
|
"""Handle streaming SSE response from Moonshot API and yield OpenAI-format chunks."""
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = self._build_headers()
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.api_key}"
|
|
||||||
}
|
|
||||||
|
|
||||||
url = f"{self.base_url}/chat/completions"
|
url = f"{self.base_url}/chat/completions"
|
||||||
response = requests.post(url, headers=headers, json=request_body, stream=True, timeout=120)
|
response = requests.post(url, headers=headers, json=request_body, stream=True, timeout=120)
|
||||||
@@ -304,10 +322,13 @@ class MoonshotBot(Bot):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
line = line.decode("utf-8")
|
line = line.decode("utf-8")
|
||||||
if not line.startswith("data: "):
|
# Handle both "data: {...}" and "data:{...}" (Kimi Coding Plan omits the space)
|
||||||
|
if line.startswith("data: "):
|
||||||
|
data_str = line[6:]
|
||||||
|
elif line.startswith("data:"):
|
||||||
|
data_str = line[5:]
|
||||||
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data_str = line[6:] # Remove "data: " prefix
|
|
||||||
if data_str.strip() == "[DONE]":
|
if data_str.strip() == "[DONE]":
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -328,9 +349,16 @@ class MoonshotBot(Bot):
|
|||||||
if not chunk.get("choices"):
|
if not chunk.get("choices"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
choice = chunk["choices"][0]
|
choices = chunk["choices"]
|
||||||
|
if not choices:
|
||||||
|
continue
|
||||||
|
choice = choices[0]
|
||||||
delta = choice.get("delta", {})
|
delta = choice.get("delta", {})
|
||||||
|
|
||||||
|
# Capture finish_reason early (it may arrive on any chunk type)
|
||||||
|
if choice.get("finish_reason"):
|
||||||
|
finish_reason = choice["finish_reason"]
|
||||||
|
|
||||||
if delta.get("reasoning_content"):
|
if delta.get("reasoning_content"):
|
||||||
yield {
|
yield {
|
||||||
"choices": [{
|
"choices": [{
|
||||||
@@ -382,10 +410,6 @@ class MoonshotBot(Bot):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Capture finish_reason
|
|
||||||
if choice.get("finish_reason"):
|
|
||||||
finish_reason = choice["finish_reason"]
|
|
||||||
|
|
||||||
# Final chunk with finish_reason
|
# Final chunk with finish_reason
|
||||||
yield {
|
yield {
|
||||||
"choices": [{
|
"choices": [{
|
||||||
@@ -409,10 +433,7 @@ class MoonshotBot(Bot):
|
|||||||
def _handle_sync_response(self, request_body: dict):
|
def _handle_sync_response(self, request_body: dict):
|
||||||
"""Handle synchronous API response and yield a single result dict."""
|
"""Handle synchronous API response and yield a single result dict."""
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = self._build_headers()
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {self.api_key}"
|
|
||||||
}
|
|
||||||
|
|
||||||
request_body.pop("stream", None)
|
request_body.pop("stream", None)
|
||||||
url = f"{self.base_url}/chat/completions"
|
url = f"{self.base_url}/chat/completions"
|
||||||
@@ -530,6 +551,7 @@ class MoonshotBot(Bot):
|
|||||||
openai_msg = {"role": "assistant"}
|
openai_msg = {"role": "assistant"}
|
||||||
text_parts = []
|
text_parts = []
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
reasoning_parts = []
|
||||||
|
|
||||||
for block in content:
|
for block in content:
|
||||||
if not isinstance(block, dict):
|
if not isinstance(block, dict):
|
||||||
@@ -545,6 +567,8 @@ class MoonshotBot(Bot):
|
|||||||
"arguments": json.dumps(block.get("input", {}))
|
"arguments": json.dumps(block.get("input", {}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
elif block.get("type") == "thinking":
|
||||||
|
reasoning_parts.append(block.get("thinking", ""))
|
||||||
|
|
||||||
if text_parts:
|
if text_parts:
|
||||||
openai_msg["content"] = "\n".join(text_parts)
|
openai_msg["content"] = "\n".join(text_parts)
|
||||||
@@ -556,6 +580,12 @@ class MoonshotBot(Bot):
|
|||||||
if not text_parts:
|
if not text_parts:
|
||||||
openai_msg["content"] = None
|
openai_msg["content"] = None
|
||||||
|
|
||||||
|
# Kimi API requires reasoning_content in assistant messages
|
||||||
|
# when thinking was active for that turn. The presence of
|
||||||
|
# reasoning_parts means thinking was on, so always round-trip it.
|
||||||
|
if reasoning_parts:
|
||||||
|
openai_msg["reasoning_content"] = "\n".join(reasoning_parts)
|
||||||
|
|
||||||
converted.append(openai_msg)
|
converted.append(openai_msg)
|
||||||
else:
|
else:
|
||||||
converted.append(msg)
|
converted.append(msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user