Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/relax-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openrouter/ai-sdk-provider": patch
---

Relax zod schemas with passthrough to allow unexpected API fields

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.
8 changes: 8 additions & 0 deletions e2e/pdf-blob/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ test('sending large pdf base64 blob with FileParserPlugin', async () => {
});

const model = openrouter('anthropic/claude-3.5-sonnet', {
plugins: [
{
id: 'file-parser',
pdf: {
engine: 'mistral-ocr',
},
},
],
usage: {
include: true,
},
Expand Down
112 changes: 112 additions & 0 deletions src/chat/file-parser-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, expect, it } from 'vitest';
import { OpenRouterNonStreamChatCompletionResponseSchema } from './schemas';

describe('FileParser annotation schema', () => {
it('should parse response with all real API fields', () => {
// This is based on actual API response structure (anonymized)
const response = {
id: 'gen-xxx',
provider: 'Amazon Bedrock',
model: 'anthropic/claude-3.5-sonnet',
object: 'chat.completion',
created: 1763157299,
choices: [
{
logprobs: null,
finish_reason: 'stop',
native_finish_reason: 'stop',
index: 0,
message: {
role: 'assistant' as const,
content: 'Test response content',
refusal: null,
reasoning: null,
annotations: [
{
type: 'file' as const,
file: {
hash: 'abc123',
name: '',
content: [
{
type: 'text',
text: '<file name="">',
},
],
},
},
],
},
},
],
usage: {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
},
};

const result =
OpenRouterNonStreamChatCompletionResponseSchema.parse(response);
expect(result).toBeDefined();
});

it('should parse file annotation with content array and extra fields', () => {
const response = {
id: 'gen-test',
provider: 'Amazon Bedrock',
model: 'anthropic/claude-3.5-sonnet',
object: 'chat.completion',
created: 1763157061,
choices: [
{
logprobs: null,
finish_reason: 'stop',
native_finish_reason: 'stop', // Extra field from API
index: 0,
message: {
role: 'assistant' as const,
content: 'Test response',
refusal: null, // Extra field from API
reasoning: null,
annotations: [
{
type: 'file' as const,
file: {
hash: '85bd49b97b7ff5be002d9f654776119f253c1cae333b49ba8f4a53da346284ba',
name: '',
content: [
{
type: 'text',
text: '<file name="">',
},
{
type: 'text',
text: 'Some file content',
},
],
},
},
],
},
},
],
usage: {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
},
};

const result =
OpenRouterNonStreamChatCompletionResponseSchema.parse(response);

// Check that parsing succeeded
expect(result).toBeDefined();
// The schema uses passthrough so we can't strictly type check, but we can verify structure
// @ts-expect-error test intentionally inspects passthrough data
const firstChoice = result.choices?.[0];
expect(firstChoice?.message.annotations).toBeDefined();
expect(firstChoice?.message.annotations?.[0]?.type).toBe('file');
});
});
8 changes: 6 additions & 2 deletions src/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,20 @@ export class OpenRouterChatLanguageModel implements LanguageModelV2 {

// Check if response is an error (HTTP 200 with error payload)
if ('error' in responseValue) {
const errorData = responseValue.error as {
message: string;
code?: string;
};
throw new APICallError({
message: responseValue.error.message,
message: errorData.message,
url: this.config.url({
path: '/chat/completions',
modelId: this.modelId,
}),
requestBodyValues: args,
statusCode: 200,
responseHeaders,
data: responseValue.error,
data: errorData,
});
}

Expand Down
Loading