Skip to content

Commit 28cdafb

Browse files
💄 style: support web_search tool for MiniMax & Zhipu (lobehub#7980)
* 💄 style: support web_search for MiniMax * ♻️ refactor: refactor zhipu `web_search` tools * 🐛 fix: fix live search citations * ♻️ refactor: refactor Minimax citations * 🐛 fix: fix ci error * 🐛 fix: fix ci error * 💄 style: support OpenAI Search model citations --------- Co-authored-by: Arvin Xu <[email protected]>
1 parent 4fc885c commit 28cdafb

File tree

6 files changed

+155
-17
lines changed

6 files changed

+155
-17
lines changed

src/config/aiModels/minimax.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const minimaxChatModels: AIChatModelCard[] = [
44
{
55
abilities: {
66
functionCall: true,
7+
search: true,
78
vision: true,
89
},
910
contextWindowTokens: 1_000_192,
@@ -19,11 +20,15 @@ const minimaxChatModels: AIChatModelCard[] = [
1920
output: 8,
2021
},
2122
releasedAt: '2025-01-15',
23+
settings: {
24+
searchImpl: 'params',
25+
},
2226
type: 'chat',
2327
},
2428
{
2529
abilities: {
2630
functionCall: true,
31+
search: true,
2732
vision: true,
2833
},
2934
contextWindowTokens: 245_760,
@@ -37,6 +42,9 @@ const minimaxChatModels: AIChatModelCard[] = [
3742
input: 1,
3843
output: 1,
3944
},
45+
settings: {
46+
searchImpl: 'params',
47+
},
4048
type: 'chat',
4149
},
4250
{

src/config/aiModels/openai.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,26 @@ export const openaiChatModels: AIChatModelCard[] = [
236236
releasedAt: '2024-07-18',
237237
type: 'chat',
238238
},
239+
{
240+
abilities: {
241+
search: true,
242+
},
243+
contextWindowTokens: 128_000,
244+
description:
245+
'GPT-4o mini 搜索预览版是一个专门训练用于理解和执行网页搜索查询的模型,使用的是 Chat Completions API。除了令牌费用之外,网页搜索查询还会按每次工具调用收取费用。',
246+
displayName: 'GPT-4o mini Search Preview',
247+
id: 'gpt-4o-mini-search-preview',
248+
maxOutput: 16_384,
249+
pricing: {
250+
input: 0.15,
251+
output: 0.6,
252+
},
253+
releasedAt: '2025-03-11',
254+
settings: {
255+
searchImpl: 'internal',
256+
},
257+
type: 'chat',
258+
},
239259
{
240260
abilities: {
241261
functionCall: true,
@@ -244,14 +264,34 @@ export const openaiChatModels: AIChatModelCard[] = [
244264
contextWindowTokens: 128_000,
245265
description:
246266
'ChatGPT-4o 是一款动态模型,实时更新以保持当前最新版本。它结合了强大的语言理解与生成能力,适合于大规模应用场景,包括客户服务、教育和技术支持。',
247-
displayName: 'GPT-4o 1120',
248-
id: 'gpt-4o-2024-11-20',
267+
displayName: 'GPT-4o',
268+
id: 'gpt-4o',
249269
pricing: {
250270
cachedInput: 1.25,
251271
input: 2.5,
252272
output: 10,
253273
},
254-
releasedAt: '2024-11-20',
274+
releasedAt: '2024-05-13',
275+
type: 'chat',
276+
},
277+
{
278+
abilities: {
279+
search: true,
280+
},
281+
contextWindowTokens: 128_000,
282+
description:
283+
'GPT-4o 搜索预览版是一个专门训练用于理解和执行网页搜索查询的模型,使用的是 Chat Completions API。除了令牌费用之外,网页搜索查询还会按每次工具调用收取费用。',
284+
displayName: 'GPT-4o Search Preview',
285+
id: 'gpt-4o-search-preview',
286+
maxOutput: 16_384,
287+
pricing: {
288+
input: 2.5,
289+
output: 10,
290+
},
291+
releasedAt: '2025-03-11',
292+
settings: {
293+
searchImpl: 'internal',
294+
},
255295
type: 'chat',
256296
},
257297
{
@@ -262,14 +302,14 @@ export const openaiChatModels: AIChatModelCard[] = [
262302
contextWindowTokens: 128_000,
263303
description:
264304
'ChatGPT-4o 是一款动态模型,实时更新以保持当前最新版本。它结合了强大的语言理解与生成能力,适合于大规模应用场景,包括客户服务、教育和技术支持。',
265-
displayName: 'GPT-4o',
266-
id: 'gpt-4o',
305+
displayName: 'GPT-4o 1120',
306+
id: 'gpt-4o-2024-11-20',
267307
pricing: {
268308
cachedInput: 1.25,
269309
input: 2.5,
270310
output: 10,
271311
},
272-
releasedAt: '2024-05-13',
312+
releasedAt: '2024-11-20',
273313
type: 'chat',
274314
},
275315
{

src/libs/model-runtime/minimax/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ export const LobeMinimaxAI = LobeOpenAICompatibleFactory({
1212
baseURL: 'https://api.minimax.chat/v1',
1313
chatCompletion: {
1414
handlePayload: (payload) => {
15-
const { max_tokens, temperature, top_p, ...params } = payload;
15+
const { enabledSearch, max_tokens, temperature, tools, top_p, ...params } = payload;
16+
17+
const minimaxTools = enabledSearch
18+
? [
19+
...(tools || []),
20+
{
21+
type: 'web_search',
22+
},
23+
]
24+
: tools;
1625

1726
return {
1827
...params,
1928
frequency_penalty: undefined,
2029
max_tokens: max_tokens !== undefined ? max_tokens : getMinimaxMaxOutputs(payload.model),
2130
presence_penalty: undefined,
2231
temperature: temperature === undefined || temperature <= 0 ? undefined : temperature / 2,
32+
tools: minimaxTools,
2333
top_p: top_p !== undefined && top_p > 0 && top_p <= 1 ? top_p : undefined,
2434
} as any;
2535
},

src/libs/model-runtime/openai/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@ export const LobeOpenAI = LobeOpenAICompatibleFactory({
2121
}
2222

2323
if (model.includes('-search-')) {
24+
const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
25+
2426
return {
2527
...payload,
2628
frequency_penalty: undefined,
2729
presence_penalty: undefined,
2830
stream: payload.stream ?? true,
2931
temperature: undefined,
3032
top_p: undefined,
31-
};
33+
...(oaiSearchContextSize && {
34+
web_search_options: {
35+
search_context_size: oaiSearchContextSize,
36+
},
37+
}),
38+
} as any;
3239
}
3340

3441
return { ...payload, stream: payload.stream ?? true };

src/libs/model-runtime/utils/streams/openai.ts

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,87 @@ export const transformOpenAIStream = (
9999
if (item.finish_reason) {
100100
// one-api 的流式接口,会出现既有 finish_reason ,也有 content 的情况
101101
// {"id":"demo","model":"deepl-en","choices":[{"index":0,"delta":{"role":"assistant","content":"Introduce yourself."},"finish_reason":"stop"}]}
102-
103102
if (typeof item.delta?.content === 'string' && !!item.delta.content) {
103+
// MiniMax 内建搜索功能会在第一个 tools 流中 content 返回引用源,需要忽略
104+
// {"id":"0483748a25071c611e2f48d2982fbe96","choices":[{"finish_reason":"stop","index":0,"delta":{"content":"[{\"no\":1,\"url\":\"https://www.xiaohongshu.com/discovery/item/66d8de3c000000001f01e752\",\"title\":\"郑钦文为国而战,没有理由不坚持🏅\",\"content\":\"·2024年08月03日\\n中国队选手郑钦文夺得巴黎奥运会网球女单比赛金牌(巴黎奥运第16金)\\n#巴黎奥运会[话题]# #郑钦文[话题]# #人物素材积累[话题]# #作文素材积累[话题]# #申论素材[话题]#\",\"web_icon\":\"https://www.xiaohongshu.com/favicon.ico\"}]","role":"tool","tool_call_id":"call_function_6696730535"}}],"created":1748255114,"model":"abab6.5s-chat","object":"chat.completion.chunk","usage":{"total_tokens":0,"total_characters":0},"input_sensitive":false,"output_sensitive":false,"input_sensitive_type":0,"output_sensitive_type":0,"output_sensitive_int":0}
105+
if (typeof item.delta?.role === 'string' && item.delta.role === 'tool') {
106+
return { data: null, id: chunk.id, type: 'text' };
107+
}
108+
104109
return { data: item.delta.content, id: chunk.id, type: 'text' };
105110
}
106111

112+
// OpenAI Search Preview 模型返回引用源
113+
// {"id":"chatcmpl-18037d13-243c-4941-8b05-9530b352cf17","object":"chat.completion.chunk","created":1748351805,"model":"gpt-4o-mini-search-preview-2025-03-11","choices":[{"index":0,"delta":{"annotations":[{"type":"url_citation","url_citation":{"url":"https://zh.wikipedia.org/wiki/%E4%B8%8A%E6%B5%B7%E4%B9%90%E9%AB%98%E4%B9%90%E5%9B%AD?utm_source=openai","title":"上海乐高乐园","start_index":75,"end_index":199}}]},"finish_reason":"stop"}],"service_tier":"default"}
114+
if ((item as any).delta?.annotations && (item as any).delta.annotations.length > 0) {
115+
const citations = (item as any).delta.annotations;
116+
117+
return [
118+
{
119+
data: {
120+
citations: citations.map(
121+
(item: any) =>
122+
({
123+
title: item.url_citation.title,
124+
url: item.url_citation.url,
125+
}) as CitationItem,
126+
),
127+
},
128+
id: chunk.id,
129+
type: 'grounding',
130+
},
131+
];
132+
}
133+
134+
// MiniMax 内建搜索功能会在最后一个流中的 message 数组中返回 4 个 Object,其中最后一个为 annotations
135+
// {"id":"0483bf14ba55225a66de2342a21b4003","choices":[{"finish_reason":"tool_calls","index":0,"messages":[{"content":"","role":"user","reasoning_content":""},{"content":"","role":"assistant","tool_calls":[{"id":"call_function_0872338692","type":"web_search","function":{"name":"get_search_result","arguments":"{\"query_tag\":[\"天气\"],\"query_list\":[\"上海 2025年5月26日 天气\"]}"}}],"reasoning_content":""},{"content":"","role":"tool","tool_call_id":"call_function_0872338692","reasoning_content":""},{"content":"","role":"assistant","name":"海螺AI","annotations":[{"text":"【5†source】","url":"https://mtianqi.eastday.com/tianqi/shanghai/20250526.html","quote":"上海天气预报提供上海2025年05月26日天气"}],"audio_content":"","reasoning_content":""}]}],"created":1748274196,"model":"MiniMax-Text-01","object":"chat.completion","usage":{"total_tokens":13110,"total_characters":0,"prompt_tokens":12938,"completion_tokens":172},"base_resp":{"status_code":0,"status_msg":"Invalid parameters detected, json: unknown field \"user\""}}
136+
if ((item as any).messages && (item as any).messages.length > 0) {
137+
const citations = (item as any).messages.at(-1).annotations;
138+
139+
return [
140+
{
141+
data: {
142+
citations: citations.map(
143+
(item: any) =>
144+
({
145+
title: item.url,
146+
url: item.url,
147+
}) as CitationItem,
148+
),
149+
},
150+
id: chunk.id,
151+
type: 'grounding',
152+
},
153+
];
154+
}
155+
107156
if (chunk.usage) {
108157
const usage = chunk.usage;
109158
return { data: convertUsage(usage), id: chunk.id, type: 'usage' };
110159
}
111160

161+
// xAI Live Search 功能返回引用源
162+
// {"id":"8721eebb-6465-4c47-ba2e-8e2ec0f97055","object":"chat.completion.chunk","created":1747809109,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":"stop"}],"system_fingerprint":"fp_1affcf9872","citations":["https://world.huanqiu.com/"]}
163+
if ((chunk as any).citations) {
164+
const citations = (chunk as any).citations;
165+
166+
return [
167+
{
168+
data: {
169+
citations: citations.map(
170+
(item: any) =>
171+
({
172+
title: item,
173+
url: item,
174+
}) as CitationItem,
175+
),
176+
},
177+
id: chunk.id,
178+
type: 'grounding',
179+
},
180+
];
181+
}
182+
112183
return { data: item.finish_reason, id: chunk.id, type: 'stop' };
113184
}
114185

@@ -146,21 +217,20 @@ export const transformOpenAIStream = (
146217
// in Hunyuan api, the citation is in every chunk
147218
('search_info' in chunk && (chunk.search_info as any)?.search_results) ||
148219
// in Wenxin api, the citation is in the first and last chunk
149-
('search_results' in chunk && chunk.search_results);
220+
('search_results' in chunk && chunk.search_results) ||
221+
// in Zhipu api, the citation is in the first chunk
222+
('web_search' in chunk && chunk.web_search);
150223

151224
if (citations) {
152225
streamContext.returnedCitation = true;
153226

154227
return [
155228
{
156229
data: {
157-
citations: (citations as any[]).map(
158-
(item) =>
159-
({
160-
title: typeof item === 'string' ? item : item.title,
161-
url: typeof item === 'string' ? item : item.url,
162-
}) as CitationItem,
163-
),
230+
citations: (citations as any[]).map((item) => ({
231+
title: typeof item === 'string' ? item : item.title,
232+
url: typeof item === 'string' ? item : item.url || item.link,
233+
})).filter(c => c.title && c.url), // Zhipu 内建搜索工具有时会返回空 link 引发程序崩溃
164234
},
165235
id: chunk.id,
166236
type: 'grounding',

src/libs/model-runtime/zhipu/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export const LobeZhipuAI = LobeOpenAICompatibleFactory({
2222
type: 'web_search',
2323
web_search: {
2424
enable: true,
25+
result_sequence: 'before', // 将搜索结果返回顺序更改为 before 适配最小化 OpenAIStream 改动
26+
search_engine: process.env.ZHIPU_SEARCH_ENGINE || 'search_std', // search_std, search_pro
27+
search_result: true,
2528
},
2629
},
2730
]

0 commit comments

Comments
 (0)