From 607556f0bd98a57f582485dbf7ae41bbcba4efea Mon Sep 17 00:00:00 2001 From: Blake Ledden Date: Sat, 20 Dec 2025 21:08:29 -0800 Subject: [PATCH 1/2] Add file content type support for PDF and document uploads The SDK was rejecting file content items with a Zod validation error before requests ever reached the OpenRouter API. This happened because the ChatMessageContentItem union type didn't include a "file" variant, even though the API accepts it. This change adds: - ChatMessageContentItemFile schema to the OpenAPI spec - New TypeScript types and Zod schemas for file content - Export from the models index - E2E test confirming PDF uploads work correctly The file content type supports: - filename (required) - file_data (URL or base64-encoded content) - file_id (for previously uploaded files) Fixes #83 --- .speakeasy/in.openapi.yaml | 25 +++++ src/models/chatmessagecontentitem.ts | 10 ++ src/models/chatmessagecontentitemfile.ts | 112 +++++++++++++++++++++++ src/models/index.ts | 1 + tests/e2e/chat.test.ts | 38 ++++++++ 5 files changed, 186 insertions(+) create mode 100644 src/models/chatmessagecontentitemfile.ts diff --git a/.speakeasy/in.openapi.yaml b/.speakeasy/in.openapi.yaml index 4047ff25..457c8c03 100644 --- a/.speakeasy/in.openapi.yaml +++ b/.speakeasy/in.openapi.yaml @@ -5046,12 +5046,36 @@ components: - type - video_url type: object + ChatMessageContentItemFile: + type: object + properties: + type: + type: string + const: file + file: + type: object + properties: + filename: + type: string + file_data: + type: string + description: URL or base64-encoded file data + file_id: + type: string + description: ID of a previously uploaded file + required: + - filename + required: + - type + - file + description: File content item for PDFs and other documents ChatMessageContentItem: oneOf: - $ref: '#/components/schemas/ChatMessageContentItemText' - $ref: '#/components/schemas/ChatMessageContentItemImage' - $ref: '#/components/schemas/ChatMessageContentItemAudio' - $ref: '#/components/schemas/ChatMessageContentItemVideo' + - $ref: '#/components/schemas/ChatMessageContentItemFile' type: object discriminator: propertyName: type @@ -5061,6 +5085,7 @@ components: input_audio: '#/components/schemas/ChatMessageContentItemAudio' input_video: '#/components/schemas/ChatMessageContentItemVideo' video_url: '#/components/schemas/ChatMessageContentItemVideo' + file: '#/components/schemas/ChatMessageContentItemFile' ChatMessageToolCall: type: object properties: diff --git a/src/models/chatmessagecontentitem.ts b/src/models/chatmessagecontentitem.ts index f1548c67..12e3a6f8 100644 --- a/src/models/chatmessagecontentitem.ts +++ b/src/models/chatmessagecontentitem.ts @@ -12,6 +12,12 @@ import { ChatMessageContentItemAudio$Outbound, ChatMessageContentItemAudio$outboundSchema, } from "./chatmessagecontentitemaudio.js"; +import { + ChatMessageContentItemFile, + ChatMessageContentItemFile$inboundSchema, + ChatMessageContentItemFile$Outbound, + ChatMessageContentItemFile$outboundSchema, +} from "./chatmessagecontentitemfile.js"; import { ChatMessageContentItemImage, ChatMessageContentItemImage$inboundSchema, @@ -36,6 +42,7 @@ export type ChatMessageContentItem = | ChatMessageContentItemText | ChatMessageContentItemImage | ChatMessageContentItemAudio + | ChatMessageContentItemFile | (ChatMessageContentItemVideo & { type: "input_video" }) | (ChatMessageContentItemVideo & { type: "video_url" }); @@ -47,6 +54,7 @@ export const ChatMessageContentItem$inboundSchema: z.ZodType< ChatMessageContentItemText$inboundSchema, ChatMessageContentItemImage$inboundSchema, ChatMessageContentItemAudio$inboundSchema, + ChatMessageContentItemFile$inboundSchema, ChatMessageContentItemVideo$inboundSchema.and( z.object({ type: z.literal("input_video") }), ), @@ -59,6 +67,7 @@ export type ChatMessageContentItem$Outbound = | ChatMessageContentItemText$Outbound | ChatMessageContentItemImage$Outbound | ChatMessageContentItemAudio$Outbound + | ChatMessageContentItemFile$Outbound | (ChatMessageContentItemVideo$Outbound & { type: "input_video" }) | (ChatMessageContentItemVideo$Outbound & { type: "video_url" }); @@ -70,6 +79,7 @@ export const ChatMessageContentItem$outboundSchema: z.ZodType< ChatMessageContentItemText$outboundSchema, ChatMessageContentItemImage$outboundSchema, ChatMessageContentItemAudio$outboundSchema, + ChatMessageContentItemFile$outboundSchema, ChatMessageContentItemVideo$outboundSchema.and( z.object({ type: z.literal("input_video") }), ), diff --git a/src/models/chatmessagecontentitemfile.ts b/src/models/chatmessagecontentitemfile.ts new file mode 100644 index 00000000..85a798dc --- /dev/null +++ b/src/models/chatmessagecontentitemfile.ts @@ -0,0 +1,112 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4"; +import { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import { SDKValidationError } from "./errors/sdkvalidationerror.js"; + +export type FileData = { + filename: string; + /** + * URL or base64-encoded file data + */ + fileData?: string | undefined; + /** + * ID of a previously uploaded file + */ + fileId?: string | undefined; +}; + +export type ChatMessageContentItemFile = { + type: "file"; + file: FileData; +}; + +/** @internal */ +export const FileData$inboundSchema: z.ZodType = z.object({ + filename: z.string(), + file_data: z.string().optional(), + file_id: z.string().optional(), +}).transform((v) => ({ + filename: v.filename, + fileData: v.file_data, + fileId: v.file_id, +})); + +/** @internal */ +export type FileData$Outbound = { + filename: string; + file_data?: string | undefined; + file_id?: string | undefined; +}; + +/** @internal */ +export const FileData$outboundSchema: z.ZodType = z + .object({ + filename: z.string(), + fileData: z.string().optional(), + fileId: z.string().optional(), + }) + .transform((v) => ({ + filename: v.filename, + file_data: v.fileData, + file_id: v.fileId, + })); + +export function fileDataToJSON(fileData: FileData): string { + return JSON.stringify(FileData$outboundSchema.parse(fileData)); +} + +export function fileDataFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => FileData$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'FileData' from JSON`, + ); +} + +/** @internal */ +export const ChatMessageContentItemFile$inboundSchema: z.ZodType< + ChatMessageContentItemFile, + unknown +> = z.object({ + type: z.literal("file"), + file: z.lazy(() => FileData$inboundSchema), +}); + +/** @internal */ +export type ChatMessageContentItemFile$Outbound = { + type: "file"; + file: FileData$Outbound; +}; + +/** @internal */ +export const ChatMessageContentItemFile$outboundSchema: z.ZodType< + ChatMessageContentItemFile$Outbound, + ChatMessageContentItemFile +> = z.object({ + type: z.literal("file"), + file: z.lazy(() => FileData$outboundSchema), +}); + +export function chatMessageContentItemFileToJSON( + chatMessageContentItemFile: ChatMessageContentItemFile, +): string { + return JSON.stringify( + ChatMessageContentItemFile$outboundSchema.parse(chatMessageContentItemFile), + ); +} + +export function chatMessageContentItemFileFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => ChatMessageContentItemFile$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'ChatMessageContentItemFile' from JSON`, + ); +} diff --git a/src/models/index.ts b/src/models/index.ts index f8e040d1..625c6e4b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -14,6 +14,7 @@ export * from "./chatgenerationtokenusage.js"; export * from "./chatmessagecontentitem.js"; export * from "./chatmessagecontentitemaudio.js"; export * from "./chatmessagecontentitemcachecontrol.js"; +export * from "./chatmessagecontentitemfile.js"; export * from "./chatmessagecontentitemimage.js"; export * from "./chatmessagecontentitemtext.js"; export * from "./chatmessagecontentitemvideo.js"; diff --git a/tests/e2e/chat.test.ts b/tests/e2e/chat.test.ts index b5ad310a..3fb134b0 100644 --- a/tests/e2e/chat.test.ts +++ b/tests/e2e/chat.test.ts @@ -1,4 +1,5 @@ import type { ChatStreamingResponseChunkData } from '../../src/models/chatstreamingresponsechunk.js'; +import type { ChatMessageContentItemFile } from '../../src/models/chatmessagecontentitemfile.js'; import { beforeAll, describe, expect, it } from 'vitest'; import { OpenRouter } from '../../src/sdk/sdk.js'; @@ -184,4 +185,41 @@ describe('Chat E2E Tests', () => { expect(foundFinishReason).toBe(true); }, 10000); }); + + describe('chat.send() - File Content Type', () => { + it('should successfully send a request with file content (PDF via URL)', async () => { + const fileContent: ChatMessageContentItemFile = { + type: 'file', + file: { + filename: 'bitcoin.pdf', + fileData: 'https://bitcoin.org/bitcoin.pdf', + }, + }; + + const response = await client.chat.send({ + model: 'anthropic/claude-sonnet-4', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'What is the title of this document? Reply in 10 words or less.' }, + fileContent, + ], + }, + ], + maxTokens: 50, + stream: false, + }); + + expect(response).toBeDefined(); + expect(Array.isArray(response.choices)).toBe(true); + expect(response.choices.length).toBeGreaterThan(0); + + const content = response.choices[0]?.message?.content; + expect(content).toBeDefined(); + expect(typeof content).toBe('string'); + // The response should mention Bitcoin or the paper title + expect((content as string).toLowerCase()).toMatch(/bitcoin|peer|electronic|cash/); + }, 30000); + }); }); From 1bb11408012fe6f89835ac97698ef3ed7d58a127 Mon Sep 17 00:00:00 2001 From: Blake Ledden Date: Sun, 21 Dec 2025 00:24:43 -0800 Subject: [PATCH 2/2] chore: remove generated file changes per CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TypeScript SDK repo does not accept direct changes to generated code. Only the OpenAPI spec change and E2E test should be included. The maintainers will regenerate the SDK after merging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/models/chatmessagecontentitem.ts | 10 -- src/models/chatmessagecontentitemfile.ts | 112 ----------------------- src/models/index.ts | 1 - 3 files changed, 123 deletions(-) delete mode 100644 src/models/chatmessagecontentitemfile.ts diff --git a/src/models/chatmessagecontentitem.ts b/src/models/chatmessagecontentitem.ts index 12e3a6f8..f1548c67 100644 --- a/src/models/chatmessagecontentitem.ts +++ b/src/models/chatmessagecontentitem.ts @@ -12,12 +12,6 @@ import { ChatMessageContentItemAudio$Outbound, ChatMessageContentItemAudio$outboundSchema, } from "./chatmessagecontentitemaudio.js"; -import { - ChatMessageContentItemFile, - ChatMessageContentItemFile$inboundSchema, - ChatMessageContentItemFile$Outbound, - ChatMessageContentItemFile$outboundSchema, -} from "./chatmessagecontentitemfile.js"; import { ChatMessageContentItemImage, ChatMessageContentItemImage$inboundSchema, @@ -42,7 +36,6 @@ export type ChatMessageContentItem = | ChatMessageContentItemText | ChatMessageContentItemImage | ChatMessageContentItemAudio - | ChatMessageContentItemFile | (ChatMessageContentItemVideo & { type: "input_video" }) | (ChatMessageContentItemVideo & { type: "video_url" }); @@ -54,7 +47,6 @@ export const ChatMessageContentItem$inboundSchema: z.ZodType< ChatMessageContentItemText$inboundSchema, ChatMessageContentItemImage$inboundSchema, ChatMessageContentItemAudio$inboundSchema, - ChatMessageContentItemFile$inboundSchema, ChatMessageContentItemVideo$inboundSchema.and( z.object({ type: z.literal("input_video") }), ), @@ -67,7 +59,6 @@ export type ChatMessageContentItem$Outbound = | ChatMessageContentItemText$Outbound | ChatMessageContentItemImage$Outbound | ChatMessageContentItemAudio$Outbound - | ChatMessageContentItemFile$Outbound | (ChatMessageContentItemVideo$Outbound & { type: "input_video" }) | (ChatMessageContentItemVideo$Outbound & { type: "video_url" }); @@ -79,7 +70,6 @@ export const ChatMessageContentItem$outboundSchema: z.ZodType< ChatMessageContentItemText$outboundSchema, ChatMessageContentItemImage$outboundSchema, ChatMessageContentItemAudio$outboundSchema, - ChatMessageContentItemFile$outboundSchema, ChatMessageContentItemVideo$outboundSchema.and( z.object({ type: z.literal("input_video") }), ), diff --git a/src/models/chatmessagecontentitemfile.ts b/src/models/chatmessagecontentitemfile.ts deleted file mode 100644 index 85a798dc..00000000 --- a/src/models/chatmessagecontentitemfile.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4"; -import { safeParse } from "../lib/schemas.js"; -import { Result as SafeParseResult } from "../types/fp.js"; -import { SDKValidationError } from "./errors/sdkvalidationerror.js"; - -export type FileData = { - filename: string; - /** - * URL or base64-encoded file data - */ - fileData?: string | undefined; - /** - * ID of a previously uploaded file - */ - fileId?: string | undefined; -}; - -export type ChatMessageContentItemFile = { - type: "file"; - file: FileData; -}; - -/** @internal */ -export const FileData$inboundSchema: z.ZodType = z.object({ - filename: z.string(), - file_data: z.string().optional(), - file_id: z.string().optional(), -}).transform((v) => ({ - filename: v.filename, - fileData: v.file_data, - fileId: v.file_id, -})); - -/** @internal */ -export type FileData$Outbound = { - filename: string; - file_data?: string | undefined; - file_id?: string | undefined; -}; - -/** @internal */ -export const FileData$outboundSchema: z.ZodType = z - .object({ - filename: z.string(), - fileData: z.string().optional(), - fileId: z.string().optional(), - }) - .transform((v) => ({ - filename: v.filename, - file_data: v.fileData, - file_id: v.fileId, - })); - -export function fileDataToJSON(fileData: FileData): string { - return JSON.stringify(FileData$outboundSchema.parse(fileData)); -} - -export function fileDataFromJSON( - jsonString: string, -): SafeParseResult { - return safeParse( - jsonString, - (x) => FileData$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'FileData' from JSON`, - ); -} - -/** @internal */ -export const ChatMessageContentItemFile$inboundSchema: z.ZodType< - ChatMessageContentItemFile, - unknown -> = z.object({ - type: z.literal("file"), - file: z.lazy(() => FileData$inboundSchema), -}); - -/** @internal */ -export type ChatMessageContentItemFile$Outbound = { - type: "file"; - file: FileData$Outbound; -}; - -/** @internal */ -export const ChatMessageContentItemFile$outboundSchema: z.ZodType< - ChatMessageContentItemFile$Outbound, - ChatMessageContentItemFile -> = z.object({ - type: z.literal("file"), - file: z.lazy(() => FileData$outboundSchema), -}); - -export function chatMessageContentItemFileToJSON( - chatMessageContentItemFile: ChatMessageContentItemFile, -): string { - return JSON.stringify( - ChatMessageContentItemFile$outboundSchema.parse(chatMessageContentItemFile), - ); -} - -export function chatMessageContentItemFileFromJSON( - jsonString: string, -): SafeParseResult { - return safeParse( - jsonString, - (x) => ChatMessageContentItemFile$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'ChatMessageContentItemFile' from JSON`, - ); -} diff --git a/src/models/index.ts b/src/models/index.ts index 625c6e4b..f8e040d1 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -14,7 +14,6 @@ export * from "./chatgenerationtokenusage.js"; export * from "./chatmessagecontentitem.js"; export * from "./chatmessagecontentitemaudio.js"; export * from "./chatmessagecontentitemcachecontrol.js"; -export * from "./chatmessagecontentitemfile.js"; export * from "./chatmessagecontentitemimage.js"; export * from "./chatmessagecontentitemtext.js"; export * from "./chatmessagecontentitemvideo.js";