Skip to content

Commit 2b49df4

Browse files
Fix/relax schema validation (#232)
* fix: relax zod schemas with passthrough to allow unexpected fields Add .passthrough() to all zod object schemas to prevent validation failures when the API returns extra fields that aren't in our schema definitions. This ensures the provider is forward-compatible with API changes and doesn't break when new fields are added to responses. Changes: - Add .passthrough() to all object schemas in chat/schemas.ts - Add .passthrough() to all object schemas in completion/schemas.ts - Add .passthrough() to provider-metadata, image, and reasoning-details schemas - Add type assertions for error handling to fix TypeScript errors from passthrough Evidence: All 74 unit tests pass, 10/11 e2e tests pass (1 unrelated failure) * test: add file parser annotation schema test Add test to verify FileParser annotations with content arrays parse correctly. Update error schema tests to expect passthrough fields in output. Evidence: Test passes with file annotations containing content arrays * chore: add changeset for schema relaxation * Add FileParserPlugin with Mistral OCR to PDF test * Format long lines in file-parser-schema.test.ts * Replace provider name with generic example in tests * Replace type cast with ts-expect-error
1 parent fd5e309 commit 2b49df4

File tree

12 files changed

+504
-278
lines changed

12 files changed

+504
-278
lines changed

.changeset/relax-schemas.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@openrouter/ai-sdk-provider": patch
3+
---
4+
5+
Relax zod schemas with passthrough to allow unexpected API fields
6+
7+
Add `.passthrough()` to all zod object schemas to prevent validation failures when the API returns extra fields not in our schema definitions. This ensures forward compatibility with API changes and prevents breaking when new fields are added to responses.

e2e/pdf-blob/index.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ test('sending large pdf base64 blob with FileParserPlugin', async () => {
7272
});
7373

7474
const model = openrouter('anthropic/claude-3.5-sonnet', {
75+
plugins: [
76+
{
77+
id: 'file-parser',
78+
pdf: {
79+
engine: 'mistral-ocr',
80+
},
81+
},
82+
],
7583
usage: {
7684
include: true,
7785
},
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { OpenRouterNonStreamChatCompletionResponseSchema } from './schemas';
3+
4+
describe('FileParser annotation schema', () => {
5+
it('should parse response with all real API fields', () => {
6+
// This is based on actual API response structure (anonymized)
7+
const response = {
8+
id: 'gen-xxx',
9+
provider: 'Amazon Bedrock',
10+
model: 'anthropic/claude-3.5-sonnet',
11+
object: 'chat.completion',
12+
created: 1763157299,
13+
choices: [
14+
{
15+
logprobs: null,
16+
finish_reason: 'stop',
17+
native_finish_reason: 'stop',
18+
index: 0,
19+
message: {
20+
role: 'assistant' as const,
21+
content: 'Test response content',
22+
refusal: null,
23+
reasoning: null,
24+
annotations: [
25+
{
26+
type: 'file' as const,
27+
file: {
28+
hash: 'abc123',
29+
name: '',
30+
content: [
31+
{
32+
type: 'text',
33+
text: '<file name="">',
34+
},
35+
],
36+
},
37+
},
38+
],
39+
},
40+
},
41+
],
42+
usage: {
43+
prompt_tokens: 100,
44+
completion_tokens: 50,
45+
total_tokens: 150,
46+
},
47+
};
48+
49+
const result =
50+
OpenRouterNonStreamChatCompletionResponseSchema.parse(response);
51+
expect(result).toBeDefined();
52+
});
53+
54+
it('should parse file annotation with content array and extra fields', () => {
55+
const response = {
56+
id: 'gen-test',
57+
provider: 'Amazon Bedrock',
58+
model: 'anthropic/claude-3.5-sonnet',
59+
object: 'chat.completion',
60+
created: 1763157061,
61+
choices: [
62+
{
63+
logprobs: null,
64+
finish_reason: 'stop',
65+
native_finish_reason: 'stop', // Extra field from API
66+
index: 0,
67+
message: {
68+
role: 'assistant' as const,
69+
content: 'Test response',
70+
refusal: null, // Extra field from API
71+
reasoning: null,
72+
annotations: [
73+
{
74+
type: 'file' as const,
75+
file: {
76+
hash: '85bd49b97b7ff5be002d9f654776119f253c1cae333b49ba8f4a53da346284ba',
77+
name: '',
78+
content: [
79+
{
80+
type: 'text',
81+
text: '<file name="">',
82+
},
83+
{
84+
type: 'text',
85+
text: 'Some file content',
86+
},
87+
],
88+
},
89+
},
90+
],
91+
},
92+
},
93+
],
94+
usage: {
95+
prompt_tokens: 100,
96+
completion_tokens: 50,
97+
total_tokens: 150,
98+
},
99+
};
100+
101+
const result =
102+
OpenRouterNonStreamChatCompletionResponseSchema.parse(response);
103+
104+
// Check that parsing succeeded
105+
expect(result).toBeDefined();
106+
// The schema uses passthrough so we can't strictly type check, but we can verify structure
107+
// @ts-expect-error test intentionally inspects passthrough data
108+
const firstChoice = result.choices?.[0];
109+
expect(firstChoice?.message.annotations).toBeDefined();
110+
expect(firstChoice?.message.annotations?.[0]?.type).toBe('file');
111+
});
112+
});

src/chat/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,20 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {
234234

235235
// Check if response is an error (HTTP 200 with error payload)
236236
if ('error' in responseValue) {
237+
const errorData = responseValue.error as {
238+
message: string;
239+
code?: string;
240+
};
237241
throw new APICallError({
238-
message: responseValue.error.message,
242+
message: errorData.message,
239243
url: this.config.url({
240244
path: '/chat/completions',
241245
modelId: this.modelId,
242246
}),
243247
requestBodyValues: args,
244248
statusCode: 200,
245249
responseHeaders,
246-
data: responseValue.error,
250+
data: errorData,
247251
});
248252
}
249253

0 commit comments

Comments
 (0)