Skip to content

Commit 792b9a4

Browse files
committed
fix: annotation schema to support file annotations
- Define a new `annotations` schema with support for `url_citation` and `file` types. - Adjust chat handling logic to parse and accommodate file annotations. - Update tests for both `url_citation` and `file` annotations.
1 parent b23f2d5 commit 792b9a4

File tree

4 files changed

+116
-39
lines changed

4 files changed

+116
-39
lines changed

src/chat/index.test.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { LanguageModelV2Prompt } from '@ai-sdk/provider';
22
import type { ReasoningDetailUnion } from '../schemas/reasoning-details';
3+
import type { AnnotationUnion } from '../schemas/annotations';
34

45
import {
56
convertReadableStreamToArray,
@@ -131,6 +132,7 @@ describe('doGenerate', () => {
131132
reasoning,
132133
reasoning_details,
133134
images,
135+
annotations,
134136
usage = {
135137
prompt_tokens: 4,
136138
total_tokens: 34,
@@ -143,6 +145,7 @@ describe('doGenerate', () => {
143145
reasoning?: string;
144146
reasoning_details?: Array<ReasoningDetailUnion>;
145147
images?: Array<ImageResponse>;
148+
annotations?: Array<AnnotationUnion>;
146149
usage?: {
147150
prompt_tokens: number;
148151
total_tokens: number;
@@ -175,6 +178,7 @@ describe('doGenerate', () => {
175178
reasoning,
176179
reasoning_details,
177180
images,
181+
annotations,
178182
},
179183
logprobs,
180184
finish_reason,
@@ -612,6 +616,73 @@ describe('doGenerate', () => {
612616
},
613617
]);
614618
});
619+
620+
it('should handle file annotations', async () => {
621+
prepareJsonResponse({
622+
content: 'Here is the document summary.',
623+
annotations: [
624+
{
625+
type: 'file',
626+
file: {
627+
hash: '666ad042331aa9cfd0a5d334fc96825be408e71a428f7508225d99d4354e202c',
628+
name: 'document.pdf',
629+
content: [{ type: 'text', text: 'Sample document content' }],
630+
},
631+
},
632+
],
633+
});
634+
635+
const result = await model.doGenerate({
636+
prompt: TEST_PROMPT,
637+
});
638+
639+
expect(result.content).toStrictEqual([
640+
{
641+
type: 'text',
642+
text: 'Here is the document summary.',
643+
},
644+
]);
645+
});
646+
647+
it('should handle url_citation annotations', async () => {
648+
prepareJsonResponse({
649+
content: 'Here is some information from a source.',
650+
annotations: [
651+
{
652+
type: 'url_citation',
653+
url_citation: {
654+
end_index: 414,
655+
start_index: 327,
656+
title: 'What to see at Berlin Art Week 2025 | Wallpaper*',
657+
url: 'https://www.wallpaper.com/art/exhibitions-shows/berlin-art-week-2025',
658+
},
659+
},
660+
],
661+
});
662+
663+
const result = await model.doGenerate({
664+
prompt: TEST_PROMPT,
665+
});
666+
667+
expect(result.content).toStrictEqual([
668+
{
669+
type: 'text',
670+
text: 'Here is some information from a source.',
671+
},
672+
{
673+
type: 'source',
674+
sourceType: 'url',
675+
id: 'https://www.wallpaper.com/art/exhibitions-shows/berlin-art-week-2025',
676+
url: 'https://www.wallpaper.com/art/exhibitions-shows/berlin-art-week-2025',
677+
title: 'What to see at Berlin Art Week 2025 | Wallpaper*',
678+
providerMetadata: {
679+
openrouter: {
680+
content: '',
681+
},
682+
},
683+
},
684+
]);
685+
});
615686
});
616687

617688
describe('doStream', () => {
@@ -899,8 +970,8 @@ describe('doStream', () => {
899970
// 5. text-delta (2 times)
900971
// 6. text-end (when stream finishes)
901972

902-
const streamOrder = elements.map(el => el.type);
903-
973+
const streamOrder = elements.map((el) => el.type);
974+
904975
// Find the positions of key events
905976
const reasoningStartIndex = streamOrder.indexOf('reasoning-start');
906977
const reasoningEndIndex = streamOrder.indexOf('reasoning-end');
@@ -926,10 +997,7 @@ describe('doStream', () => {
926997
.filter((el) => el.type === 'text-delta')
927998
.map((el) => (el as { type: 'text-delta'; delta: string }).delta);
928999

929-
expect(textDeltas).toEqual([
930-
'Hello! ',
931-
'How can I help you today?',
932-
]);
1000+
expect(textDeltas).toEqual(['Hello! ', 'How can I help you today?']);
9331001
});
9341002

9351003
it('should stream tool deltas', async () => {

src/chat/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,9 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
351351
},
352352
},
353353
});
354+
} else if (annotation.type === 'file') {
355+
// File annotations are handled but not currently converted to content
356+
// This allows the schema to parse them without breaking
354357
}
355358
}
356359
}
@@ -648,6 +651,9 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
648651
},
649652
},
650653
});
654+
} else if (annotation.type === 'file') {
655+
// File annotations are handled but not currently converted to stream events
656+
// This allows the schema to parse them without breaking
651657
}
652658
}
653659
}
@@ -788,7 +794,7 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
788794
type: 'file',
789795
mediaType: getMediaType(image.image_url.url, 'image/jpeg'),
790796
data: getBase64FromDataUrl(image.image_url.url),
791-
})
797+
});
792798
}
793799
}
794800
},
@@ -832,12 +838,12 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
832838
} = {
833839
usage: openrouterUsage,
834840
};
835-
841+
836842
// Only include provider if it's actually set
837843
if (provider !== undefined) {
838844
openrouterMetadata.provider = provider;
839845
}
840-
846+
841847
controller.enqueue({
842848
type: 'finish',
843849
finishReason,

src/chat/schemas.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from 'zod/v4';
22
import { OpenRouterErrorResponseSchema } from '../schemas/error-response';
33
import { ReasoningDetailArraySchema } from '../schemas/reasoning-details';
44
import { ImageResponseArraySchema } from '../schemas/image';
5+
import { AnnotationArraySchema } from '../schemas/annotations';
56

67
const OpenRouterChatCompletionBaseResponseSchema = z.object({
78
id: z.string().optional(),
@@ -43,6 +44,7 @@ export const OpenRouterNonStreamChatCompletionResponseSchema =
4344
reasoning: z.string().nullable().optional(),
4445
reasoning_details: ReasoningDetailArraySchema.nullish(),
4546
images: ImageResponseArraySchema.nullish(),
47+
annotations: AnnotationArraySchema.nullish(),
4648

4749
tool_calls: z
4850
.array(
@@ -56,21 +58,6 @@ export const OpenRouterNonStreamChatCompletionResponseSchema =
5658
}),
5759
)
5860
.optional(),
59-
60-
annotations: z
61-
.array(
62-
z.object({
63-
type: z.enum(['url_citation']),
64-
url_citation: z.object({
65-
end_index: z.number(),
66-
start_index: z.number(),
67-
title: z.string(),
68-
url: z.string(),
69-
content: z.string().optional(),
70-
}),
71-
}),
72-
)
73-
.nullish(),
7461
}),
7562
index: z.number().nullish(),
7663
logprobs: z
@@ -109,6 +96,7 @@ export const OpenRouterStreamChatCompletionChunkSchema = z.union([
10996
reasoning: z.string().nullish().optional(),
11097
reasoning_details: ReasoningDetailArraySchema.nullish(),
11198
images: ImageResponseArraySchema.nullish(),
99+
annotations: AnnotationArraySchema.nullish(),
112100
tool_calls: z
113101
.array(
114102
z.object({
@@ -122,21 +110,6 @@ export const OpenRouterStreamChatCompletionChunkSchema = z.union([
122110
}),
123111
)
124112
.nullish(),
125-
126-
annotations: z
127-
.array(
128-
z.object({
129-
type: z.enum(['url_citation']),
130-
url_citation: z.object({
131-
end_index: z.number(),
132-
start_index: z.number(),
133-
title: z.string(),
134-
url: z.string(),
135-
content: z.string().optional(),
136-
}),
137-
}),
138-
)
139-
.nullish(),
140113
})
141114
.nullish(),
142115
logprobs: z

src/schemas/annotations.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { z } from 'zod/v4';
2+
3+
export const UrlCitationAnnotationSchema = z.object({
4+
type: z.literal('url_citation'),
5+
url_citation: z.object({
6+
end_index: z.number(),
7+
start_index: z.number(),
8+
title: z.string(),
9+
url: z.string(),
10+
content: z.string().optional(),
11+
}),
12+
});
13+
14+
export const FileAnnotationSchema = z.object({
15+
type: z.literal('file'),
16+
file: z.object({
17+
hash: z.string(),
18+
name: z.string(),
19+
content: z.array(z.record(z.string(), z.unknown())).optional(),
20+
}),
21+
});
22+
23+
export const AnnotationUnionSchema = z.discriminatedUnion('type', [
24+
UrlCitationAnnotationSchema,
25+
FileAnnotationSchema,
26+
]);
27+
28+
export type AnnotationUnion = z.infer<typeof AnnotationUnionSchema>;
29+
30+
export const AnnotationArraySchema = z.array(AnnotationUnionSchema);

0 commit comments

Comments
 (0)