@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
1,002 lines (865 loc) • 34 kB
text/typescript
import type { ChatModelCard } from '@lobechat/types';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import debug from 'debug';
import { LOBE_DEFAULT_MODEL_LIST } from 'model-bank';
import type { AiModelType } from 'model-bank';
import OpenAI, { ClientOptions } from 'openai';
import { Stream } from 'openai/streaming';
import { responsesAPIModels } from '../../const/models';
import {
ChatCompletionErrorPayload,
ChatCompletionTool,
ChatMethodOptions,
ChatStreamCallbacks,
ChatStreamPayload,
Embeddings,
EmbeddingsOptions,
EmbeddingsPayload,
GenerateObjectOptions,
GenerateObjectPayload,
TextToImagePayload,
TextToSpeechOptions,
TextToSpeechPayload,
} from '../../types';
import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../../types/error';
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
import { AgentRuntimeError } from '../../utils/createError';
import { debugResponse, debugStream } from '../../utils/debugStream';
import { desensitizeUrl } from '../../utils/desensitizeUrl';
import { getModelPropertyWithFallback } from '../../utils/getFallbackModelProperty';
import { getModelPricing } from '../../utils/getModelPricing';
import { handleOpenAIError } from '../../utils/handleOpenAIError';
import { postProcessModelList } from '../../utils/postProcessModelList';
import { StreamingResponse } from '../../utils/response';
import { LobeRuntimeAI } from '../BaseAI';
import { convertOpenAIMessages, convertOpenAIResponseInputs } from '../contextBuilders/openai';
import { OpenAIResponsesStream, OpenAIStream, OpenAIStreamOptions } from '../streams';
import { createOpenAICompatibleImage } from './createImage';
import { transformResponseAPIToStream, transformResponseToStream } from './nonStreamToStream';
export * from './nonStreamToStream';
// the model contains the following keywords is not a chat model, so we should filter them out
export const CHAT_MODELS_BLOCK_LIST = [
'embedding',
'davinci',
'curie',
'moderation',
'ada',
'babbage',
'tts',
'whisper',
'dall-e',
];
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
export type CreateImageOptions = Omit<ClientOptions, 'apiKey'> & {
apiKey: string;
provider: string;
};
export interface CustomClientOptions<T extends Record<string, any> = any> {
createChatCompletionStream?: (
client: any,
payload: ChatStreamPayload,
instance: any,
) => ReadableStream<any>;
createClient?: (options: ConstructorOptions<T>) => any;
}
export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
apiKey?: string;
baseURL?: string;
chatCompletion?: {
excludeUsage?: boolean;
handleError?: (
error: any,
options: ConstructorOptions<T>,
) => Omit<ChatCompletionErrorPayload, 'provider'> | undefined;
handlePayload?: (
payload: ChatStreamPayload,
options: ConstructorOptions<T>,
) => OpenAI.ChatCompletionCreateParamsStreaming;
handleStream?: (
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number },
) => ReadableStream;
handleStreamBizErrorType?: (error: {
message: string;
name: string;
}) => ILobeAgentRuntimeErrorType | undefined;
handleTransformResponseToStream?: (
data: OpenAI.ChatCompletion,
) => ReadableStream<OpenAI.ChatCompletionChunk>;
noUserId?: boolean;
/**
* If true, route chat requests to Responses API path directly
*/
useResponse?: boolean;
/**
* Allow only some models to use Responses API by simple matching.
* If any string appears in model id or RegExp matches, Responses API is used.
*/
useResponseModels?: Array<string | RegExp>;
};
constructorOptions?: ConstructorOptions<T>;
createImage?: (
payload: CreateImagePayload,
options: CreateImageOptions,
) => Promise<CreateImageResponse>;
customClient?: CustomClientOptions<T>;
debug?: {
chatCompletion: () => boolean;
responses?: () => boolean;
};
errorType?: {
bizError: ILobeAgentRuntimeErrorType;
invalidAPIKey: ILobeAgentRuntimeErrorType;
};
generateObject?: {
/**
* Transform schema before sending to the provider (e.g., filter unsupported properties)
*/
handleSchema?: (schema: any) => any;
/**
* If true, route generateObject requests to Responses API path directly
*/
useResponse?: boolean;
/**
* Allow only some models to use Responses API by simple matching.
* If any string appears in model id or RegExp matches, Responses API is used.
*/
useResponseModels?: Array<string | RegExp>;
/**
* Use tool calling to simulate structured output for providers that don't support native structured output
*/
useToolsCalling?: boolean;
};
models?:
| ((params: { client: OpenAI }) => Promise<ChatModelCard[]>)
| {
transformModel?: (model: OpenAI.Model) => ChatModelCard;
};
provider: string;
responses?: {
handlePayload?: (
payload: ChatStreamPayload,
options: ConstructorOptions<T>,
) => ChatStreamPayload;
};
}
export const createOpenAICompatibleRuntime = <T extends Record<string, any> = any>({
provider,
baseURL: DEFAULT_BASE_URL,
apiKey: DEFAULT_API_LEY,
errorType,
debug: debugParams,
constructorOptions,
chatCompletion,
models,
customClient,
responses,
createImage: customCreateImage,
generateObject: generateObjectConfig,
}: OpenAICompatibleFactoryOptions<T>) => {
const ErrorType = {
bizError: errorType?.bizError || AgentRuntimeErrorType.ProviderBizError,
invalidAPIKey: errorType?.invalidAPIKey || AgentRuntimeErrorType.InvalidProviderAPIKey,
};
return class LobeOpenAICompatibleAI implements LobeRuntimeAI {
client!: OpenAI;
private id: string;
private logPrefix: string;
baseURL!: string;
protected _options: ConstructorOptions<T>;
constructor(options: ClientOptions & Record<string, any> = {}) {
const _options = {
...options,
apiKey: options.apiKey?.trim() || DEFAULT_API_LEY,
baseURL: options.baseURL?.trim() || DEFAULT_BASE_URL,
};
const { apiKey, baseURL = DEFAULT_BASE_URL, ...res } = _options;
this._options = _options as ConstructorOptions<T>;
if (!apiKey) throw AgentRuntimeError.createError(ErrorType?.invalidAPIKey);
const initOptions = { apiKey, baseURL, ...constructorOptions, ...res };
// if the custom client is provided, use it as client
if (customClient?.createClient) {
this.client = customClient.createClient(initOptions as any);
} else {
this.client = new OpenAI(initOptions);
}
this.baseURL = baseURL || this.client.baseURL;
this.id = options.id || provider;
this.logPrefix = `lobe-model-runtime:${this.id}`;
}
async chat({ responseMode, ...payload }: ChatStreamPayload, options?: ChatMethodOptions) {
try {
const log = debug(`${this.logPrefix}:chat`);
const inputStartAt = Date.now();
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 = (() => {
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;
} else {
log('using Chat Completions API mode');
}
// 再进行工厂级处理
const postPayload = chatCompletion?.handlePayload
? chatCompletion.handlePayload(processedPayload, this._options)
: ({
...processedPayload,
stream: processedPayload.stream ?? true,
} as OpenAI.ChatCompletionCreateParamsStreaming);
if ((postPayload as any).apiMode === 'responses') {
return this.handleResponseAPIMode(processedPayload, options);
}
const messages = await convertOpenAIMessages(postPayload.messages);
let response: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
const streamOptions: OpenAIStreamOptions = {
bizErrorTypeTransformer: chatCompletion?.handleStreamBizErrorType,
callbacks: options?.callback,
payload: {
model: payload.model,
pricing: await getModelPricing(payload.model, this.id),
provider: this.id,
},
};
if (customClient?.createChatCompletionStream) {
log('using custom client for chat completion stream');
response = customClient.createChatCompletionStream(
this.client,
processedPayload,
this,
) as any;
} else {
const finalPayload = {
...postPayload,
messages,
...(chatCompletion?.noUserId ? {} : { user: options?.user }),
stream_options:
postPayload.stream && !chatCompletion?.excludeUsage
? { include_usage: true }
: undefined,
};
log('sending chat completion request with %d messages', messages.length);
if (debugParams?.chatCompletion?.()) {
console.log('[requestPayload]');
console.log(JSON.stringify(finalPayload), '\n');
}
response = await this.client.chat.completions.create(finalPayload, {
// https://github.com/lobehub/lobe-chat/pull/318
headers: { Accept: '*/*', ...options?.requestHeaders },
signal: options?.signal,
});
}
if (postPayload.stream) {
log('processing streaming response');
const [prod, useForDebug] = response.tee();
if (debugParams?.chatCompletion?.()) {
const useForDebugStream =
useForDebug instanceof ReadableStream ? useForDebug : useForDebug.toReadableStream();
debugStream(useForDebugStream).catch(console.error);
}
return StreamingResponse(
chatCompletion?.handleStream
? chatCompletion.handleStream(prod, {
callbacks: streamOptions.callbacks,
inputStartAt,
})
: OpenAIStream(prod, {
...streamOptions,
inputStartAt,
}),
{
headers: options?.headers,
},
);
}
if (debugParams?.chatCompletion?.()) {
debugResponse(response);
}
if (responseMode === 'json') {
log('returning JSON response mode');
return Response.json(response);
}
log('transforming non-streaming response to stream');
const transformHandler =
chatCompletion?.handleTransformResponseToStream || transformResponseToStream;
const stream = transformHandler(response as unknown as OpenAI.ChatCompletion);
return StreamingResponse(
chatCompletion?.handleStream
? chatCompletion.handleStream(stream, {
callbacks: streamOptions.callbacks,
inputStartAt,
})
: OpenAIStream(stream, { ...streamOptions, enableStreaming: false, inputStartAt }),
{
headers: options?.headers,
},
);
} catch (error) {
throw this.handleError(error);
}
}
async createImage(payload: CreateImagePayload) {
const log = debug(`${this.logPrefix}:createImage`);
// If custom createImage implementation is provided, use it
if (customCreateImage) {
log('using custom createImage implementation');
return customCreateImage(payload, {
...this._options,
apiKey: this._options.apiKey!,
provider,
});
}
log('using default createOpenAICompatibleImage');
// Use the new createOpenAICompatibleImage function
return createOpenAICompatibleImage(this.client, payload, this.id);
}
async models() {
const log = debug(`${this.logPrefix}:models`);
log('fetching available models');
let resultModels: ChatModelCard[] = [];
if (typeof models === 'function') {
log('using custom models function');
resultModels = await models({ client: this.client });
} else {
log('fetching models from client API');
const list = await this.client.models.list();
resultModels = list.data
.filter((model) => {
return CHAT_MODELS_BLOCK_LIST.every(
(keyword) => !model.id.toLowerCase().includes(keyword),
);
})
.map((item) => {
if (models?.transformModel) {
return models.transformModel(item);
}
const toReleasedAt = () => {
if (!item.created) return;
dayjs.extend(utc);
// guarantee item.created in Date String format
if (
typeof (item.created as any) === 'string' ||
// or in milliseconds
item.created.toFixed(0).length === 13
) {
return dayjs.utc(item.created).format('YYYY-MM-DD');
}
// by default, the created time is in seconds
return dayjs.utc(item.created * 1000).format('YYYY-MM-DD');
};
// TODO: should refactor after remove v1 user/modelList code
const knownModel = LOBE_DEFAULT_MODEL_LIST.find((model) => model.id === item.id);
if (knownModel) {
const releasedAt = knownModel.releasedAt ?? toReleasedAt();
return { ...knownModel, releasedAt };
}
return {
id: item.id,
releasedAt: toReleasedAt(),
};
})
.filter(Boolean) as ChatModelCard[];
}
log('fetched %d models', resultModels.length);
return await postProcessModelList(resultModels, (modelId) =>
getModelPropertyWithFallback<AiModelType>(modelId, 'type'),
);
}
async generateObject(payload: GenerateObjectPayload, options?: GenerateObjectOptions) {
const { messages, schema, model, responseApi, tools } = payload;
const log = debug(`${this.logPrefix}:generateObject`);
log(
'generateObject called with model: %s, hasTools: %s, hasSchema: %s',
model,
!!tools,
!!schema,
);
if (tools) {
log('using tools-based generation');
return this.generateObjectWithTools(payload, options);
}
if (!schema) throw new Error('tools or schema is required');
// Use tool calling fallback if configured
if (generateObjectConfig?.useToolsCalling) {
log('using tool calling fallback for structured output');
// Apply schema transformation if configured
const processedSchema = generateObjectConfig.handleSchema
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
: schema;
const tool: ChatCompletionTool = {
function: {
description:
processedSchema.description ||
'Generate structured output according to the provided schema',
name: processedSchema.name || 'structured_output',
parameters: processedSchema.schema,
},
type: 'function',
};
const res = await this.client.chat.completions.create(
{
messages,
model,
tool_choice: { function: { name: tool.function.name }, type: 'function' },
tools: [tool],
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
);
const toolCalls = res.choices[0].message.tool_calls!;
try {
return toolCalls.map((item) => ({
arguments: JSON.parse(item.function.arguments),
name: item.function.name,
}));
} catch {
console.error('parse tool call arguments error:', toolCalls);
return undefined;
}
}
// Factory-level Responses API routing control (supports instance override)
const shouldUseResponses = (() => {
const instanceGenerateObject = ((this._options as any).generateObject || {}) as {
useResponse?: boolean;
useResponseModels?: Array<string | RegExp>;
};
const flagUseResponse =
instanceGenerateObject.useResponse ??
(generateObjectConfig ? generateObjectConfig.useResponse : undefined);
const flagUseResponseModels =
instanceGenerateObject.useResponseModels ?? generateObjectConfig?.useResponseModels;
if (responseApi) {
log('using Responses API due to explicit responseApi flag');
return true;
}
if (flagUseResponse) {
log('using Responses API due to useResponse flag');
return true;
}
// Use factory-configured model list if provided
if (model && flagUseResponseModels?.length) {
const matches = flagUseResponseModels.some((m: string | RegExp) =>
typeof m === 'string' ? model.includes(m) : (m as RegExp).test(model),
);
if (matches) {
log('using Responses API: model %s matches useResponseModels config', model);
return true;
}
}
// Default: use built-in responsesAPIModels
if (model && responsesAPIModels.has(model)) {
log('using Responses API: model %s in built-in responsesAPIModels', model);
return true;
}
log('using Chat Completions API for generateObject');
return false;
})();
// Apply schema transformation if configured
const processedSchema = generateObjectConfig?.handleSchema
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
: schema;
if (shouldUseResponses) {
log('calling responses.create for structured output');
const res = await this.client!.responses.create(
{
input: messages,
model,
text: { format: { strict: true, type: 'json_schema', ...processedSchema } },
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
);
const text = res.output_text;
log('received structured output from Responses API, length: %d', text?.length || 0);
try {
const result = JSON.parse(text);
log('successfully parsed JSON output');
return result;
} catch (error) {
log('failed to parse JSON output: %O', error);
console.error('parse json error:', text);
return undefined;
}
}
log('calling chat.completions.create for structured output');
const res = await this.client.chat.completions.create(
{
messages,
model,
response_format: { json_schema: processedSchema, type: 'json_schema' },
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
);
const text = res.choices[0].message.content!;
log('received structured output from Chat Completions API, length: %d', text?.length || 0);
try {
const result = JSON.parse(text);
log('successfully parsed JSON output');
return result;
} catch (error) {
log('failed to parse JSON output: %O', error);
console.error('parse json error:', text);
return undefined;
}
}
async embeddings(
payload: EmbeddingsPayload,
options?: EmbeddingsOptions,
): Promise<Embeddings[]> {
const log = debug(`${this.logPrefix}:embeddings`);
log(
'embeddings called with model: %s, input items: %d',
payload.model,
Array.isArray(payload.input) ? payload.input.length : 1,
);
try {
const res = await this.client.embeddings.create(
{ ...payload, encoding_format: 'float', user: options?.user },
{ headers: options?.headers, signal: options?.signal },
);
log('received %d embeddings', res.data.length);
return res.data.map((item) => item.embedding);
} catch (error) {
throw this.handleError(error);
}
}
async textToImage(payload: TextToImagePayload) {
const log = debug(`${this.logPrefix}:textToImage`);
log('textToImage called with prompt length: %d', payload.prompt?.length || 0);
try {
const res = await this.client.images.generate(payload);
log('generated %d images', res.data?.length || 0);
return (res.data || []).map((o) => o.url) as string[];
} catch (error) {
throw this.handleError(error);
}
}
async textToSpeech(payload: TextToSpeechPayload, options?: TextToSpeechOptions) {
const log = debug(`${this.logPrefix}:textToSpeech`);
log(
'textToSpeech called with input length: %d, voice: %s',
payload.input?.length || 0,
payload.voice,
);
try {
const mp3 = await this.client.audio.speech.create(payload as any, {
headers: options?.headers,
signal: options?.signal,
});
const buffer = await mp3.arrayBuffer();
log('generated audio with size: %d bytes', buffer.byteLength);
return buffer;
} catch (error) {
throw this.handleError(error);
}
}
protected handleError(error: any): ChatCompletionErrorPayload {
const log = debug(`${this.logPrefix}:error`);
log('handling error: %O', error);
let desensitizedEndpoint = this.baseURL;
// refs: https://github.com/lobehub/lobe-chat/issues/842
if (this.baseURL !== DEFAULT_BASE_URL) {
desensitizedEndpoint = desensitizeUrl(this.baseURL);
}
if (chatCompletion?.handleError) {
log('using custom error handler');
const errorResult = chatCompletion.handleError(error, this._options);
if (errorResult)
return AgentRuntimeError.chat({
...errorResult,
provider: this.id,
} as ChatCompletionErrorPayload);
}
if ('status' in (error as any)) {
const status = (error as Response).status;
log('HTTP error with status: %d', status);
switch (status) {
case 401: {
log('invalid API key error');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: error as any,
errorType: ErrorType.invalidAPIKey,
provider: this.id,
});
}
default: {
break;
}
}
}
const { errorResult, RuntimeError } = handleOpenAIError(error);
log('error code: %s, message: %s', errorResult.code, errorResult.message);
// Check for "Insufficient Balance" in error message
const errorMessage = errorResult.error?.message || errorResult.message;
if (errorMessage?.includes('Insufficient Balance')) {
log('insufficient balance error detected in message');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.InsufficientQuota,
provider: this.id,
});
}
switch (errorResult.code) {
case 'insufficient_quota': {
log('insufficient quota error');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.InsufficientQuota,
provider: this.id,
});
}
case 'model_not_found': {
log('model not found error');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.ModelNotFound,
provider: this.id,
});
}
// content too long
case 'context_length_exceeded':
case 'string_above_max_length': {
log('context length exceeded error');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: AgentRuntimeErrorType.ExceededContextWindow,
provider: this.id,
});
}
}
log('returning generic error');
return AgentRuntimeError.chat({
endpoint: desensitizedEndpoint,
error: errorResult,
errorType: RuntimeError || ErrorType.bizError,
provider: this.id,
});
}
private async handleResponseAPIMode(
payload: ChatStreamPayload,
options?: ChatMethodOptions,
): Promise<Response> {
const log = debug(`${this.logPrefix}:handleResponseAPIMode`);
log('handleResponseAPIMode called with model: %s', payload.model);
const inputStartAt = Date.now();
const { messages, reasoning_effort, tools, reasoning, responseMode, ...res } =
responses?.handlePayload
? (responses?.handlePayload(payload, this._options) as ChatStreamPayload)
: payload;
// remove penalty params
delete res.apiMode;
delete res.frequency_penalty;
delete res.presence_penalty;
const input = await convertOpenAIResponseInputs(messages as any);
const isStreaming = payload.stream !== false;
log(
'isStreaming: %s, hasTools: %s, hasReasoning: %s',
isStreaming,
!!tools,
!!(reasoning || reasoning_effort),
);
const postPayload = {
...res,
...(reasoning || reasoning_effort
? {
reasoning: {
...reasoning,
...(reasoning_effort && { effort: reasoning_effort }),
},
}
: {}),
input,
store: false,
stream: !isStreaming ? undefined : isStreaming,
tools: tools?.map((tool) => this.convertChatCompletionToolToResponseTool(tool)),
} as OpenAI.Responses.ResponseCreateParamsStreaming | OpenAI.Responses.ResponseCreateParams;
if (debugParams?.responses?.()) {
console.log('[requestPayload]');
console.log(JSON.stringify(postPayload), '\n');
}
log('sending responses.create request');
const response = await this.client.responses.create(postPayload, {
headers: options?.requestHeaders,
signal: options?.signal,
});
const streamOptions: OpenAIStreamOptions = {
bizErrorTypeTransformer: chatCompletion?.handleStreamBizErrorType,
callbacks: options?.callback,
payload: {
model: payload.model,
pricing: await getModelPricing(payload.model, this.id),
provider: this.id,
},
};
if (isStreaming) {
log('processing streaming Responses API response');
const stream = response as Stream<OpenAI.Responses.ResponseStreamEvent>;
const [prod, useForDebug] = stream.tee();
if (debugParams?.responses?.()) {
const useForDebugStream =
useForDebug instanceof ReadableStream ? useForDebug : useForDebug.toReadableStream();
debugStream(useForDebugStream).catch(console.error);
}
return StreamingResponse(OpenAIResponsesStream(prod, { ...streamOptions, inputStartAt }), {
headers: options?.headers,
});
}
log('processing non-streaming Responses API response');
// Handle non-streaming response
if (debugParams?.responses?.()) {
debugResponse(response);
}
if (responseMode === 'json') {
log('returning JSON response mode');
return Response.json(response);
}
log('transforming non-streaming Responses API response to stream');
const stream = transformResponseAPIToStream(response as OpenAI.Responses.Response);
return StreamingResponse(
OpenAIResponsesStream(stream, { ...streamOptions, enableStreaming: false, inputStartAt }),
{
headers: options?.headers,
},
);
}
private convertChatCompletionToolToResponseTool = (
tool: ChatCompletionTool,
): OpenAI.Responses.Tool => {
return { type: tool.type, ...tool.function } as any;
};
private async generateObjectWithTools(
payload: GenerateObjectPayload,
options?: GenerateObjectOptions,
) {
const { messages, model, tools, responseApi } = payload;
const log = debug(`${this.logPrefix}:generateObject`);
log(
'generateObjectWithTools called with model: %s, toolsCount: %d',
model,
tools?.length || 0,
);
// Factory-level Responses API routing control (supports instance override)
const shouldUseResponses = (() => {
const instanceGenerateObject = ((this._options as any).generateObject || {}) as {
useResponse?: boolean;
useResponseModels?: Array<string | RegExp>;
};
const flagUseResponse =
instanceGenerateObject.useResponse ??
(generateObjectConfig ? generateObjectConfig.useResponse : undefined);
const flagUseResponseModels =
instanceGenerateObject.useResponseModels ?? generateObjectConfig?.useResponseModels;
if (responseApi) {
log('using Responses API due to explicit responseApi flag');
return true;
}
if (flagUseResponse) {
log('using Responses API due to useResponse flag');
return true;
}
// Use factory-configured model list if provided
if (model && flagUseResponseModels?.length) {
const matches = flagUseResponseModels.some((m: string | RegExp) =>
typeof m === 'string' ? model.includes(m) : (m as RegExp).test(model),
);
if (matches) {
log('using Responses API: model %s matches useResponseModels config', model);
return true;
}
}
// Default: use built-in responsesAPIModels
if (model && responsesAPIModels.has(model)) {
log('using Responses API: model %s in built-in responsesAPIModels', model);
return true;
}
log('using Chat Completions API for tool calling');
return false;
})();
if (shouldUseResponses) {
log('calling responses.create for tool calling');
const input = await convertOpenAIResponseInputs(messages as any);
const res = await this.client.responses.create(
{
input,
model,
tool_choice: 'required',
tools: tools!.map((tool) => this.convertChatCompletionToolToResponseTool(tool)),
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
);
const functionCalls = res.output?.filter((item: any) => item.type === 'function_call');
log('received %d function calls from Responses API', functionCalls?.length || 0);
try {
const result = functionCalls?.map((item: any) => ({
arguments:
typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments,
name: item.name,
}));
log(
'successfully parsed function calls: %O',
result?.map((r) => r.name),
);
return result;
} catch (error) {
log('failed to parse tool call arguments: %O', error);
console.error('parse tool call arguments error:', res);
return undefined;
}
}
log('calling chat.completions.create for tool calling');
const msgs = messages;
const res = await this.client.chat.completions.create(
{
messages: msgs,
model,
tool_choice: 'required',
tools,
user: options?.user,
},
{ headers: options?.headers, signal: options?.signal },
);
const toolCalls = res.choices[0].message.tool_calls!;
log('received %d tool calls from Chat Completions API', toolCalls?.length || 0);
try {
const result = toolCalls.map((item) => ({
arguments: JSON.parse(item.function.arguments),
name: item.function.name,
}));
log(
'successfully parsed tool calls: %O',
result.map((r) => r.name),
);
return result;
} catch (error) {
log('failed to parse tool call arguments: %O', error);
console.error('parse tool call arguments error:', res);
return undefined;
}
}
};
};