Skip to content
Open
43 changes: 25 additions & 18 deletions packages/model-runtime/src/core/openaiCompatibleFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,32 +208,39 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an

log('chat called with model: %s, stream: %s', payload.model, payload.stream ?? true);

// 工厂级 Responses API 路由控制(支持实例覆盖)
const modelId = (payload as any).model as string | undefined;
const shouldUseResponses = (() => {
let processedPayload: any = payload;
const userApiMode = (payload as any).apiMode;

if (userApiMode === 'responses') {
// User switch is ON, check if model is in the whitelist
const modelId = (payload as any).model as string | undefined;
const instanceChat = ((this._options as any).chatCompletion || {}) as {
useResponse?: boolean;
useResponseModels?: Array<string | RegExp>;
};
const flagUseResponse =
instanceChat.useResponse ?? (chatCompletion ? chatCompletion.useResponse : undefined);
const flagUseResponseModels =
instanceChat.useResponseModels ?? chatCompletion?.useResponseModels;

if (!chatCompletion && !instanceChat) return false;
if (flagUseResponse) return true;
if (!modelId || !flagUseResponseModels?.length) return false;
return flagUseResponseModels.some((m: string | RegExp) =>
typeof m === 'string' ? modelId.includes(m) : (m as RegExp).test(modelId),
);
})();

let processedPayload: any = payload;
if (shouldUseResponses) {
log('using Responses API mode');
processedPayload = { ...payload, apiMode: 'responses' } as any;
// Check if model matches useResponseModels whitelist
const modelInWhitelist = (() => {
if (!modelId) return false;
if (!flagUseResponseModels?.length) return true; // No whitelist = allow all
return flagUseResponseModels.some((m: string | RegExp) =>
typeof m === 'string' ? modelId.includes(m) : (m as RegExp).test(modelId),
);
})();

if (modelInWhitelist) {
log('using Responses API mode (switch ON + model in whitelist)');
// Keep apiMode: 'responses'
} else {
log('using Chat Completions API mode (switch ON but model not in whitelist)');
processedPayload = { ...payload, apiMode: undefined } as any;
}
} else {
log('using Chat Completions API mode');
// User switch is OFF or not set, always use Chat Completions API
log('using Chat Completions API mode (switch OFF)');
processedPayload = { ...payload, apiMode: undefined } as any;
}

// 再进行工厂级处理
Expand Down
78 changes: 3 additions & 75 deletions packages/model-runtime/src/providers/newapi/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { responsesAPIModels } from '../../const/models';
import { ChatStreamPayload } from '../../types/chat';
import * as modelParseModule from '../../utils/modelParse';
import { LobeNewAPIAI, NewAPIModelCard, NewAPIPricing, handlePayload, params } from './index';
import { LobeNewAPIAI, NewAPIModelCard, NewAPIPricing, params } from './index';

// Mock external dependencies
vi.mock('../../utils/modelParse');
Expand Down Expand Up @@ -701,78 +701,6 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
});
});

describe('HandlePayload Function - Direct Testing', () => {
beforeEach(() => {
// Mock responsesAPIModels as a Set for testing
(responsesAPIModels as any).has = vi.fn((model: string) => model === 'o1-pro');
});

it('should add apiMode for models in responsesAPIModels set', () => {
(responsesAPIModels as any).has = vi.fn((model: string) => model === 'o1-pro');

const payload: ChatStreamPayload = {
model: 'o1-pro',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.5,
};

const result = handlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});

it('should add apiMode for gpt- models', () => {
(responsesAPIModels as any).has = vi.fn(() => false);

const payload: ChatStreamPayload = {
model: 'gpt-4o',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.5,
};

const result = handlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});

it('should add apiMode for o1 models', () => {
(responsesAPIModels as any).has = vi.fn(() => false);

const payload: ChatStreamPayload = {
model: 'o1-mini',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.5,
};

const result = handlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});

it('should add apiMode for o3 models', () => {
(responsesAPIModels as any).has = vi.fn(() => false);

const payload: ChatStreamPayload = {
model: 'o3-turbo',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.5,
};

const result = handlePayload(payload);
expect(result).toEqual({ ...payload, apiMode: 'responses' });
});

it('should not modify payload for regular models', () => {
(responsesAPIModels as any).has = vi.fn(() => false);

const payload: ChatStreamPayload = {
model: 'claude-3-sonnet',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.5,
};

const result = handlePayload(payload);
expect(result).toEqual(payload);
});
});

