@lobehub/lobehub 2.0.0-next.116 → 2.0.0-next.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import {
4
- MODEL_PARAMETER_CONFLICTS,
5
- createParameterResolver,
6
- resolveParameters,
7
- } from './parameterResolver';
3
+ import { hasTemperatureTopPConflict } from '../const/models';
4
+ import { createParameterResolver, resolveParameters } from './parameterResolver';
8
5
 
9
6
  describe('resolveParameters', () => {
10
7
  describe('Basic functionality', () => {
@@ -247,54 +244,41 @@ describe('createParameterResolver', () => {
247
244
  });
248
245
  });
249
246
 
250
- describe('MODEL_PARAMETER_CONFLICTS', () => {
251
- describe('ANTHROPIC_CLAUDE_4_PLUS', () => {
252
- it('should contain expected Claude 4+ models', () => {
253
- expect(MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-opus-4-1')).toBe(true);
254
- expect(
255
- MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-opus-4-1-20250805'),
256
- ).toBe(true);
257
- expect(
258
- MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-sonnet-4-5-20250929'),
259
- ).toBe(true);
260
- });
261
-
262
- it('should not contain Claude 3.x models', () => {
263
- expect(MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-3-opus-20240229')).toBe(
264
- false,
265
- );
266
- expect(
267
- MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has('claude-3.5-sonnet-20240620'),
268
- ).toBe(false);
247
+ describe('hasTemperatureTopPConflict', () => {
248
+ describe('Anthropic Claude 4+ models', () => {
249
+ it('should return true for Claude 4+ models', () => {
250
+ expect(hasTemperatureTopPConflict('claude-opus-4-1-20250805')).toBe(true);
251
+ expect(hasTemperatureTopPConflict('claude-sonnet-4-5-20250929')).toBe(true);
252
+ expect(hasTemperatureTopPConflict('claude-haiku-4-5-20251001')).toBe(true);
253
+ });
254
+
255
+ it('should return false for Claude 3.x models', () => {
256
+ expect(hasTemperatureTopPConflict('claude-3-opus-20240229')).toBe(false);
257
+ expect(hasTemperatureTopPConflict('claude-3-5-sonnet-20240620')).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('OpenRouter Claude 4+ models', () => {
262
+ it('should return true for OpenRouter Claude 4+ models', () => {
263
+ expect(hasTemperatureTopPConflict('anthropic/claude-opus-4.5')).toBe(true);
264
+ expect(hasTemperatureTopPConflict('anthropic/claude-sonnet-4.1')).toBe(true);
265
+ expect(hasTemperatureTopPConflict('anthropic/claude-4.5-opus')).toBe(true);
266
+ });
267
+
268
+ it('should return false for OpenRouter Claude 3.x models', () => {
269
+ expect(hasTemperatureTopPConflict('anthropic/claude-3.5-sonnet')).toBe(false);
270
+ expect(hasTemperatureTopPConflict('anthropic/claude-3.7-sonnet')).toBe(false);
269
271
  });
270
272
  });
271
273
 
272
- describe('BEDROCK_CLAUDE_4_PLUS', () => {
273
- it('should contain both standard and Bedrock-specific model IDs', () => {
274
- expect(MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has('claude-opus-4-1')).toBe(true);
275
- expect(
276
- MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
277
- 'anthropic.claude-opus-4-1-20250805-v1:0',
278
- ),
279
- ).toBe(true);
280
- expect(
281
- MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
282
- 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
283
- ),
284
- ).toBe(true);
285
- });
286
-
287
- it('should contain all Bedrock regional variants', () => {
288
- expect(
289
- MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
290
- 'anthropic.claude-opus-4-20250514-v1:0',
291
- ),
292
- ).toBe(true);
293
- expect(
294
- MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(
295
- 'us.anthropic.claude-opus-4-20250514-v1:0',
296
- ),
297
- ).toBe(true);
274
+ describe('Bedrock Claude 4+ models', () => {
275
+ it('should return true for Bedrock Claude 4+ models', () => {
276
+ expect(hasTemperatureTopPConflict('anthropic.claude-opus-4-1-20250805-v1:0')).toBe(true);
277
+ expect(hasTemperatureTopPConflict('us.anthropic.claude-sonnet-4-5-20250929-v1:0')).toBe(true);
278
+ });
279
+
280
+ it('should return false for Bedrock Claude 3.x models', () => {
281
+ expect(hasTemperatureTopPConflict('anthropic.claude-3-5-sonnet-20240620-v1:0')).toBe(false);
298
282
  });
299
283
  });
300
284
  });
@@ -239,44 +239,3 @@ export const createParameterResolver = (options: ParameterResolverOptions) => {
239
239
  return resolveParameters(config, options);
240
240
  };
241
241
  };
242
-
243
- /**
244
- * Common model sets that have parameter conflicts
245
- */
246
- export const MODEL_PARAMETER_CONFLICTS = {
247
- /**
248
- * Claude models after Opus 4.1 that don't allow both temperature and top_p
249
- */
250
- ANTHROPIC_CLAUDE_4_PLUS: new Set([
251
- 'claude-opus-4-1',
252
- 'claude-opus-4-1-20250805',
253
- 'claude-sonnet-4-5-20250929',
254
- 'claude-haiku-4-5-20251001',
255
- 'claude-opus-4-5-20251101',
256
- ]),
257
-
258
- /**
259
- * Bedrock Claude 4+ models (including Bedrock-specific model IDs)
260
- */
261
- BEDROCK_CLAUDE_4_PLUS: new Set([
262
- 'claude-opus-4-1',
263
- 'claude-opus-4-1-20250805',
264
- 'claude-opus-4-20250514',
265
- 'claude-sonnet-4-20250514',
266
- 'claude-sonnet-4-5-20250929',
267
- 'claude-haiku-4-5-20251001',
268
- // Bedrock model IDs
269
- 'anthropic.claude-opus-4-1-20250805-v1:0',
270
- 'us.anthropic.claude-opus-4-1-20250805-v1:0',
271
- 'anthropic.claude-opus-4-20250514-v1:0',
272
- 'us.anthropic.claude-opus-4-20250514-v1:0',
273
- 'anthropic.claude-sonnet-4-20250514-v1:0',
274
- 'us.anthropic.claude-sonnet-4-20250514-v1:0',
275
- 'anthropic.claude-sonnet-4-5-20250929-v1:0',
276
- 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
277
- 'anthropic.claude-haiku-4-5-20251001-v1:0',
278
- 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
279
- 'global.anthropic.claude-opus-4-5-20251101-v1:0',
280
- 'anthropic.claude-opus-4-5-20251101-v1:0',
281
- ]),
282
- };
@@ -1,3 +1,4 @@
1
+ export * from './const/models';
1
2
  export * from './core/BaseAI';
2
3
  export { pruneReasoningPayload } from './core/contextBuilders/openai';
3
4
  export { ModelRuntime } from './core/ModelRuntime';
@@ -1,9 +1,14 @@
1
1
  import Anthropic, { ClientOptions } from '@anthropic-ai/sdk';
2
2
  import { ModelProvider } from 'model-bank';
3
3
 
4
+ import { hasTemperatureTopPConflict } from '../../const/models';
4
5
  import { LobeRuntimeAI } from '../../core/BaseAI';
5
- import { buildAnthropicMessages, buildAnthropicTools } from '../../core/contextBuilders/anthropic';
6
- import { MODEL_PARAMETER_CONFLICTS, resolveParameters } from '../../core/parameterResolver';
6
+ import {
7
+ buildAnthropicMessages,
8
+ buildAnthropicTools,
9
+ buildSearchTool,
10
+ } from '../../core/contextBuilders/anthropic';
11
+ import { resolveParameters } from '../../core/parameterResolver';
7
12
  import { AnthropicStream } from '../../core/streams';
8
13
  import {
9
14
  type ChatCompletionErrorPayload,
@@ -22,6 +27,7 @@ import { StreamingResponse } from '../../utils/response';
22
27
  import { createAnthropicGenerateObject } from './generateObject';
23
28
  import { handleAnthropicError } from './handleAnthropicError';
24
29
  import { resolveCacheTTL } from './resolveCacheTTL';
30
+ import { resolveMaxTokens } from './resolveMaxTokens';
25
31
 
26
32
  export interface AnthropicModelCard {
27
33
  created_at: string;
@@ -31,8 +37,6 @@ export interface AnthropicModelCard {
31
37
 
32
38
  type anthropicTools = Anthropic.Tool | Anthropic.WebSearchTool20250305;
33
39
 
34
- const modelsWithSmallContextWindow = new Set(['claude-3-opus-20240229', 'claude-3-haiku-20240307']);
35
-
36
40
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
37
41
 
38
42
  interface AnthropicAIParams extends ClientOptions {
@@ -140,15 +144,13 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
140
144
  } = payload;
141
145
 
142
146
  const { anthropic: anthropicModels } = await import('model-bank');
143
- const modelConfig = anthropicModels.find((m) => m.id === model);
144
- const defaultMaxOutput = modelConfig?.maxOutput;
145
147
 
146
- // 配置优先级:用户设置 > 模型配置 > 硬编码默认值
147
- const getMaxTokens = () => {
148
- if (max_tokens) return max_tokens;
149
- if (defaultMaxOutput) return defaultMaxOutput;
150
- return undefined;
151
- };
148
+ const resolvedMaxTokens = await resolveMaxTokens({
149
+ max_tokens,
150
+ model,
151
+ providerModels: anthropicModels,
152
+ thinking,
153
+ });
152
154
 
153
155
  const system_message = messages.find((m) => m.role === 'system');
154
156
  const user_messages = messages.filter((m) => m.role !== 'system');
@@ -170,20 +172,8 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
170
172
  });
171
173
 
172
174
  if (enabledSearch) {
173
- // Limit the number of searches per request
174
- const maxUses = process.env.ANTHROPIC_MAX_USES;
175
-
176
- const webSearchTool: Anthropic.WebSearchTool20250305 = {
177
- name: 'web_search',
178
- type: 'web_search_20250305',
179
- ...(maxUses &&
180
- Number.isInteger(Number(maxUses)) &&
181
- Number(maxUses) > 0 && {
182
- max_uses: Number(maxUses),
183
- }),
184
- };
185
-
186
- // 如果已有工具,则添加到现有工具列表中;否则创建新的工具列表
175
+ const webSearchTool = buildSearchTool();
176
+
187
177
  if (postTools && postTools.length > 0) {
188
178
  postTools = [...postTools, webSearchTool];
189
179
  } else {
@@ -192,19 +182,17 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
192
182
  }
193
183
 
194
184
  if (!!thinking && thinking.type === 'enabled') {
195
- const maxTokens = getMaxTokens() || 32_000; // Claude Opus 4 has minimum maxOutput
196
-
197
185
  // `temperature` may only be set to 1 when thinking is enabled.
198
186
  // `top_p` must be unset when thinking is enabled.
199
187
  return {
200
- max_tokens: maxTokens,
188
+ max_tokens: resolvedMaxTokens,
201
189
  messages: postMessages,
202
190
  model,
203
191
  system: systemPrompts,
204
192
  thinking: {
205
193
  ...thinking,
206
194
  budget_tokens: thinking?.budget_tokens
207
- ? Math.min(thinking.budget_tokens, maxTokens - 1) // `max_tokens` must be greater than `thinking.budget_tokens`.
195
+ ? Math.min(thinking.budget_tokens, resolvedMaxTokens - 1) // `max_tokens` must be greater than `thinking.budget_tokens`.
208
196
  : 1024,
209
197
  },
210
198
  tools: postTools,
@@ -212,7 +200,7 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
212
200
  }
213
201
 
214
202
  // Resolve temperature and top_p parameters based on model constraints
215
- const hasConflict = MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has(model);
203
+ const hasConflict = hasTemperatureTopPConflict(model);
216
204
  const resolvedParams = resolveParameters(
217
205
  { temperature, top_p },
218
206
  { hasConflict, normalizeTemperature: true, preferTemperature: true },
@@ -221,7 +209,7 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
221
209
  return {
222
210
  // claude 3 series model hax max output token of 4096, 3.x series has 8192
223
211
  // https://docs.anthropic.com/en/docs/about-claude/models/all-models#:~:text=200K-,Max%20output,-Normal%3A
224
- max_tokens: getMaxTokens() || (modelsWithSmallContextWindow.has(model) ? 4096 : 8192),
212
+ max_tokens: resolvedMaxTokens,
225
213
  messages: postMessages,
226
214
  model,
227
215
  system: systemPrompts,
@@ -0,0 +1,35 @@
1
+ import type { ChatStreamPayload } from '../../types';
2
+
3
+ const smallContextWindowPatterns = [
4
+ /claude-3-opus-20240229/,
5
+ /claude-3-haiku-20240307/,
6
+ /claude-v2(:1)?$/,
7
+ ];
8
+
9
+ /**
10
+ * Resolve the max_tokens value to align Anthropic and Bedrock behavior.
11
+ * Priority: user input > model-bank default maxOutput > hardcoded fallback (context-window aware).
12
+ */
13
+ export const resolveMaxTokens = async ({
14
+ max_tokens,
15
+ model,
16
+ thinking,
17
+ providerModels,
18
+ }: {
19
+ max_tokens?: number;
20
+ model: string;
21
+ providerModels: { id: string; maxOutput?: number }[];
22
+ thinking?: ChatStreamPayload['thinking'];
23
+ }) => {
24
+ const defaultMaxOutput = providerModels.find((m) => m.id === model)?.maxOutput;
25
+
26
+ const preferredMaxTokens = max_tokens ?? defaultMaxOutput;
27
+
28
+ if (preferredMaxTokens) return preferredMaxTokens;
29
+
30
+ if (thinking?.type === 'enabled') return 32_000;
31
+
32
+ const hasSmallContextWindow = smallContextWindowPatterns.some((pattern) => pattern.test(model));
33
+
34
+ return hasSmallContextWindow ? 4096 : 8192;
35
+ };
@@ -10,8 +10,6 @@ import { AgentRuntimeErrorType } from '../../types/error';
10
10
  import * as debugStreamModule from '../../utils/debugStream';
11
11
  import { LobeBedrockAI, experimental_buildLlama2Prompt } from './index';
12
12
 
13
- const provider = 'bedrock';
14
-
15
13
  // Mock the console.error to avoid polluting test output
16
14
  vi.spyOn(console, 'error').mockImplementation(() => {});
17
15
 
@@ -478,7 +476,7 @@ describe('LobeBedrockAI', () => {
478
476
  accept: 'application/json',
479
477
  body: JSON.stringify({
480
478
  anthropic_version: 'bedrock-2023-05-31',
481
- max_tokens: 4096,
479
+ max_tokens: 8192,
482
480
  messages: [
483
481
  {
484
482
  content: [
@@ -521,7 +519,7 @@ describe('LobeBedrockAI', () => {
521
519
  accept: 'application/json',
522
520
  body: JSON.stringify({
523
521
  anthropic_version: 'bedrock-2023-05-31',
524
- max_tokens: 4096,
522
+ max_tokens: 8192,
525
523
  messages: [
526
524
  {
527
525
  content: [
@@ -565,7 +563,7 @@ describe('LobeBedrockAI', () => {
565
563
  accept: 'application/json',
566
564
  body: JSON.stringify({
567
565
  anthropic_version: 'bedrock-2023-05-31',
568
- max_tokens: 4096,
566
+ max_tokens: 8192,
569
567
  messages: [
570
568
  {
571
569
  content: [
@@ -610,7 +608,7 @@ describe('LobeBedrockAI', () => {
610
608
  accept: 'application/json',
611
609
  body: JSON.stringify({
612
610
  anthropic_version: 'bedrock-2023-05-31',
613
- max_tokens: 4096,
611
+ max_tokens: 64_000,
614
612
  messages: [
615
613
  {
616
614
  content: [
@@ -654,7 +652,7 @@ describe('LobeBedrockAI', () => {
654
652
  accept: 'application/json',
655
653
  body: JSON.stringify({
656
654
  anthropic_version: 'bedrock-2023-05-31',
657
- max_tokens: 4096,
655
+ max_tokens: 8192,
658
656
  messages: [
659
657
  {
660
658
  content: [
@@ -1,3 +1,4 @@
1
+ import type Anthropic from '@anthropic-ai/sdk';
1
2
  import {
2
3
  BedrockRuntimeClient,
3
4
  InvokeModelCommand,
@@ -5,9 +6,10 @@ import {
5
6
  } from '@aws-sdk/client-bedrock-runtime';
6
7
  import { ModelProvider } from 'model-bank';
7
8
 
9
+ import { hasTemperatureTopPConflict } from '../../const/models';
8
10
  import { LobeRuntimeAI } from '../../core/BaseAI';
9
11
  import { buildAnthropicMessages, buildAnthropicTools } from '../../core/contextBuilders/anthropic';
10
- import { MODEL_PARAMETER_CONFLICTS, resolveParameters } from '../../core/parameterResolver';
12
+ import { resolveParameters } from '../../core/parameterResolver';
11
13
  import {
12
14
  AWSBedrockClaudeStream,
13
15
  AWSBedrockLlamaStream,
@@ -26,6 +28,7 @@ import { debugStream } from '../../utils/debugStream';
26
28
  import { getModelPricing } from '../../utils/getModelPricing';
27
29
  import { StreamingResponse } from '../../utils/response';
28
30
  import { resolveCacheTTL } from '../anthropic/resolveCacheTTL';
31
+ import { resolveMaxTokens } from '../anthropic/resolveMaxTokens';
29
32
 
30
33
  /**
31
34
  * A prompt constructor for HuggingFace LLama 2 chat models.
@@ -62,19 +65,24 @@ export function experimental_buildLlama2Prompt(messages: { content: string; role
62
65
  export interface LobeBedrockAIParams {
63
66
  accessKeyId?: string;
64
67
  accessKeySecret?: string;
68
+ id?: string;
65
69
  region?: string;
66
70
  sessionToken?: string;
67
71
  }
68
72
 
69
73
  export class LobeBedrockAI implements LobeRuntimeAI {
70
74
  private client: BedrockRuntimeClient;
75
+ private id: string;
71
76
 
72
77
  region: string;
73
78
 
74
- constructor({ region, accessKeyId, accessKeySecret, sessionToken }: LobeBedrockAIParams = {}) {
79
+ constructor(options: LobeBedrockAIParams = {}) {
80
+ const { id, region, accessKeyId, accessKeySecret, sessionToken } = options;
81
+
75
82
  if (!(accessKeyId && accessKeySecret))
76
83
  throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidBedrockCredentials);
77
84
  this.region = region ?? 'us-east-1';
85
+ this.id = id ?? ModelProvider.Bedrock;
78
86
  this.client = new BedrockRuntimeClient({
79
87
  credentials: {
80
88
  accessKeyId: accessKeyId,
@@ -158,18 +166,28 @@ export class LobeBedrockAI implements LobeRuntimeAI {
158
166
  temperature,
159
167
  top_p,
160
168
  tools,
169
+ thinking,
161
170
  } = payload;
162
171
  const inputStartAt = Date.now();
163
172
  const system_message = messages.find((m) => m.role === 'system');
164
173
  const user_messages = messages.filter((m) => m.role !== 'system');
165
174
 
166
175
  // Resolve temperature and top_p parameters based on model constraints
167
- const hasConflict = MODEL_PARAMETER_CONFLICTS.BEDROCK_CLAUDE_4_PLUS.has(model);
176
+ const hasConflict = hasTemperatureTopPConflict(model);
168
177
  const resolvedParams = resolveParameters(
169
178
  { temperature, top_p },
170
179
  { hasConflict, normalizeTemperature: true, preferTemperature: true },
171
180
  );
172
181
 
182
+ const { bedrock: bedrockModels } = await import('model-bank');
183
+
184
+ const resolvedMaxTokens = await resolveMaxTokens({
185
+ max_tokens,
186
+ model,
187
+ providerModels: bedrockModels,
188
+ thinking,
189
+ });
190
+
173
191
  const systemPrompts = !!system_message?.content
174
192
  ? ([
175
193
  {
@@ -177,19 +195,40 @@ export class LobeBedrockAI implements LobeRuntimeAI {
177
195
  text: system_message.content as string,
178
196
  type: 'text',
179
197
  },
180
- ] as any)
198
+ ] as Anthropic.TextBlockParam[])
181
199
  : undefined;
182
200
 
183
- const anthropicPayload = {
201
+ const postTools = buildAnthropicTools(tools, {
202
+ enabledContextCaching,
203
+ });
204
+
205
+ const anthropicBase = {
184
206
  anthropic_version: 'bedrock-2023-05-31',
185
- max_tokens: max_tokens || 4096,
207
+ max_tokens: resolvedMaxTokens,
186
208
  messages: await buildAnthropicMessages(user_messages, { enabledContextCaching }),
187
209
  system: systemPrompts,
188
- temperature: resolvedParams.temperature,
189
- tools: buildAnthropicTools(tools, { enabledContextCaching }),
190
- top_p: resolvedParams.top_p,
210
+ tools: postTools,
191
211
  };
192
212
 
213
+ const anthropicPayload =
214
+ thinking?.type === 'enabled'
215
+ ? {
216
+ ...anthropicBase,
217
+ thinking: {
218
+ ...thinking,
219
+ // `max_tokens` must be greater than `budget_tokens`
220
+ budget_tokens: Math.max(
221
+ 1,
222
+ Math.min(thinking.budget_tokens || 1024, resolvedMaxTokens - 1),
223
+ ),
224
+ },
225
+ }
226
+ : {
227
+ ...anthropicBase,
228
+ temperature: resolvedParams.temperature,
229
+ top_p: resolvedParams.top_p,
230
+ };
231
+
193
232
  const command = new InvokeModelWithResponseStreamCommand({
194
233
  accept: 'application/json',
195
234
  body: JSON.stringify(anthropicPayload),
@@ -209,7 +248,7 @@ export class LobeBedrockAI implements LobeRuntimeAI {
209
248
  debugStream(debug).catch(console.error);
210
249
  }
211
250
 
212
- const pricing = await getModelPricing(payload.model, ModelProvider.Bedrock);
251
+ const pricing = await getModelPricing(payload.model, this.id);
213
252
  const cacheTTL = resolveCacheTTL({ ...payload, enabledContextCaching }, anthropicPayload);
214
253
  const pricingOptions = cacheTTL ? { lookupParams: { ttl: cacheTTL } } : undefined;
215
254
 
@@ -218,7 +257,7 @@ export class LobeBedrockAI implements LobeRuntimeAI {
218
257
  AWSBedrockClaudeStream(prod, {
219
258
  callbacks: options?.callback,
220
259
  inputStartAt,
221
- payload: { model, pricing, pricingOptions, provider: ModelProvider.Bedrock },
260
+ payload: { model, pricing, pricingOptions, provider: this.id },
222
261
  }),
223
262
  {
224
263
  headers: options?.headers,
@@ -1,6 +1,6 @@
1
1
  import { Block } from '@lobehub/ui';
2
2
  import { Empty } from 'antd';
3
- import Link from 'next/link';
3
+ import { Link } from 'react-router-dom';
4
4
  import { memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
  import urlJoin from 'url-join';
@@ -20,11 +20,16 @@ const Plugin = memo(() => {
20
20
 
21
21
  return (
22
22
  <Flexbox gap={8}>
23
- {config?.plugins.map((item) => (
24
- <Link href={urlJoin('/discover/plugin', item)} key={item}>
25
- <PluginItem identifier={item} />
26
- </Link>
27
- ))}
23
+ {config?.plugins.map((item) => {
24
+ const identifier =
25
+ typeof item === 'string' ? item : (item as { identifier: string }).identifier;
26
+
27
+ return (
28
+ <Link key={identifier} to={urlJoin('/discover/plugin', identifier)}>
29
+ <PluginItem identifier={identifier} />
30
+ </Link>
31
+ );
32
+ })}
28
33
  </Flexbox>
29
34
  );
30
35
  });
@@ -5,6 +5,8 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
5
5
  {
6
6
  "description": "Echoes back a message with 'Hello' prefix",
7
7
  "inputSchema": {
8
+ "$schema": "http://json-schema.org/draft-07/schema#",
9
+ "additionalProperties": false,
8
10
  "properties": {
9
11
  "message": {
10
12
  "description": "The message to echo",
@@ -21,6 +23,7 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
21
23
  {
22
24
  "description": "Lists all available tools and methods",
23
25
  "inputSchema": {
26
+ "$schema": "http://json-schema.org/draft-07/schema#",
24
27
  "properties": {},
25
28
  "type": "object",
26
29
  },
@@ -29,6 +32,8 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
29
32
  {
30
33
  "description": "Adds two numbers",
31
34
  "inputSchema": {
35
+ "$schema": "http://json-schema.org/draft-07/schema#",
36
+ "additionalProperties": false,
32
37
  "properties": {
33
38
  "a": {
34
39
  "description": "The first number",
@@ -886,8 +886,48 @@ export class DiscoverService {
886
886
  const all = await this._getPluginList(locale);
887
887
  let raw = all.find((item) => item.identifier === identifier);
888
888
  if (!raw) {
889
- log('getPluginDetail: plugin not found for identifier=%s', identifier);
890
- return;
889
+ log('getPluginDetail: plugin not found in default store for identifier=%s, trying MCP plugin', identifier);
890
+ try {
891
+ const mcpDetail = await this.getMcpDetail({ identifier, locale });
892
+ const convertedMcp: Partial<DiscoverPluginDetail> = {
893
+ author:
894
+ typeof (mcpDetail as any).author === 'object'
895
+ ? (mcpDetail as any).author?.name || ''
896
+ : (mcpDetail as any).author || '',
897
+ avatar: (mcpDetail as any).icon || (mcpDetail as any).avatar || '',
898
+ category: (mcpDetail as any).category as any,
899
+ createdAt: (mcpDetail as any).createdAt || '',
900
+ description: mcpDetail.description || '',
901
+ homepage: mcpDetail.homepage || '',
902
+ identifier: mcpDetail.identifier,
903
+ manifest: undefined,
904
+ related: mcpDetail.related.map((item) => ({
905
+ author:
906
+ typeof (item as any).author === 'object'
907
+ ? (item as any).author?.name || ''
908
+ : (item as any).author || '',
909
+ avatar: (item as any).icon || (item as any).avatar || '',
910
+ category: (item as any).category as any,
911
+ createdAt: (item as any).createdAt || '',
912
+ description: (item as any).description || '',
913
+ homepage: (item as any).homepage || '',
914
+ identifier: item.identifier,
915
+ manifest: undefined,
916
+ schemaVersion: 1,
917
+ tags: (item as any).tags || [],
918
+ title: (item as any).name || item.identifier,
919
+ })) as unknown as DiscoverPluginItem[],
920
+ schemaVersion: 1,
921
+ tags: (mcpDetail as any).tags || [],
922
+ title: (mcpDetail as any).name || mcpDetail.identifier,
923
+ };
924
+ const plugin = merge(cloneDeep(DEFAULT_DISCOVER_PLUGIN_ITEM), convertedMcp);
925
+ log('getPluginDetail: returning converted MCP plugin');
926
+ return plugin as DiscoverPluginDetail;
927
+ } catch (error) {
928
+ log('getPluginDetail: MCP plugin not found for identifier=%s, error=%O', identifier, error);
929
+ return;
930
+ }
891
931
  }
892
932
 
893
933
  raw = merge(cloneDeep(DEFAULT_DISCOVER_PLUGIN_ITEM), raw);
@@ -13,9 +13,9 @@ const log = debug('lobe-mcp:content-processor');
13
13
  export type ProcessContentBlocksFn = (blocks: ToolCallContent[]) => Promise<ToolCallContent[]>;
14
14
 
15
15
  /**
16
- * 处理 MCP 返回的 content blocks
17
- * - 上传图片/音频到存储并替换 data 为代理 URL
18
- * - 保持其他类型的 block 不变
16
+ * Process content blocks returned by MCP
17
+ * - Upload images/audio to storage and replace data with proxy URL
18
+ * - Keep other types of blocks unchanged
19
19
  */
20
20
  export const processContentBlocks = async (
21
21
  blocks: ToolCallContent[],
@@ -64,10 +64,10 @@ export const processContentBlocks = async (
64
64
  };
65
65
 
66
66
  /**
67
- * content blocks 转换为字符串
68
- * - text: 提取 text 字段
69
- * - image/audio: 提取 data 字段(通常是上传后的代理 URL
70
- * - 其他: 返回空字符串
67
+ * Convert content blocks to string
68
+ * - text: Extract text field
69
+ * - image/audio: Extract data field (usually the proxy URL after upload)
70
+ * - others: Return empty string
71
71
  */
72
72
  export const contentBlocksToString = (blocks: ToolCallContent[] | null | undefined): string => {
73
73
  if (!blocks) return '';
@@ -195,13 +195,13 @@ class MCPSystemDepsCheckService {
195
195
  // Check if all system dependencies meet requirements
196
196
  const allDependenciesMet = systemDependenciesResults.every((dep) => dep.meetRequirement);
197
197
 
198
- // Check if configuration is required (有必填项)
198
+ // Check if configuration is required (has mandatory fields)
199
199
  const configSchema = option.connection?.configSchema;
200
200
  const needsConfig = Boolean(
201
201
  configSchema &&
202
- // 检查是否有 required 数组且不为空
202
+ // Check if there's a non-empty required array
203
203
  ((Array.isArray(configSchema.required) && configSchema.required.length > 0) ||
204
- // 检查 properties 中是否有字段标记为 required
204
+ // Check if any field in properties is marked as required
205
205
  (configSchema.properties &&
206
206
  Object.values(configSchema.properties).some((prop: any) => prop.required === true))),
207
207
  );