mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
feat(core): add o4-mini model (#12175)
Close [AI-85](https://linear.app/affine-design/issue/AI-85) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced AI chat experience with improved web search integration, including a new "AUTO" mode that allows the AI to search the web only when needed. - Updated AI model for chat to "o4-mini" for improved performance. - **Improvements** - Refined instructions to reduce hallucinations and ensure the AI admits uncertainty when unsure. - Enhanced streaming responses to display web search queries and results more clearly. - Updated web search tool modes for more accurate and flexible search behavior. - Centralized AI provider options and improved tool selection logic for better response quality. - **Bug Fixes** - Improved handling of tool calls and reasoning steps in AI chat responses. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -177,6 +177,12 @@ export class AnthropicProvider
|
||||
yield message.textDelta;
|
||||
break;
|
||||
}
|
||||
case 'tool-call': {
|
||||
if (message.toolName === 'web_search') {
|
||||
yield '\n' + `Searching the web "${message.args.query}"` + '\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool-result': {
|
||||
if (message.toolName === 'web_search') {
|
||||
this.toolResults.push(this.getWebSearchLinks(message.result));
|
||||
@@ -219,18 +225,15 @@ export class AnthropicProvider
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getAnthropicOptions(
|
||||
options: CopilotChatOptions
|
||||
): AnthropicProviderOptions {
|
||||
private getAnthropicOptions(options: CopilotChatOptions) {
|
||||
const result: AnthropicProviderOptions = {};
|
||||
if (options?.reasoning) {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budgetTokens: 12000,
|
||||
},
|
||||
result.thinking = {
|
||||
type: 'enabled',
|
||||
budgetTokens: 12000,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
return result;
|
||||
}
|
||||
|
||||
private getWebSearchLinks(
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createOpenAI,
|
||||
openai,
|
||||
type OpenAIProvider as VercelOpenAIProvider,
|
||||
OpenAIResponsesProviderOptions,
|
||||
} from '@ai-sdk/openai';
|
||||
import {
|
||||
AISDKError,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
generateObject,
|
||||
generateText,
|
||||
streamText,
|
||||
ToolSet,
|
||||
} from 'ai';
|
||||
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
metrics,
|
||||
UserFriendlyError,
|
||||
} from '../../../base';
|
||||
import { createExaTool } from '../tools';
|
||||
import { CopilotProvider } from './provider';
|
||||
import {
|
||||
ChatMessageRole,
|
||||
@@ -42,6 +43,11 @@ export type OpenAIConfig = {
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
type OpenAITools = {
|
||||
web_search_preview: ReturnType<typeof openai.tools.webSearchPreview>;
|
||||
web_search: ReturnType<typeof createExaTool>;
|
||||
};
|
||||
|
||||
export class OpenAIProvider
|
||||
extends CopilotProvider<OpenAIConfig>
|
||||
implements
|
||||
@@ -68,7 +74,7 @@ export class OpenAIProvider
|
||||
'gpt-4.1-2025-04-14',
|
||||
'gpt-4.1-mini',
|
||||
'o1',
|
||||
'o3-mini',
|
||||
'o4-mini',
|
||||
// embeddings
|
||||
'text-embedding-3-large',
|
||||
'text-embedding-3-small',
|
||||
@@ -81,6 +87,8 @@ export class OpenAIProvider
|
||||
'gpt-image-1',
|
||||
];
|
||||
|
||||
private readonly MAX_STEPS = 20;
|
||||
|
||||
#instance!: VercelOpenAIProvider;
|
||||
|
||||
override configured(): boolean {
|
||||
@@ -179,21 +187,28 @@ export class OpenAIProvider
|
||||
}
|
||||
}
|
||||
|
||||
private getTools(options: CopilotChatOptions): ToolSet | undefined {
|
||||
private getTools(
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
): Partial<OpenAITools> {
|
||||
const tools: Partial<OpenAITools> = {};
|
||||
if (options?.tools?.length) {
|
||||
const tools: ToolSet = {};
|
||||
for (const tool of options.tools) {
|
||||
switch (tool) {
|
||||
case 'webSearch': {
|
||||
tools.web_search_preview = openai.tools.webSearchPreview();
|
||||
// o series reasoning models
|
||||
if (model.startsWith('o')) {
|
||||
tools.web_search = createExaTool(this.AFFiNEConfig);
|
||||
} else {
|
||||
tools.web_search_preview = openai.tools.webSearchPreview();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return tools;
|
||||
}
|
||||
|
||||
// ====== text to text ======
|
||||
@@ -231,10 +246,10 @@ export class OpenAIProvider
|
||||
: await generateText({
|
||||
...commonParams,
|
||||
providerOptions: {
|
||||
openai: options.user ? { user: options.user } : {},
|
||||
openai: this.getOpenAIOptions(options),
|
||||
},
|
||||
toolChoice: options.webSearch ? 'required' : 'auto',
|
||||
tools: this.getTools(options),
|
||||
tools: this.getTools(options, model),
|
||||
maxSteps: this.MAX_STEPS,
|
||||
});
|
||||
|
||||
return text.trim();
|
||||
@@ -258,14 +273,16 @@ export class OpenAIProvider
|
||||
|
||||
const modelInstance = this.#instance.responses(model);
|
||||
|
||||
const tools = this.getTools(options, model);
|
||||
const { fullStream } = streamText({
|
||||
model: modelInstance,
|
||||
system,
|
||||
messages: msgs,
|
||||
tools: this.getTools(options),
|
||||
providerOptions: {
|
||||
openai: options.user ? { user: options.user } : {},
|
||||
openai: this.getOpenAIOptions(options),
|
||||
},
|
||||
tools: tools as OpenAITools,
|
||||
maxSteps: this.MAX_STEPS,
|
||||
frequencyPenalty: options.frequencyPenalty || 0,
|
||||
presencePenalty: options.presencePenalty || 0,
|
||||
temperature: options.temperature || 0,
|
||||
@@ -274,25 +291,53 @@ export class OpenAIProvider
|
||||
});
|
||||
|
||||
const parser = new CitationParser();
|
||||
let lastType;
|
||||
for await (const chunk of fullStream) {
|
||||
if (chunk) {
|
||||
switch (chunk.type) {
|
||||
case 'text-delta': {
|
||||
if (lastType !== chunk.type) {
|
||||
yield `\n`;
|
||||
}
|
||||
const result = parser.parse(chunk.textDelta);
|
||||
yield result;
|
||||
break;
|
||||
}
|
||||
case 'reasoning': {
|
||||
if (lastType !== chunk.type) {
|
||||
yield `\n`;
|
||||
}
|
||||
yield chunk.textDelta;
|
||||
break;
|
||||
}
|
||||
case 'tool-call': {
|
||||
if (chunk.toolName === 'web_search') {
|
||||
yield '\n' + `Searching the web "${chunk.args.query}"` + '\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool-result': {
|
||||
if (chunk.toolName === 'web_search') {
|
||||
yield '\n' + this.getWebSearchLinks(chunk.result) + '\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'step-finish': {
|
||||
const result = parser.end();
|
||||
yield result;
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
const error = chunk.error as { type: string; message: string };
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
await fullStream.cancel();
|
||||
break;
|
||||
}
|
||||
lastType = chunk.type;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -380,4 +425,28 @@ export class OpenAIProvider
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private getOpenAIOptions(options: CopilotChatOptions) {
|
||||
const result: OpenAIResponsesProviderOptions = {};
|
||||
if (options?.reasoning) {
|
||||
result.reasoningEffort = 'medium';
|
||||
result.reasoningSummary = 'auto';
|
||||
}
|
||||
if (options?.user) {
|
||||
result.user = options.user;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getWebSearchLinks(
|
||||
list: {
|
||||
title: string | null;
|
||||
url: string;
|
||||
}[]
|
||||
): string {
|
||||
const links = list.reduce((acc, result) => {
|
||||
return acc + `\n[${result.title ?? result.url}](${result.url})\n`;
|
||||
}, '\n');
|
||||
return links + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user