mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-10-25 17:36:29 +08:00
feat: add gpt-6-astra model with Responses API tool calling
Route gpt-6-astra tool calls through the Responses API without depending on the openai SDK, translating requests/streaming back to the chat-completion shape consumed downstream. Also surface it in the console dropdown, README and docs, and reorder the recommended list (gpt group ahead of gemini). Co-authored-by: cowagent <cow@cowagent.ai>
This commit is contained in:
1 parent
f4dbae8f49
commit
ead145a7ae
15 files changed
+541
-21
No files matched your search
@@ -132,8 +132,8 @@ CowAgent supports all mainstream LLM providers. **Chat, vision, image generation
|
||||
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||
| [DeepSeek](https://docs.cowagent.ai/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
|
||||
| [Claude](https://docs.cowagent.ai/models/claude) | claude-opus-5 / fable-5.1 | ✅ | ✅ | | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/models/openai) | gpt-5.6 series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/models/gemini) | gemini-3.7-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/models/openai) | gpt-6-astra / gpt-5.6 series | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/models/gemini) | gemini-3.8-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [MiniMax](https://docs.cowagent.ai/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
|
||||
| [GLM](https://docs.cowagent.ai/models/glm) | glm-5.3-flash, glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
|
||||
| [Qwen](https://docs.cowagent.ai/models/qwen) | qwen3.8-flash | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -2728,8 +2728,8 @@ class ConfigHandler:
|
||||
const.MINIMAX_M3, const.MINIMAX_M2_7_HIGHSPEED, const.MINIMAX_M2_7,
|
||||
# claude-opus-5 is the Claude default; claude-sonnet-5 / claude-fable-5 follow right after it.
|
||||
const.CLAUDE_OPUS_5, const.CLAUDE_SONNET_5, const.CLAUDE_FABLE_5_1, const.CLAUDE_FABLE_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS,
|
||||
const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE,
|
||||
const.GPT_56_LUNA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o,
|
||||
const.GPT_56_LUNA, const.GPT_6_ASTRA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o,
|
||||
const.GEMINI_38_FLASH, const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE,
|
||||
const.GLM_5_3_FLASH, const.GLM_5_3, const.GLM_5_2, const.GLM_5_1, const.GLM_5_TURBO, const.GLM_5, const.GLM_4_7,
|
||||
const.QWEN38_FLASH, const.QWEN38_MAX, const.QWEN37_PLUS, const.QWEN37_MAX, const.QWEN36_PLUS,
|
||||
const.DOUBAO_SEED_2_1_PRO, const.DOUBAO_SEED_2_1_TURBO, const.DOUBAO_SEED_2_CODE,
|
||||
@@ -2772,7 +2772,7 @@ class ConfigHandler:
|
||||
"api_base_key": "open_ai_api_base",
|
||||
"api_base_default": "https://api.openai.com/v1",
|
||||
"api_base_placeholder": _PLACEHOLDER_V1,
|
||||
"models": [const.GPT_56_LUNA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o],
|
||||
"models": [const.GPT_56_LUNA, const.GPT_6_ASTRA, const.GPT_56_TERRA, const.GPT_56_SOL, const.GPT_55, const.GPT_54, const.GPT_54_MINI, const.GPT_54_NANO, const.GPT_5, const.GPT_41, const.GPT_4o],
|
||||
}),
|
||||
("gemini", {
|
||||
"label": "Gemini",
|
||||
@@ -2780,7 +2780,7 @@ class ConfigHandler:
|
||||
"api_base_key": "gemini_api_base",
|
||||
"api_base_default": "https://generativelanguage.googleapis.com",
|
||||
"api_base_placeholder": _PLACEHOLDER_GEMINI,
|
||||
"models": [const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
|
||||
"models": [const.GEMINI_38_FLASH, const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
|
||||
}),
|
||||
("minimax", {
|
||||
"label": "MiniMax",
|
||||
@@ -3476,7 +3476,7 @@ class ModelsHandler:
|
||||
# entry is the auto-picked vision model, and image understanding does
|
||||
# not justify the Opus price.
|
||||
"claudeAPI": [const.CLAUDE_SONNET_5, const.CLAUDE_OPUS_5, const.CLAUDE_FABLE_5_1, const.CLAUDE_FABLE_5, const.CLAUDE_4_8_OPUS, const.CLAUDE_4_7_OPUS, const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS],
|
||||
"gemini": [const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
|
||||
"gemini": [const.GEMINI_38_FLASH, const.GEMINI_37_FLASH, const.GEMINI_36_FLASH, const.GEMINI_35_FLASH, const.GEMINI_31_FLASH_LITE_PRE, const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
|
||||
"qianfan": [const.ERNIE_45_TURBO_VL],
|
||||
# glm-5.3-flash is natively multimodal and dispatched as-is; the
|
||||
# text-only chat models (glm-5.2, glm-5-turbo, etc.) fall back to the
|
||||
@@ -3867,7 +3867,7 @@ class ModelsHandler:
|
||||
("doubao", "ark_api_key", const.DOUBAO_SEED_2_PRO),
|
||||
("dashscope", "dashscope_api_key", const.QWEN37_PLUS),
|
||||
("claudeAPI", "claude_api_key", const.CLAUDE_SONNET_5),
|
||||
("gemini", "gemini_api_key", const.GEMINI_37_FLASH),
|
||||
("gemini", "gemini_api_key", const.GEMINI_38_FLASH),
|
||||
("qianfan", "qianfan_api_key", const.ERNIE_45_TURBO_VL),
|
||||
("zhipu", "zhipu_ai_api_key", const.GLM_5V_TURBO),
|
||||
("minimax", "minimax_api_key", const.MINIMAX_TEXT_01),
|
||||
|
||||
+3
-1
@@ -56,6 +56,7 @@ GEMINI_31_FLASH_LITE_PRE = "gemini-3.1-flash-lite-preview" # Gemini 3.1 Flash L
|
||||
GEMINI_35_FLASH = "gemini-3.5-flash" # Gemini 3.5 Flash
|
||||
GEMINI_36_FLASH = "gemini-3.6-flash" # Gemini 3.6 Flash
|
||||
GEMINI_37_FLASH = "gemini-3.7-flash" # Gemini 3.7 Flash - Agent recommended model (default)
|
||||
GEMINI_38_FLASH = "gemini-3.8-flash" # Gemini 3.8 Flash - latest flagship
|
||||
|
||||
# OpenAI
|
||||
GPT35 = "gpt-3.5-turbo"
|
||||
@@ -85,6 +86,7 @@ GPT_54_MINI = "gpt-5.4-mini"
|
||||
GPT_54_NANO = "gpt-5.4-nano"
|
||||
GPT_55 = "gpt-5.5" # GPT-5.5 - top-tier (expensive), not default
|
||||
GPT_56_LUNA = "gpt-5.6-luna" # GPT-5.6 Luna - default flagship model for GPT
|
||||
GPT_6_ASTRA = "gpt-6-astra" # GPT-6 Astra - most intelligent flagship (Responses API, higher cost)
|
||||
GPT_56_TERRA = "gpt-5.6-terra" # GPT-5.6 Terra
|
||||
GPT_56_SOL = "gpt-5.6-sol" # GPT-5.6 Sol - highest intelligence, higher latency
|
||||
O1 = "o1-preview"
|
||||
@@ -218,7 +220,7 @@ MODEL_LIST = [
|
||||
"claude", "claude-3-haiku", "claude-3-sonnet", "claude-3-opus", "claude-3.5-sonnet",
|
||||
|
||||
# Gemini
|
||||
GEMINI_37_FLASH, GEMINI_36_FLASH, GEMINI_35_FLASH, GEMINI_31_FLASH_LITE_PRE, GEMINI_31_PRO_PRE, GEMINI_3_PRO_PRE, GEMINI_3_FLASH_PRE, GEMINI_25_PRO_PRE, GEMINI_25_FLASH_PRE,
|
||||
GEMINI_38_FLASH, GEMINI_37_FLASH, GEMINI_36_FLASH, GEMINI_35_FLASH, GEMINI_31_FLASH_LITE_PRE, GEMINI_31_PRO_PRE, GEMINI_3_PRO_PRE, GEMINI_3_FLASH_PRE, GEMINI_25_PRO_PRE, GEMINI_25_FLASH_PRE,
|
||||
GEMINI_20_FLASH, GEMINI_20_flash_exp, GEMINI_15_PRO, GEMINI_15_flash, GEMINI_PRO, GEMINI,
|
||||
|
||||
# OpenAI
|
||||
|
||||
+2
-2
@@ -132,8 +132,8 @@ CowAgent は主要な LLM プロバイダーすべてに対応しています。
|
||||
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||
| [DeepSeek](https://docs.cowagent.ai/ja/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
|
||||
| [Claude](https://docs.cowagent.ai/ja/models/claude) | claude-opus-5 / fable-5.1 | ✅ | ✅ | | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/ja/models/openai) | gpt-5.6 シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/ja/models/gemini) | gemini-3.7-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/ja/models/openai) | gpt-6-astra / gpt-5.6 シリーズ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/ja/models/gemini) | gemini-3.8-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [MiniMax](https://docs.cowagent.ai/ja/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
|
||||
| [GLM](https://docs.cowagent.ai/ja/models/glm) | glm-5.3-flash、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
|
||||
| [Qwen](https://docs.cowagent.ai/ja/models/qwen) | qwen3.8-flash | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -22,7 +22,7 @@ OpenAI は最も広範な機能をカバーするベンダーで、テキスト
|
||||
|
||||
| パラメータ | 説明 |
|
||||
| --- | --- |
|
||||
| `model` | OpenAI API の [model パラメータ](https://platform.openai.com/docs/models) と同じです。`gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` シリーズ、`gpt-4.1` などをサポート。Agent モードのデフォルトは `gpt-5.6-luna`、コストパフォーマンスを重視する場合は `gpt-5.4` に変更可能 |
|
||||
| `model` | OpenAI API の [model パラメータ](https://platform.openai.com/docs/models) と同じです。`gpt-5.6-luna`、`gpt-6-astra`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` シリーズ、`gpt-4.1` などをサポート。Agent モードのデフォルトは `gpt-5.6-luna`、`gpt-6-astra` は最強のフラッグシップ(最高の知能・高コスト、ツール呼び出しは Responses API 経由)、コストパフォーマンスを重視する場合は `gpt-5.4` に変更可能 |
|
||||
| `open_ai_api_key` | [OpenAI プラットフォーム](https://platform.openai.com/api-keys) で作成 |
|
||||
| `open_ai_api_base` | 任意。サードパーティのプロキシに接続するために変更可能 |
|
||||
| `bot_type` | OpenAI 公式モデルを使用する場合は不要。互換プロトコルでベンダーモデルに接続する場合は `openai` に設定 |
|
||||
|
||||
@@ -64,7 +64,7 @@ Thanks @chimyves (#3087)、@sufan721 (#3079)
|
||||
|
||||
## 🤖 モデル機能
|
||||
|
||||
- **新モデル追加**:`claude-fable-5-1`、`qwen3.8-flash`、`glm-5.3-flash`、および視覚モデル `deepseek-v4-flash-vision-exp` に対応しました(Thanks @a1094174619 #3094)。
|
||||
- **新モデル追加**:フラッグシップモデル `gpt-6-astra`、`claude-fable-5-1`、`qwen3.8-flash`、`glm-5.3-flash`、`gemini-3.8-flash`、および視覚モデル `deepseek-v4-flash-vision-exp` に対応しました(Thanks @a1094174619 #3094)。
|
||||
- **モデルのフォールバック**:「モデル設定 - メインモデル」でフォールバックモデルを設定でき、メインモデルが失敗した際に自動で切り替わるため、単一プロバイダーの障害による中断を防げます。Thanks @alanyz106 (#3099)
|
||||
- **検索プロバイダーを 6 種追加**:
|
||||
- **AnySearch**(匿名モードに対応)。Thanks @WangPan59 (#3083)
|
||||
|
||||
@@ -22,7 +22,7 @@ OpenAI offers the most complete coverage and can simultaneously serve text chat,
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `model` | Same as OpenAI's [model parameter](https://platform.openai.com/docs/models); supports `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, the `gpt-5` series, `gpt-4.1`, etc. Agent mode defaults to `gpt-5.6-luna`; use `gpt-5.4` for better cost-efficiency |
|
||||
| `model` | Same as OpenAI's [model parameter](https://platform.openai.com/docs/models); supports `gpt-5.6-luna`, `gpt-6-astra`, `gpt-5.6-terra`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, the `gpt-5` series, `gpt-4.1`, etc. Agent mode defaults to `gpt-5.6-luna`; `gpt-6-astra` is the top flagship (most intelligent, higher cost, tool calling goes through the Responses API); use `gpt-5.4` for better cost-efficiency |
|
||||
| `open_ai_api_key` | Create one on the [OpenAI Platform](https://platform.openai.com/api-keys) |
|
||||
| `open_ai_api_base` | Optional; change it to access a third-party proxy |
|
||||
| `bot_type` | Not required when using OpenAI's official models; set to `openai` when accessing other providers via the compatible protocol |
|
||||
|
||||
@@ -64,7 +64,7 @@ Thanks @chimyves (#3087), @sufan721 (#3079)
|
||||
|
||||
## 🤖 Model Capabilities
|
||||
|
||||
- **New models**: added `claude-fable-5-1`, `qwen3.8-flash`, `glm-5.3-flash`, and the vision model `deepseek-v4-flash-vision-exp` (Thanks @a1094174619 #3094).
|
||||
- **New models**: added the flagship `gpt-6-astra`, `claude-fable-5-1`, `qwen3.8-flash`, `glm-5.3-flash`, `gemini-3.8-flash`, and the vision model `deepseek-v4-flash-vision-exp` (Thanks @a1094174619 #3094).
|
||||
- **Model fallback**: configure a fallback model under Model Config > Primary Model, which takes over automatically when the primary model fails, preventing interruptions from a single provider outage. Thanks @alanyz106 (#3099)
|
||||
- **Six new search providers**:
|
||||
- **AnySearch** (supports anonymous mode). Thanks @WangPan59 (#3083)
|
||||
|
||||
@@ -132,8 +132,8 @@ CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像
|
||||
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||
| [DeepSeek](https://docs.cowagent.ai/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
|
||||
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-opus-5 / fable-5.1 | ✅ | ✅ | | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.7-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-6-astra / gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.8-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [MiniMax](https://docs.cowagent.ai/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
|
||||
| [智譜 GLM](https://docs.cowagent.ai/zh/models/glm) | glm-5.3-flash、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
|
||||
| [通義千問](https://docs.cowagent.ai/zh/models/qwen) | qwen3.8-flash | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
+2
-2
@@ -132,8 +132,8 @@ CowAgent 支持国内外主流厂商的大语言模型。**文本对话、图像
|
||||
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
|
||||
| [DeepSeek](https://docs.cowagent.ai/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
|
||||
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-opus-5 / fable-5.1 | ✅ | ✅ | | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.7-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-6-astra / gpt-5.6 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.8-flash | ✅ | ✅ | ✅ | | | |
|
||||
| [MiniMax](https://docs.cowagent.ai/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
|
||||
| [智谱 GLM](https://docs.cowagent.ai/zh/models/glm) | glm-5.3-flash、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
|
||||
| [通义千问](https://docs.cowagent.ai/zh/models/qwen) | qwen3.8-flash | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -22,7 +22,7 @@ OpenAI 是覆盖最完整的厂商,可同时承担文本对话、视觉理解
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `model` | 与 OpenAI 接口的 [model 参数](https://platform.openai.com/docs/models) 一致,支持 `gpt-5.6-luna`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` 系列、`gpt-4.1` 等;Agent 模式默认 `gpt-5.6-luna`,追求性价比可改为 `gpt-5.4` |
|
||||
| `model` | 与 OpenAI 接口的 [model 参数](https://platform.openai.com/docs/models) 一致,支持 `gpt-5.6-luna`、`gpt-6-astra`、`gpt-5.6-terra`、`gpt-5.6-sol`、`gpt-5.5`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.4-nano`、`gpt-5` 系列、`gpt-4.1` 等;Agent 模式默认 `gpt-5.6-luna`,`gpt-6-astra` 为最强旗舰(智能最高、成本较高,工具调用走 Responses 接口),追求性价比可改为 `gpt-5.4` |
|
||||
| `open_ai_api_key` | 在 [OpenAI 平台](https://platform.openai.com/api-keys) 创建 |
|
||||
| `open_ai_api_base` | 可选,修改可接入第三方代理 |
|
||||
| `bot_type` | 使用 OpenAI 官方模型时无需填写;通过兼容协议接入厂商模型时需设为 `openai` |
|
||||
|
||||
@@ -64,7 +64,7 @@ Thanks @chimyves (#3087)、@sufan721 (#3079)
|
||||
|
||||
## 🤖 模型能力
|
||||
|
||||
- **新增模型**:接入 `claude-fable-5-1`、`qwen3.8-flash`、`glm-5.3-flash`,以及视觉模型 `deepseek-v4-flash-vision-exp`(Thanks @a1094174619 #3094)。
|
||||
- **新增模型**:接入 `gpt-6-astra`、`claude-fable-5-1`、`qwen3.8-flash`、`glm-5.3-flash`、`gemini-3.8-flash`,以及视觉模型 `deepseek-v4-flash-vision-exp`(Thanks @a1094174619 #3094)。
|
||||
- **模型故障回退**:在「模型配置-主模型」处可配置「兜底模型」,当主模型失败时自动切换,避免因单一厂商故障而中断。Thanks @alanyz106 (#3099)
|
||||
- **新增 6 个搜索工具厂商**:
|
||||
- **AnySearch**(支持匿名模式)。Thanks @WangPan59 (#3083)
|
||||
|
||||
@@ -172,6 +172,45 @@ class OpenAIHTTPClient:
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
def responses(
|
||||
self,
|
||||
*,
|
||||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None,
|
||||
timeout: Optional[float] = None,
|
||||
proxy: Optional[str] = None,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
extra_query: Optional[Dict[str, str]] = None,
|
||||
path: str = "/responses",
|
||||
stream: bool = False,
|
||||
**payload,
|
||||
):
|
||||
"""POST /responses (OpenAI Responses API).
|
||||
|
||||
Required for models like gpt-6-astra whose tool calling is only
|
||||
supported on Responses (Chat Completions cannot do tool calling with a
|
||||
non-``none`` reasoning effort, and Astra does not support ``none``).
|
||||
|
||||
Non-streaming returns the raw Responses ``dict``. Streaming returns a
|
||||
generator yielding parsed SSE event ``dict`` objects (each carries a
|
||||
``type`` such as ``response.output_text.delta``); the caller is
|
||||
responsible for translating them into whatever shape it needs. On
|
||||
error during streaming a single ``{"error": ..., "status_code": ...}``
|
||||
chunk is yielded, matching :meth:`chat_completions`.
|
||||
"""
|
||||
payload["stream"] = stream
|
||||
return self._request(
|
||||
path=path,
|
||||
payload=payload,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
timeout=timeout,
|
||||
proxy=proxy,
|
||||
extra_headers=extra_headers,
|
||||
extra_query=extra_query,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
def completions(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
# encoding:utf-8
|
||||
|
||||
"""
|
||||
OpenAI Responses API adapter.
|
||||
|
||||
Some models (e.g. ``gpt-6-astra``) only support tool calling through the
|
||||
Responses API: on Chat Completions, tool calling is rejected unless the
|
||||
reasoning effort is ``none``, and Astra does not support ``none``. To keep the
|
||||
rest of the agent unchanged we translate at the edges:
|
||||
|
||||
- request: Chat-Completions-shaped ``messages`` / ``tools`` (the shape the
|
||||
agent already produces) -> Responses ``input`` items / tools.
|
||||
- response: Responses output / SSE events -> Chat-Completions-shaped ``dict``
|
||||
responses and stream chunks, so ``agent/protocol/agent_stream.py``
|
||||
consumes them exactly as it consumes ``/chat/completions`` output:
|
||||
chunk["choices"][0]["delta"]["content" | "tool_calls" | ...]
|
||||
chunk["choices"][0]["finish_reason"]
|
||||
chunk["usage"] = {prompt_tokens, completion_tokens, total_tokens}
|
||||
|
||||
This module has no dependency on the ``openai`` SDK — it only shapes plain
|
||||
dicts consumed/produced by ``OpenAIHTTPClient``.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
|
||||
from common.log import logger
|
||||
|
||||
|
||||
# Models whose tool calling requires the Responses API. Matched by prefix so
|
||||
# dated snapshots (gpt-6-astra-2026-..) and future gpt-6 variants are covered.
|
||||
_RESPONSES_ONLY_PREFIXES = ("gpt-6",)
|
||||
|
||||
# gpt-6-astra does not support reasoning effort "none"/"minimal"; clamp to the
|
||||
# lowest it accepts so a config carried over from gpt-5.x does not 400.
|
||||
_MIN_EFFORT_FALLBACK = "low"
|
||||
_UNSUPPORTED_EFFORTS = {"none", "minimal"}
|
||||
|
||||
|
||||
def is_responses_only_model(model_name: str) -> bool:
|
||||
"""Whether the model must use the Responses API for tool calling."""
|
||||
if not model_name or not isinstance(model_name, str):
|
||||
return False
|
||||
name = model_name.strip().lower()
|
||||
return name.startswith(_RESPONSES_ONLY_PREFIXES)
|
||||
|
||||
|
||||
def normalize_effort(effort: Optional[str]) -> Optional[str]:
|
||||
"""Map a reasoning effort to one Responses-only models accept."""
|
||||
if not effort:
|
||||
return None
|
||||
value = str(effort).strip().lower()
|
||||
if value in _UNSUPPORTED_EFFORTS:
|
||||
return _MIN_EFFORT_FALLBACK
|
||||
return value
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Request translation: Chat Completions -> Responses
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _content_to_input_parts(content: Any, role: str) -> Any:
|
||||
"""Convert a Chat-Completions message content into Responses content.
|
||||
|
||||
Strings pass through. A list of blocks is converted to Responses input
|
||||
parts (``input_text`` / ``input_image``); assistant text uses
|
||||
``output_text`` per the Responses schema.
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if not isinstance(content, list):
|
||||
return "" if content is None else str(content)
|
||||
|
||||
text_type = "output_text" if role == "assistant" else "input_text"
|
||||
parts: List[Dict[str, Any]] = []
|
||||
for item in content:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
itype = item.get("type")
|
||||
if itype in ("text", "input_text", "output_text"):
|
||||
text = item.get("text") or ""
|
||||
if text:
|
||||
parts.append({"type": text_type, "text": text})
|
||||
elif itype in ("image_url", "input_image") and role == "user":
|
||||
image = item.get("image_url")
|
||||
url = image.get("url") if isinstance(image, dict) else image
|
||||
if url:
|
||||
parts.append({"type": "input_image", "image_url": url})
|
||||
return parts
|
||||
|
||||
|
||||
def messages_to_input(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Convert Chat-Completions messages into Responses ``input`` items.
|
||||
|
||||
Handles the tool-calling round trip:
|
||||
- assistant ``tool_calls`` -> ``function_call`` items (internally tagged).
|
||||
- ``role: tool`` results -> ``function_call_output`` items linked by
|
||||
``call_id``.
|
||||
Plain system/user/assistant text becomes ``message`` items.
|
||||
"""
|
||||
input_items: List[Dict[str, Any]] = []
|
||||
for msg in messages or []:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
role = msg.get("role")
|
||||
|
||||
if role == "tool":
|
||||
input_items.append({
|
||||
"type": "function_call_output",
|
||||
"call_id": msg.get("tool_call_id") or "",
|
||||
"output": _tool_output_text(msg.get("content")),
|
||||
})
|
||||
continue
|
||||
|
||||
if role == "assistant" and msg.get("tool_calls"):
|
||||
content = msg.get("content")
|
||||
if content:
|
||||
input_items.append({
|
||||
"role": "assistant",
|
||||
"content": _content_to_input_parts(content, "assistant"),
|
||||
})
|
||||
for tc in msg.get("tool_calls") or []:
|
||||
fn = tc.get("function") or {}
|
||||
input_items.append({
|
||||
"type": "function_call",
|
||||
"call_id": tc.get("id") or "",
|
||||
"name": fn.get("name") or "",
|
||||
"arguments": fn.get("arguments") or "{}",
|
||||
})
|
||||
continue
|
||||
|
||||
role = role if role in ("system", "developer", "user", "assistant") else "user"
|
||||
content = _content_to_input_parts(msg.get("content"), role)
|
||||
if content == "" or content == []:
|
||||
continue
|
||||
input_items.append({"role": role, "content": content})
|
||||
return input_items
|
||||
|
||||
|
||||
def _tool_output_text(content: Any) -> str:
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return " ".join(
|
||||
str(b.get("text", "")) for b in content
|
||||
if isinstance(b, dict) and b.get("type") in ("text", "output_text", None)
|
||||
)
|
||||
if content is None:
|
||||
return ""
|
||||
return json.dumps(content, ensure_ascii=False)
|
||||
|
||||
|
||||
def tools_to_responses(tools: Optional[List[Dict[str, Any]]]) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Convert Chat-Completions tools (externally tagged) into Responses tools
|
||||
(internally tagged): ``{type:function, function:{name,...}}`` ->
|
||||
``{type:function, name, description, parameters}``."""
|
||||
if not tools:
|
||||
return None
|
||||
converted: List[Dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
fn = tool.get("function")
|
||||
if isinstance(fn, dict):
|
||||
entry = {
|
||||
"type": "function",
|
||||
"name": fn.get("name"),
|
||||
"description": fn.get("description"),
|
||||
"parameters": fn.get("parameters") or {},
|
||||
}
|
||||
else:
|
||||
# Already internally tagged / Claude-shaped fallback.
|
||||
entry = {
|
||||
"type": "function",
|
||||
"name": tool.get("name"),
|
||||
"description": tool.get("description"),
|
||||
"parameters": tool.get("parameters") or tool.get("input_schema") or {},
|
||||
}
|
||||
converted.append({k: v for k, v in entry.items() if v is not None})
|
||||
return converted or None
|
||||
|
||||
|
||||
def build_responses_payload(
|
||||
*,
|
||||
model: str,
|
||||
messages: List[Dict[str, Any]],
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Any] = None,
|
||||
max_output_tokens: Optional[int] = None,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
response_format: Optional[Dict[str, Any]] = None,
|
||||
store: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Assemble a Responses API request body.
|
||||
|
||||
Deliberately omits ``temperature`` / ``top_p`` / penalties: Responses-only
|
||||
reasoning models reject them.
|
||||
"""
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model,
|
||||
"input": messages_to_input(messages),
|
||||
"store": store,
|
||||
}
|
||||
r_tools = tools_to_responses(tools)
|
||||
if r_tools:
|
||||
payload["tools"] = r_tools
|
||||
payload["tool_choice"] = tool_choice or "auto"
|
||||
if max_output_tokens:
|
||||
payload["max_output_tokens"] = max_output_tokens
|
||||
effort = normalize_effort(reasoning_effort)
|
||||
if effort:
|
||||
payload["reasoning"] = {"effort": effort}
|
||||
if response_format and response_format.get("type") == "json_object":
|
||||
payload["text"] = {"format": {"type": "json_object"}}
|
||||
return payload
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Response translation: Responses -> Chat Completions
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _cc_usage(usage: Any) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(usage, dict):
|
||||
return None
|
||||
return {
|
||||
"prompt_tokens": usage.get("input_tokens") or 0,
|
||||
"completion_tokens": usage.get("output_tokens") or 0,
|
||||
"total_tokens": usage.get("total_tokens") or 0,
|
||||
}
|
||||
|
||||
|
||||
def _status_to_finish_reason(response: Dict[str, Any], has_tool_calls: bool) -> str:
|
||||
status = response.get("status")
|
||||
if has_tool_calls:
|
||||
return "tool_calls"
|
||||
if status == "incomplete":
|
||||
details = response.get("incomplete_details") or {}
|
||||
if details.get("reason") == "max_output_tokens":
|
||||
return "length"
|
||||
return "stop"
|
||||
|
||||
|
||||
def responses_to_chat_completion(response: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Convert a non-streaming Responses object into a Chat-Completions dict."""
|
||||
text_parts: List[str] = []
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
for item in response.get("output") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
itype = item.get("type")
|
||||
if itype == "message":
|
||||
for part in item.get("content") or []:
|
||||
if isinstance(part, dict) and part.get("type") == "output_text":
|
||||
text_parts.append(str(part.get("text") or ""))
|
||||
elif itype == "function_call":
|
||||
tool_calls.append({
|
||||
"id": item.get("call_id") or item.get("id") or "",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": item.get("name") or "",
|
||||
"arguments": item.get("arguments") or "{}",
|
||||
},
|
||||
})
|
||||
|
||||
message: Dict[str, Any] = {
|
||||
"role": "assistant",
|
||||
"content": "".join(text_parts) or None,
|
||||
}
|
||||
if tool_calls:
|
||||
message["tool_calls"] = tool_calls
|
||||
|
||||
return {
|
||||
"id": response.get("id"),
|
||||
"object": "chat.completion",
|
||||
"model": response.get("model"),
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": message,
|
||||
"finish_reason": _status_to_finish_reason(response, bool(tool_calls)),
|
||||
}],
|
||||
"usage": _cc_usage(response.get("usage")) or {
|
||||
"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _text_chunk(model: Optional[str], delta: Dict[str, Any],
|
||||
finish_reason: Optional[str] = None) -> Dict[str, Any]:
|
||||
return {
|
||||
"object": "chat.completion.chunk",
|
||||
"model": model,
|
||||
"choices": [{"index": 0, "delta": delta, "finish_reason": finish_reason}],
|
||||
}
|
||||
|
||||
|
||||
def responses_stream_to_chat_chunks(
|
||||
events: Generator[Dict[str, Any], None, None],
|
||||
model: Optional[str] = None,
|
||||
) -> Generator[Dict[str, Any], None, None]:
|
||||
"""Translate a Responses SSE event stream into Chat-Completions chunks.
|
||||
|
||||
Emits the shapes ``agent_stream`` already handles:
|
||||
- text: delta.content
|
||||
- reasoning: delta.reasoning_content
|
||||
- tool calls: delta.tool_calls = [{index, id, function:{name, arguments}}]
|
||||
- final: finish_reason + top-level usage
|
||||
- errors: {"error": {...}, "message": ..., "status_code": ...}
|
||||
|
||||
Function-call arguments are streamed by Responses as
|
||||
``response.function_call_arguments.delta`` events keyed by ``output_index``;
|
||||
we map each output index to a stable ``tool_calls`` index and forward the
|
||||
id/name once (on the ``output_item.added`` event) then argument deltas.
|
||||
"""
|
||||
# output_index -> tool_calls stream index
|
||||
tool_index_map: Dict[int, int] = {}
|
||||
next_tool_index = 0
|
||||
saw_tool_call = False
|
||||
finished = False
|
||||
|
||||
for event in events:
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
|
||||
# Error chunk forwarded by OpenAIHTTPClient._stream_chat.
|
||||
if event.get("error") and "type" not in event:
|
||||
yield event
|
||||
return
|
||||
|
||||
etype = event.get("type")
|
||||
|
||||
if etype == "response.output_text.delta":
|
||||
delta = event.get("delta")
|
||||
if delta:
|
||||
yield _text_chunk(model, {"content": str(delta)})
|
||||
continue
|
||||
|
||||
if etype in ("response.reasoning_summary_text.delta", "response.reasoning_text.delta"):
|
||||
delta = event.get("delta")
|
||||
if delta:
|
||||
yield _text_chunk(model, {"reasoning_content": str(delta)})
|
||||
continue
|
||||
|
||||
if etype == "response.output_item.added":
|
||||
item = event.get("item") or {}
|
||||
if item.get("type") == "function_call":
|
||||
out_idx = event.get("output_index", 0)
|
||||
if out_idx not in tool_index_map:
|
||||
tool_index_map[out_idx] = next_tool_index
|
||||
next_tool_index += 1
|
||||
saw_tool_call = True
|
||||
yield _text_chunk(model, {"tool_calls": [{
|
||||
"index": tool_index_map[out_idx],
|
||||
"id": item.get("call_id") or item.get("id") or "",
|
||||
"type": "function",
|
||||
"function": {"name": item.get("name") or "", "arguments": ""},
|
||||
}]})
|
||||
continue
|
||||
|
||||
if etype == "response.function_call_arguments.delta":
|
||||
out_idx = event.get("output_index", 0)
|
||||
if out_idx not in tool_index_map:
|
||||
tool_index_map[out_idx] = next_tool_index
|
||||
next_tool_index += 1
|
||||
saw_tool_call = True
|
||||
delta = event.get("delta")
|
||||
if delta:
|
||||
yield _text_chunk(model, {"tool_calls": [{
|
||||
"index": tool_index_map[out_idx],
|
||||
"function": {"arguments": str(delta)},
|
||||
}]})
|
||||
continue
|
||||
|
||||
if etype in ("response.completed", "response.incomplete"):
|
||||
response = event.get("response") or {}
|
||||
finish_reason = _status_to_finish_reason(response, saw_tool_call)
|
||||
usage = _cc_usage(response.get("usage"))
|
||||
final_chunk = _text_chunk(model or response.get("model"), {}, finish_reason)
|
||||
if usage:
|
||||
final_chunk["usage"] = usage
|
||||
yield final_chunk
|
||||
finished = True
|
||||
continue
|
||||
|
||||
if etype in ("response.failed", "error"):
|
||||
response = event.get("response") or {}
|
||||
err = event.get("error") or response.get("error") or {}
|
||||
message = err.get("message") if isinstance(err, dict) else str(err)
|
||||
code = err.get("code") if isinstance(err, dict) else ""
|
||||
logger.error(f"[Responses] stream error: {message} (code={code})")
|
||||
yield {
|
||||
"error": {"message": message or "Responses stream error",
|
||||
"code": code or "", "type": err.get("type", "") if isinstance(err, dict) else ""},
|
||||
"message": message or "Responses stream error",
|
||||
"status_code": 500,
|
||||
}
|
||||
return
|
||||
|
||||
# Some proxies omit an explicit completed event; make sure the agent sees a
|
||||
# terminal finish_reason so it doesn't treat the turn as truncated.
|
||||
if not finished:
|
||||
yield _text_chunk(model, {}, "tool_calls" if saw_tool_call else "stop")
|
||||
@@ -108,6 +108,21 @@ class OpenAICompatibleBot:
|
||||
|
||||
# Build request parameters
|
||||
model_name = kwargs.get("model", api_config.get('model', 'gpt-5.4'))
|
||||
|
||||
# Models like gpt-6-astra only support tool calling via the Responses
|
||||
# API (Chat Completions rejects tools with a non-"none" reasoning
|
||||
# effort, and Astra has no "none"). Route them through Responses and
|
||||
# translate the result back to Chat-Completions shape.
|
||||
from models.openai import responses_adapter as responses_adapter
|
||||
if responses_adapter.is_responses_only_model(model_name):
|
||||
return self._call_with_tools_responses(
|
||||
model_name=model_name,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
stream=stream,
|
||||
api_config=api_config,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
request_params = {
|
||||
"model": model_name,
|
||||
"messages": messages,
|
||||
@@ -174,6 +189,69 @@ class OpenAICompatibleBot:
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _call_with_tools_responses(self, *, model_name, messages, tools, stream,
|
||||
api_config, kwargs):
|
||||
"""Tool-calling path for Responses-only models (e.g. gpt-6-astra).
|
||||
|
||||
Builds a Responses request from the already-converted OpenAI-shaped
|
||||
``messages`` / ``tools`` and translates the Responses output (sync) or
|
||||
SSE events (stream) back into Chat-Completions shape so the agent
|
||||
consumes it identically to the ``/chat/completions`` path.
|
||||
"""
|
||||
from models.openai import responses_adapter
|
||||
|
||||
payload = responses_adapter.build_responses_payload(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
tool_choice=kwargs.get("tool_choice", "auto") if tools else None,
|
||||
max_output_tokens=kwargs.get("max_tokens"),
|
||||
reasoning_effort=kwargs.get("reasoning_effort"),
|
||||
response_format=kwargs.get("response_format"),
|
||||
)
|
||||
api_key = api_config.get("api_key")
|
||||
api_base = api_config.get("api_base")
|
||||
timeout = kwargs.get("request_timeout") or kwargs.get("timeout")
|
||||
|
||||
if stream:
|
||||
return self._handle_responses_stream(payload, api_key, api_base, timeout, model_name)
|
||||
return self._handle_responses_sync(payload, api_key, api_base, timeout)
|
||||
|
||||
def _handle_responses_sync(self, payload, api_key, api_base, timeout):
|
||||
from models.openai import responses_adapter
|
||||
try:
|
||||
client = self._get_http_client()
|
||||
response = client.responses(
|
||||
api_key=api_key, api_base=api_base, timeout=timeout,
|
||||
stream=False, **payload,
|
||||
)
|
||||
return responses_adapter.responses_to_chat_completion(response)
|
||||
except OpenAIHTTPError as e:
|
||||
logger.error(f"[{self.__class__.__name__}] responses sync error: "
|
||||
f"HTTP {e.status_code}: {e.message}")
|
||||
return {"error": True, "message": e.message, "status_code": e.status_code or 500}
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.__class__.__name__}] responses sync error: {e}")
|
||||
return {"error": True, "message": str(e), "status_code": 500}
|
||||
|
||||
def _handle_responses_stream(self, payload, api_key, api_base, timeout, model_name):
|
||||
from models.openai import responses_adapter
|
||||
try:
|
||||
client = self._get_http_client()
|
||||
events = client.responses(
|
||||
api_key=api_key, api_base=api_base, timeout=timeout,
|
||||
stream=True, **payload,
|
||||
)
|
||||
for chunk in responses_adapter.responses_stream_to_chat_chunks(events, model_name):
|
||||
yield chunk
|
||||
except OpenAIHTTPError as e:
|
||||
logger.error(f"[{self.__class__.__name__}] responses stream error: "
|
||||
f"HTTP {e.status_code}: {e.message}")
|
||||
yield {"error": True, "message": e.message, "status_code": e.status_code or 500}
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.__class__.__name__}] responses stream error: {e}")
|
||||
yield {"error": True, "message": str(e), "status_code": 500}
|
||||
|
||||
def _get_http_client(self) -> OpenAIHTTPClient:
|
||||
"""Build an HTTP client honoring the global proxy config.
|
||||
|
||||
|
||||
Reference in new issue
Block a user