feat(core): add gemini callout syntax highlighting (#12413)

Close [AI-125](https://linear.app/affine-design/issue/AI-125)

What Changed?
- Add `gemini-2.5-flash-preview-04-17` model
- Add `thinkingConfig` provider options
- Add callout syntax highlighting

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for the "Gemini 2.5 Flash" model and updated the "Gemini 2.5 Pro" model to a newer version.
  - Enhanced streaming responses to better format reasoning outputs and provide clearer callouts in AI-generated content.

- **Bug Fixes**
  - Improved audio transcription prompts in test cases for more accurate and explicit testing.

- **Documentation**
  - Expanded citation instructions for AI chat responses, including examples for multiple citations.

- **Chores**
  - Updated the "@ai-sdk/google" dependency to a newer version.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
akumatus
2025-05-23 01:58:01 +00:00
parent 0ce05ca96e
commit 119cf9442b
7 changed files with 101 additions and 19 deletions
@@ -232,7 +232,7 @@ export class AnthropicProvider extends CopilotProvider<AnthropicConfig> {
private getAnthropicOptions(options: CopilotChatOptions, model: string) {
const result: AnthropicProviderOptions = {};
if (options?.reasoning && this.isThinkingModel(model)) {
if (options?.reasoning && this.isReasoningModel(model)) {
result.thinking = {
type: 'enabled',
budgetTokens: 12000,
@@ -257,7 +257,8 @@ export class AnthropicProvider extends CopilotProvider<AnthropicConfig> {
return text.replaceAll('\n', '\n> ');
}
private isThinkingModel(model: string) {
private isReasoningModel(model: string) {
// only claude 3.7 sonnet supports reasoning config
return model.startsWith('claude-3-7-sonnet');
}
}
@@ -1,6 +1,7 @@
import {
createGoogleGenerativeAI,
type GoogleGenerativeAIProvider,
type GoogleGenerativeAIProviderOptions,
} from '@ai-sdk/google';
import {
AISDKError,
@@ -52,9 +53,23 @@ export class GeminiProvider extends CopilotProvider<GeminiConfig> {
},
],
},
{
name: 'Gemini 2.5 Flash',
id: 'gemini-2.5-flash-preview-04-17',
capabilities: [
{
input: [
ModelInputType.Text,
ModelInputType.Image,
ModelInputType.Audio,
],
output: [ModelOutputType.Text, ModelOutputType.Structured],
},
],
},
{
name: 'Gemini 2.5 Pro',
id: 'gemini-2.5-pro-preview-03-25',
id: 'gemini-2.5-pro-preview-05-06',
capabilities: [
{
input: [
@@ -78,6 +93,10 @@ export class GeminiProvider extends CopilotProvider<GeminiConfig> {
},
];
private readonly MAX_STEPS = 20;
private readonly CALLOUT_PREFIX = '\n> [!]\n> ';
#instance!: GoogleGenerativeAIProvider;
override configured(): boolean {
@@ -203,20 +222,55 @@ export class GeminiProvider extends CopilotProvider<GeminiConfig> {
metrics.ai.counter('chat_text_stream_calls').add(1, { model: model.id });
const [system, msgs] = await chatToGPTMessage(messages);
const { textStream } = streamText({
model: this.#instance(model.id),
const { fullStream } = streamText({
model: this.#instance(model.id, {
useSearchGrounding: this.useSearchGrounding(options),
}),
system,
messages: msgs,
abortSignal: options.signal,
maxSteps: this.MAX_STEPS,
providerOptions: {
google: this.getGeminiOptions(options, model.id),
},
});
for await (const message of textStream) {
if (message) {
yield message;
let lastType;
// reasoning, tool-call, tool-result need to mark as callout
let prefix: string | null = this.CALLOUT_PREFIX;
for await (const chunk of fullStream) {
if (chunk) {
switch (chunk.type) {
case 'text-delta': {
let result = chunk.textDelta;
if (lastType !== chunk.type) {
result = '\n\n' + result;
}
yield result;
break;
}
case 'reasoning': {
if (prefix) {
yield prefix;
prefix = null;
}
let result = chunk.textDelta;
if (lastType !== chunk.type) {
result = '\n\n' + result;
}
yield this.markAsCallout(result);
break;
}
case 'error': {
const error = chunk.error as { type: string; message: string };
throw new Error(error.message);
}
}
if (options.signal?.aborted) {
await textStream.cancel();
await fullStream.cancel();
break;
}
lastType = chunk.type;
}
}
} catch (e: any) {
@@ -224,4 +278,27 @@ export class GeminiProvider extends CopilotProvider<GeminiConfig> {
throw this.handleError(e);
}
}
private getGeminiOptions(options: CopilotChatOptions, model: string) {
const result: GoogleGenerativeAIProviderOptions = {};
if (options?.reasoning && this.isReasoningModel(model)) {
result.thinkingConfig = {
thinkingBudget: 12000,
includeThoughts: true,
};
}
return result;
}
private markAsCallout(text: string) {
return text.replaceAll('\n', '\n> ');
}
private isReasoningModel(model: string) {
return model.startsWith('gemini-2.5');
}
private useSearchGrounding(options: CopilotChatOptions) {
return options?.tools?.includes('webSearch');
}
}