🐛 fix: enhance NewAPI with environment variables and fix routers compatibility (#9110)

 feat: enhance NewAPI with environment variables and fix routers compatibility

- Add NEWAPI_API_KEY and NEWAPI_PROXY_URL environment variable support
- Update documentation for NewAPI configuration options
- Fix routers baseURL handling to prevent duplicate version paths
- Remove /v1 baseURL requirement to avoid SDK compatibility issues
- Auto-detect model capabilities based on provider detection
- Support dynamic routing to correct provider endpoints

This resolves URL duplication issues like /v1beta/v1beta/ and ensures
proper routing to Anthropic, Google, OpenAI, and XAI endpoints.
This commit is contained in:
Maple Gao
2025-09-06 11:30:12 +08:00
committed by GitHub
parent e7036af61e
commit a66856dc83
12 changed files with 229 additions and 184 deletions

View File

@@ -173,6 +173,11 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# NEBIUS_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
### NewAPI Service ###
# NEWAPI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# NEWAPI_PROXY_URL=https://your-newapi-server.com
########################################
############ Market Service ############
########################################

View File

@@ -196,6 +196,8 @@ ENV \
MOONSHOT_API_KEY="" MOONSHOT_MODEL_LIST="" MOONSHOT_PROXY_URL="" \
# Nebius
NEBIUS_API_KEY="" NEBIUS_MODEL_LIST="" NEBIUS_PROXY_URL="" \
# NewAPI
NEWAPI_API_KEY="" NEWAPI_PROXY_URL="" \
# Novita
NOVITA_API_KEY="" NOVITA_MODEL_LIST="" \
# Nvidia NIM

View File

@@ -238,6 +238,8 @@ ENV \
MOONSHOT_API_KEY="" MOONSHOT_MODEL_LIST="" MOONSHOT_PROXY_URL="" \
# Nebius
NEBIUS_API_KEY="" NEBIUS_MODEL_LIST="" NEBIUS_PROXY_URL="" \
# NewAPI
NEWAPI_API_KEY="" NEWAPI_PROXY_URL="" \
# Novita
NOVITA_API_KEY="" NOVITA_MODEL_LIST="" \
# Nvidia NIM

View File

@@ -198,6 +198,8 @@ ENV \
MOONSHOT_API_KEY="" MOONSHOT_MODEL_LIST="" MOONSHOT_PROXY_URL="" \
# Nebius
NEBIUS_API_KEY="" NEBIUS_MODEL_LIST="" NEBIUS_PROXY_URL="" \
# NewAPI
NEWAPI_API_KEY="" NEWAPI_PROXY_URL="" \
# Novita
NOVITA_API_KEY="" NOVITA_MODEL_LIST="" \
# Nvidia NIM

View File

@@ -675,4 +675,22 @@ The above example disables all models first, then enables `flux/schnell` and `fl
The above example disables all models first, then enables `flux-pro-1.1` and `flux-kontext-pro` (displayed as `FLUX.1 Kontext [pro]`).
## NewAPI
### `NEWAPI_API_KEY`
- Type: Optional
- Description: This is the API key for your NewAPI service instance. NewAPI is a multi-provider model aggregation service that provides unified access to various AI model APIs.
- Default: -
- Example: `sk-xxxxxx...xxxxxx`
### `NEWAPI_PROXY_URL`
- Type: Optional
- Description: The base URL for your NewAPI server instance. This should point to your deployed NewAPI service endpoint.
- Default: -
- Example: `https://your-newapi-server.com/`
NewAPI is a multi-provider model aggregation service that supports automatic model routing based on provider detection. It offers cost management features and provides a single endpoint for accessing models from multiple providers including OpenAI, Anthropic, Google, and more. Learn more about NewAPI at [https://github.com/Calcium-Ion/new-api](https://github.com/Calcium-Ion/new-api).
[model-list]: /docs/self-hosting/advanced/model-list

View File

@@ -674,4 +674,24 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
上述示例表示先禁用所有模型,再启用 `flux-pro-1.1` 和 `flux-kontext-pro`(显示名为 `FLUX.1 Kontext [pro]`)。
## NewAPI
### `NEWAPI_API_KEY`
- 类型:可选
- 描述:这是你的 NewAPI 服务实例的 API 密钥。NewAPI 是一个多供应商模型聚合服务,提供对各种 AI 模型 API 的统一访问。
- 默认值:-
- 示例:`sk-xxxxxx...xxxxxx`
### `NEWAPI_PROXY_URL`
- 类型:可选
- 描述:你的 NewAPI 服务器实例的基础 URL。这应该指向你部署的 NewAPI 服务端点。
- 默认值:-
- 示例:`https://your-newapi-server.com`
<Callout type={'info'}>
NewAPI 是一个多供应商模型聚合服务,支持基于供应商检测的自动模型路由。它提供成本管理功能,并为访问包括 OpenAI、Anthropic、Google 等多个供应商的模型提供单一端点。了解更多关于 NewAPI 的信息请访问 [https://github.com/Calcium-Ion/new-api](https://github.com/Calcium-Ion/new-api)。
</Callout>
[model-list]: /zh/docs/self-hosting/advanced/model-list

View File

@@ -161,7 +161,7 @@
"title": "API 密钥"
},
"apiUrl": {
"desc": "New API 服务的 API 地址,大部分时候要带 /v1",
"desc": "New API 服务的 API 地址,大部分时候要带 /v1",
"title": "API 地址"
},
"enabled": {

View File

@@ -62,7 +62,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
describe('HandlePayload Function Branch Coverage - Direct Testing', () => {
// Create a mock Set for testing
let testResponsesAPIModels: Set<string>;
const testHandlePayload = (payload: ChatStreamPayload) => {
// This replicates the exact handlePayload logic from the source
if (
@@ -85,7 +85,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
};
const result = testHandlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});
@@ -99,7 +99,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
};
const result = testHandlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});
@@ -113,7 +113,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
};
const result = testHandlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});
@@ -127,7 +127,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
};
const result = testHandlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});
@@ -141,7 +141,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
};
const result = testHandlePayload(payload);
expect(result).toEqual(payload);
});
});
@@ -207,7 +207,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
describe('Models Function Branch Coverage - Logical Testing', () => {
// Test the complex models function logic by replicating its branching behavior
describe('Data Handling Branches', () => {
it('should handle undefined data from models.list (Branch 3.1: data = undefined)', () => {
const data = undefined;
@@ -293,63 +293,63 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
it('should use model_price when > 0 (Branch 3.8: model_price && model_price > 0 = true)', () => {
const pricing = { model_price: 15, model_ratio: 10 };
let inputPrice;
if (pricing.model_price && pricing.model_price > 0) {
inputPrice = pricing.model_price * 2;
} else if (pricing.model_ratio) {
inputPrice = pricing.model_ratio * 2;
}
expect(inputPrice).toBe(30); // model_price * 2
});
it('should fallback to model_ratio when model_price = 0 (Branch 3.8: model_price > 0 = false, Branch 3.9: model_ratio = true)', () => {
const pricing = { model_price: 0, model_ratio: 12 };
let inputPrice;
if (pricing.model_price && pricing.model_price > 0) {
inputPrice = pricing.model_price * 2;
} else if (pricing.model_ratio) {
inputPrice = pricing.model_ratio * 2;
}
expect(inputPrice).toBe(24); // model_ratio * 2
});
it('should handle missing model_ratio (Branch 3.9: model_ratio = undefined)', () => {
const pricing: Partial<NewAPIPricing> = { quota_type: 0 }; // No model_price and no model_ratio
let inputPrice: number | undefined;
if (pricing.model_price && pricing.model_price > 0) {
inputPrice = pricing.model_price * 2;
} else if (pricing.model_ratio) {
inputPrice = pricing.model_ratio * 2;
}
expect(inputPrice).toBeUndefined();
});
it('should calculate output price when inputPrice is defined (Branch 3.10: inputPrice !== undefined = true)', () => {
const inputPrice = 20;
const completionRatio = 1.5;
let outputPrice;
if (inputPrice !== undefined) {
outputPrice = inputPrice * (completionRatio || 1);
}
expect(outputPrice).toBe(30);
});
it('should use default completion_ratio when not provided', () => {
const inputPrice = 16;
const completionRatio = undefined;
let outputPrice;
if (inputPrice !== undefined) {
outputPrice = inputPrice * (completionRatio || 1);
}
expect(outputPrice).toBe(16); // input * 1 (default)
});
});
@@ -358,74 +358,77 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
it('should use supported_endpoint_types with anthropic (Branch 3.11: length > 0 = true, Branch 3.12: includes anthropic = true)', () => {
const model = { supported_endpoint_types: ['anthropic'] };
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
if (model.supported_endpoint_types.includes('anthropic')) {
detectedProvider = 'anthropic';
}
}
expect(detectedProvider).toBe('anthropic');
});
it('should use supported_endpoint_types with gemini (Branch 3.13: includes gemini = true)', () => {
const model = { supported_endpoint_types: ['gemini'] };
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
if (model.supported_endpoint_types.includes('gemini')) {
detectedProvider = 'google';
}
}
expect(detectedProvider).toBe('google');
});
it('should use supported_endpoint_types with xai (Branch 3.14: includes xai = true)', () => {
const model = { supported_endpoint_types: ['xai'] };
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
if (model.supported_endpoint_types.includes('xai')) {
detectedProvider = 'xai';
}
}
expect(detectedProvider).toBe('xai');
});
it('should fallback to owned_by when supported_endpoint_types is empty (Branch 3.11: length > 0 = false, Branch 3.15: owned_by = true)', () => {
const model: Partial<NewAPIModelCard> = { supported_endpoint_types: [], owned_by: 'anthropic' };
const model: Partial<NewAPIModelCard> = {
supported_endpoint_types: [],
owned_by: 'anthropic',
};
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
// Skip - empty array
} else if (model.owned_by) {
detectedProvider = 'anthropic'; // Simplified for test
}
expect(detectedProvider).toBe('anthropic');
});
it('should fallback to owned_by when no supported_endpoint_types (Branch 3.15: owned_by = true)', () => {
const model: Partial<NewAPIModelCard> = { owned_by: 'google' };
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
// Skip - no supported_endpoint_types
} else if (model.owned_by) {
detectedProvider = 'google'; // Simplified for test
}
expect(detectedProvider).toBe('google');
});
it('should use detectModelProvider fallback when no owned_by (Branch 3.15: owned_by = false, Branch 3.17)', () => {
const model: Partial<NewAPIModelCard> = { id: 'claude-3-sonnet', owned_by: '' };
mockDetectModelProvider.mockReturnValue('anthropic');
let detectedProvider = 'openai';
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
// Skip - no supported_endpoint_types
} else if (model.owned_by) {
@@ -433,7 +436,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
} else {
detectedProvider = mockDetectModelProvider(model.id || '');
}
expect(detectedProvider).toBe('anthropic');
expect(mockDetectModelProvider).toHaveBeenCalledWith('claude-3-sonnet');
});
@@ -444,11 +447,11 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
displayName: 'Test Model',
_detectedProvider: 'openai',
};
if (model._detectedProvider) {
delete model._detectedProvider;
}
expect(model).not.toHaveProperty('_detectedProvider');
});
@@ -457,27 +460,31 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
id: 'test-model',
displayName: 'Test Model',
};
const hadDetectedProvider = '_detectedProvider' in model;
if (model._detectedProvider) {
delete model._detectedProvider;
}
expect(hadDetectedProvider).toBe(false);
});
});
describe('URL Processing Branch Coverage', () => {
it('should remove trailing /v1 from baseURL', () => {
it('should remove trailing API version paths from baseURL', () => {
const testURLs = [
{ input: 'https://api.newapi.com/v1', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com/v1/', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com/v1beta', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com/v1beta/', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com/v2', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com/v1alpha', expected: 'https://api.newapi.com' },
{ input: 'https://api.newapi.com', expected: 'https://api.newapi.com' },
];
testURLs.forEach(({ input, expected }) => {
const result = input.replace(/\/v1\/?$/, '');
const result = input.replace(/\/v\d+[a-z]*\/?$/, '');
expect(result).toBe(expected);
});
});
@@ -538,7 +545,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
{ model_name: 'openai-gpt4', quota_type: 1, model_price: 30 }, // Should be skipped
];
const pricingMap = new Map(pricingData.map(p => [p.model_name, p]));
const pricingMap = new Map(pricingData.map((p) => [p.model_name, p]));
const enrichedModels = models.map((model) => {
let enhancedModel: any = { ...model };
@@ -601,7 +608,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
// Test the dynamic routers configuration
const testOptions = {
apiKey: 'test-key',
baseURL: 'https://yourapi.cn/v1'
baseURL: 'https://yourapi.cn/v1',
};
// Create instance to test dynamic routers
@@ -611,8 +618,8 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
// The dynamic routers should be configured with user's baseURL
// This is tested indirectly through successful instantiation
// since the routers function processes the options.baseURL
const expectedBaseURL = testOptions.baseURL.replace(/\/v1\/?$/, '');
const expectedBaseURL = testOptions.baseURL.replace(/\/v\d+[a-z]*\/?$/, '');
expect(expectedBaseURL).toBe('https://yourapi.cn');
});
});
});
});