describe('Routers Function - Direct Testing', () => {
it('should generate routers with correct apiTypes', () => {
const options = { apiKey: 'test', baseURL: 'https://api.newapi.com/v1' };
Expand Down Expand Up @@ -823,11 +751,11 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
expect(routers[3].options.baseURL).toBe('https://custom.com/v1');
});

it('should configure openai router with handlePayload', () => {
it('should configure openai router with useResponseModels', () => {
const options = { apiKey: 'test', baseURL: 'https://custom.com/v1' };
const routers = params.routers(options);

expect((routers[3].options as any).chatCompletion?.handlePayload).toBe(handlePayload);
expect((routers[3].options as any).chatCompletion?.useResponseModels).toBeDefined();
});

it('should filter anthropic models for anthropic router', () => {
Expand Down
15 changes: 1 addition & 14 deletions packages/model-runtime/src/providers/newapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import urlJoin from 'url-join';
import { responsesAPIModels } from '../../const/models';
import { createRouterRuntime } from '../../core/RouterRuntime';
import { CreateRouterRuntimeOptions } from '../../core/RouterRuntime/createRuntime';
import { ChatStreamPayload } from '../../types/chat';
import { detectModelProvider, processMultiProviderModelList } from '../../utils/modelParse';

export interface NewAPIModelCard {
Expand All @@ -26,18 +25,6 @@ export interface NewAPIPricing {
supported_endpoint_types?: string[];
}

export const handlePayload = (payload: ChatStreamPayload) => {
// Handle OpenAI responses API mode
if (
responsesAPIModels.has(payload.model) ||
payload.model.includes('gpt-') ||
/^o\d/.test(payload.model)
) {
return { ...payload, apiMode: 'responses' };
}
return payload;
};

export const params = {
debug: {
chatCompletion: () => process.env.DEBUG_NEWAPI_CHAT_COMPLETION === '1',
Expand Down Expand Up @@ -178,7 +165,7 @@ export const params = {
...options,
baseURL: urlJoin(userBaseURL, '/v1'),
chatCompletion: {
handlePayload,
useResponseModels: [...Array.from(responsesAPIModels), /gpt-\d(?!\d)/, /^o\d/],
},
},
},
Expand Down
5 changes: 4 additions & 1 deletion src/app/(backend)/webapi/stt/openai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export const POST = async (req: Request) => {
// if resOrOpenAI is a Response, it means there is an error,just return it
if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;

const res = await createOpenaiAudioTranscriptions({ openai: openaiOrErrResponse, payload });
const res = await createOpenaiAudioTranscriptions({
openai: openaiOrErrResponse as any,
payload,
});

return new Response(JSON.stringify(res), {
headers: {
Expand Down
2 changes: 1 addition & 1 deletion src/app/(backend)/webapi/tts/openai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ export const POST = async (req: Request) => {
// if resOrOpenAI is a Response, it means there is an error,just return it
if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;

return await createOpenaiAudioSpeech({ openai: openaiOrErrResponse, payload });
return await createOpenaiAudioSpeech({ openai: openaiOrErrResponse as any, payload });
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Flexbox } from 'react-layout-kit';
import { useAiInfraStore } from '@/store/aiInfra/store';
import { CreateAiProviderParams } from '@/types/aiProvider';

import { CUSTOM_PROVIDER_SDK_OPTIONS } from '../customProviderSdkOptions';
import { KeyVaultsConfigKey, LLMProviderApiTokenKey, LLMProviderBaseUrlKey } from '../../const';
import { CUSTOM_PROVIDER_SDK_OPTIONS } from '../customProviderSdkOptions';

interface CreateNewProviderProps {
onClose?: () => void;
Expand All @@ -42,6 +42,15 @@ const CreateNewProvider = memo<CreateNewProviderProps>(({ onClose, open }) => {
name: values.name || values.id,
};

// 只为 openai 和 router (newapi) 类型的自定义 provider 添加 supportResponsesApi: true
const sdkType = values.settings?.sdkType;
if (sdkType === 'openai' || sdkType === 'router') {
finalValues.settings = {
...finalValues.settings,
supportResponsesApi: true,
};
}

await createNewAiProvider(finalValues);
setLoading(false);
router.push(`/settings?active=provider&provider=${values.id}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const CUSTOM_PROVIDER_SDK_OPTIONS = [
{ label: 'Qwen', value: 'qwen' },
{ label: 'Volcengine', value: 'volcengine' },
{ label: 'Ollama', value: 'ollama' },
{ label: 'New API', value: 'router' },
] satisfies { label: string; value: AiProviderSDKType }[];
1 change: 1 addition & 0 deletions src/config/modelProviders/aihubmix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const AiHubMix: ModelProviderCard = {
settings: {
sdkType: 'router',
showModelFetcher: true,
supportResponsesApi: true,
},
url: 'https://aihubmix.com?utm_source=lobehub',
};
Expand Down
1 change: 1 addition & 0 deletions src/config/modelProviders/newapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const NewAPI: ModelProviderCard = {
},
sdkType: 'router',
showModelFetcher: true,
supportResponsesApi: true,
},
url: 'https://github.com/Calcium-Ion/new-api',
};
Expand Down
2 changes: 1 addition & 1 deletion src/locales/default/modelProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export default {
},
helpDoc: '配置教程',
responsesApi: {
desc: '采用 OpenAI 新一代请求格式规范,解锁思维链等进阶特性',
desc: '采用 OpenAI 新一代请求格式规范,解锁思维链等进阶特性 (仅 OpenAI 模型支持)',
title: '使用 Responses API 规范',
},
waitingForMore: '更多模型正在 <1>计划接入</1> 中,敬请期待',
Expand Down
Loading