View File

@@ -1,3 +1,4 @@
import { LOBE_DEFAULT_MODEL_LIST } from 'model-bank';
import urlJoin from 'url-join';
import { createRouterRuntime } from '../RouterRuntime';
@@ -54,9 +55,6 @@ const getProviderFromOwnedBy = (ownedBy: string): string => {
return 'openai';
};
// 全局的模型路由映射,在 models 函数执行后被填充
let globalModelRouteMap: Map<string, string> = new Map();
export const LobeNewAPIAI = createRouterRuntime({
debug: {
chatCompletion: () => process.env.DEBUG_NEWAPI_CHAT_COMPLETION === '1',
@@ -66,180 +64,163 @@ export const LobeNewAPIAI = createRouterRuntime({
},
id: ModelProvider.NewAPI,
models: async ({ client: openAIClient }) => {
// 每次调用 models 时清空并重建路由映射
globalModelRouteMap.clear();
// 获取基础 URL移除末尾的 API 版本路径如 /v1、/v1beta 等)
const baseURL = openAIClient.baseURL.replace(/\/v\d+[a-z]*\/?$/, '');
// 获取基础 URL移除末尾的 /v1
const baseURL = openAIClient.baseURL.replace(/\/v1\/?$/, '');
const modelsPage = (await openAIClient.models.list()) as any;
const modelList: NewAPIModelCard[] = modelsPage.data || [];
const modelsPage = (await openAIClient.models.list()) as any;
const modelList: NewAPIModelCard[] = modelsPage.data || [];
// 尝试获取 pricing 信息以补充模型详细信息
let pricingMap: Map<string, NewAPIPricing> = new Map();
try {
// 使用保存的 baseURL
const pricingResponse = await fetch(`${baseURL}/api/pricing`, {
headers: {
Authorization: `Bearer ${openAIClient.apiKey}`,
},
});
// 尝试获取 pricing 信息以补充模型详细信息
let pricingMap: Map<string, NewAPIPricing> = new Map();
try {
// 使用保存的 baseURL
const pricingResponse = await fetch(`${baseURL}/api/pricing`, {
headers: {
Authorization: `Bearer ${openAIClient.apiKey}`,
},
});
if (pricingResponse.ok) {
const pricingData = await pricingResponse.json();
if (pricingData.success && pricingData.data) {
(pricingData.data as NewAPIPricing[]).forEach((pricing) => {
pricingMap.set(pricing.model_name, pricing);
});
}
}
} catch (error) {
// If fetching pricing information fails, continue using the basic model information
console.debug('Failed to fetch NewAPI pricing info:', error);
}
if (pricingResponse.ok) {
const pricingData = await pricingResponse.json();
if (pricingData.success && pricingData.data) {
(pricingData.data as NewAPIPricing[]).forEach((pricing) => {
pricingMap.set(pricing.model_name, pricing);
});
// Process the model list: determine the provider for each model based on priority rules
const enrichedModelList = modelList.map((model) => {
let enhancedModel: any = { ...model };
// 1. 添加 pricing 信息
const pricing = pricingMap.get(model.id);
if (pricing) {
// NewAPI 的价格计算逻辑:
// - quota_type: 0 表示按量计费(按 token1 表示按次计费
// - model_ratio: 相对于基础价格的倍率(基础价格 = $0.002/1K tokens
// - model_price: 直接指定的价格(优先使用)
// - completion_ratio: 输出价格相对于输入价格的倍率
//
// LobeChat 需要的格式:美元/百万 token
let inputPrice: number | undefined;
let outputPrice: number | undefined;
if (pricing.quota_type === 0) {
// 按量计费
if (pricing.model_price && pricing.model_price > 0) {
// model_price is a direct price value; need to confirm its unit.
// Assumption: model_price is the price per 1,000 tokens (i.e., $/1K tokens).
// To convert to price per 1,000,000 tokens ($/1M tokens), multiply by 1,000,000 / 1,000 = 1,000.
// Since the base price is $0.002/1K tokens, multiplying by 2 gives $2/1M tokens.
// Therefore, inputPrice = model_price * 2 converts the price to $/1M tokens for LobeChat.
inputPrice = pricing.model_price * 2;
} else if (pricing.model_ratio) {
// model_ratio × $0.002/1K = model_ratio × $2/1M
inputPrice = pricing.model_ratio * 2; // 转换为 $/1M tokens
}
if (inputPrice !== undefined) {
// 计算输出价格
outputPrice = inputPrice * (pricing.completion_ratio || 1);
enhancedModel.pricing = {
input: inputPrice,
output: outputPrice,
};
}
}
} catch (error) {
// If fetching pricing information fails, continue using the basic model information
console.debug('Failed to fetch NewAPI pricing info:', error);
// quota_type === 1 按次计费暂不支持
}
// Process the model list: determine the provider for each model based on priority rules
const enrichedModelList = modelList.map((model) => {
let enhancedModel: any = { ...model };
// 2. 根据优先级处理 provider 信息并缓存路由
let detectedProvider = 'openai'; // 默认
// 1. 添加 pricing 信息
const pricing = pricingMap.get(model.id);
if (pricing) {
// NewAPI 的价格计算逻辑:
// - quota_type: 0 表示按量计费(按 token1 表示按次计费
// - model_ratio: 相对于基础价格的倍率(基础价格 = $0.002/1K tokens
// - model_price: 直接指定的价格(优先使用)
// - completion_ratio: 输出价格相对于输入价格的倍率
//
// LobeChat 需要的格式:美元/百万 token
let inputPrice: number | undefined;
let outputPrice: number | undefined;
if (pricing.quota_type === 0) {
// 按量计费
if (pricing.model_price && pricing.model_price > 0) {
// model_price is a direct price value; need to confirm its unit.
// Assumption: model_price is the price per 1,000 tokens (i.e., $/1K tokens).
// To convert to price per 1,000,000 tokens ($/1M tokens), multiply by 1,000,000 / 1,000 = 1,000.
// Since the base price is $0.002/1K tokens, multiplying by 2 gives $2/1M tokens.
// Therefore, inputPrice = model_price * 2 converts the price to $/1M tokens for LobeChat.
inputPrice = pricing.model_price * 2;
} else if (pricing.model_ratio) {
// model_ratio × $0.002/1K = model_ratio × $2/1M
inputPrice = pricing.model_ratio * 2; // 转换为 $/1M tokens
}
if (inputPrice !== undefined) {
// 计算输出价格
outputPrice = inputPrice * (pricing.completion_ratio || 1);
enhancedModel.pricing = {
input: inputPrice,
output: outputPrice,
};
}
}
// quota_type === 1 按次计费暂不支持
// 优先级1使用 supported_endpoint_types
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
if (model.supported_endpoint_types.includes('anthropic')) {
detectedProvider = 'anthropic';
} else if (model.supported_endpoint_types.includes('gemini')) {
detectedProvider = 'google';
} else if (model.supported_endpoint_types.includes('xai')) {
detectedProvider = 'xai';
}
}
// 优先级2使用 owned_by 字段
else if (model.owned_by) {
detectedProvider = getProviderFromOwnedBy(model.owned_by);
}
// 优先级3基于模型名称检测
else {
detectedProvider = detectModelProvider(model.id);
}
// 2. 根据优先级处理 provider 信息并缓存路由
let detectedProvider = 'openai'; // 默认
// 将检测到的 provider 信息附加到模型上
enhancedModel._detectedProvider = detectedProvider;
// 优先级1使用 supported_endpoint_types
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
if (model.supported_endpoint_types.includes('anthropic')) {
detectedProvider = 'anthropic';
} else if (model.supported_endpoint_types.includes('gemini')) {
detectedProvider = 'google';
} else if (model.supported_endpoint_types.includes('xai')) {
detectedProvider = 'xai';
}
}
// 优先级2使用 owned_by 字段
else if (model.owned_by) {
detectedProvider = getProviderFromOwnedBy(model.owned_by);
}
// 优先级3基于模型名称检测
else {
detectedProvider = detectModelProvider(model.id);
}
return enhancedModel;
});
// 将检测到的 provider 信息附加到模型上,供路由使用
enhancedModel._detectedProvider = detectedProvider;
// 同时更新全局路由映射表
globalModelRouteMap.set(model.id, detectedProvider);
// 使用 processMultiProviderModelList 处理模型能力
const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
return enhancedModel;
});
// 清理临时字段
return processedModels.map((model: any) => {
if (model._detectedProvider) {
delete model._detectedProvider;
}
return model;
});
},
routers: (options) => {
const userBaseURL = options.baseURL?.replace(/\/v\d+[a-z]*\/?$/, '') || '';
// 使用 processMultiProviderModelList 处理模型能力
const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
// 如果我们检测到了 provider确保它被正确应用
return processedModels.map((model: any) => {
if (model._detectedProvider) {
// Here you can adjust certain model properties as needed.
// FIXME: The current data structure does not support storing provider information, and the official NewAPI does not provide a corresponding field. Consider extending the model schema if provider tracking is required in the future.
delete model._detectedProvider; // Remove temporary field
}
return model;
});
},
// 使用动态 routers 配置,在构造时获取用户的 baseURL
routers: (options) => {
// 使用全局的模型路由映射
const userBaseURL = options.baseURL?.replace(/\/v1\/?$/, '') || '';
return [
return [
{
apiType: 'anthropic',
models: () =>
Promise.resolve(
Array.from(globalModelRouteMap.entries())
.filter(([, provider]) => provider === 'anthropic')
.map(([modelId]) => modelId),
),
models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
(id) => detectModelProvider(id) === 'anthropic',
),
options: {
// Anthropic 在 NewAPI 中使用 /v1 路径,会自动转换为 /v1/messages
baseURL: urlJoin(userBaseURL, '/v1'),
...options,
baseURL: userBaseURL,
},
},
{
apiType: 'google',
models: () =>
Promise.resolve(
Array.from(globalModelRouteMap.entries())
.filter(([, provider]) => provider === 'google')
.map(([modelId]) => modelId),
),
models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
(id) => detectModelProvider(id) === 'google',
),
options: {
// Gemini 在 NewAPI 中使用 /v1beta 路径
baseURL: urlJoin(userBaseURL, '/v1beta'),
...options,
baseURL: userBaseURL,
},
},
{
apiType: 'xai',
models: () =>
Promise.resolve(
Array.from(globalModelRouteMap.entries())
.filter(([, provider]) => provider === 'xai')
.map(([modelId]) => modelId),
),
models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
(id) => detectModelProvider(id) === 'xai',
),
options: {
// xAI 使用标准 OpenAI 格式,走 /v1 路径
...options,
baseURL: urlJoin(userBaseURL, '/v1'),
},
},
{
apiType: 'openai',
options: {
...options,
baseURL: urlJoin(userBaseURL, '/v1'),
chatCompletion: {
handlePayload,
},
},
},
];
},
];
},
});

View File

@@ -16,7 +16,7 @@ const Page = () => {
...NewAPIProviderCard.settings,
proxyUrl: {
desc: t('newapi.apiUrl.desc'),
placeholder: 'https://any-newapi-provider.com/v1',
placeholder: 'https://any-newapi-provider.com/',
title: t('newapi.apiUrl.title'),
},
}}

View File

@@ -186,6 +186,10 @@ export const getLLMConfig = () => {
ENABLED_AIHUBMIX: z.boolean(),
AIHUBMIX_API_KEY: z.string().optional(),
ENABLED_NEWAPI: z.boolean(),
NEWAPI_API_KEY: z.string().optional(),
NEWAPI_PROXY_URL: z.string().optional(),
},
runtimeEnv: {
API_KEY_SELECT_MODE: process.env.API_KEY_SELECT_MODE,
@@ -368,6 +372,10 @@ export const getLLMConfig = () => {
ENABLED_AIHUBMIX: !!process.env.AIHUBMIX_API_KEY,
AIHUBMIX_API_KEY: process.env.AIHUBMIX_API_KEY,
ENABLED_NEWAPI: !!process.env.NEWAPI_API_KEY,
NEWAPI_API_KEY: process.env.NEWAPI_API_KEY,
NEWAPI_PROXY_URL: process.env.NEWAPI_PROXY_URL,
ENABLED_NEBIUS: !!process.env.NEBIUS_API_KEY,
NEBIUS_API_KEY: process.env.NEBIUS_API_KEY,
},

View File

@@ -164,7 +164,7 @@ export default {
title: 'API 密钥',
},
apiUrl: {
desc: 'New API 服务的 API 地址,大部分时候要带 /v1',
desc: 'New API 服务的 API 地址,大部分时候要带 /v1',
title: 'API 地址',
},
enabled: {