diff --git a/.changeset/telegram-attachment-media-type.md b/.changeset/telegram-attachment-media-type.md new file mode 100644 index 000000000..268fb773d --- /dev/null +++ b/.changeset/telegram-attachment-media-type.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Telegram channel: prefer the known attachment media type over the HTTP `content-type` header when fetching files. Telegram's file endpoint frequently returns `application/octet-stream` (or no content-type) for photos, which broke image recognition in vision models that key off the declared media type. diff --git a/packages/eve/src/public/channels/telegram/attachments.test.ts b/packages/eve/src/public/channels/telegram/attachments.test.ts index fa96b3f8f..1848ad765 100644 --- a/packages/eve/src/public/channels/telegram/attachments.test.ts +++ b/packages/eve/src/public/channels/telegram/attachments.test.ts @@ -86,4 +86,39 @@ describe("createTelegramFetchFile", () => { await expect(fetchFile("https://example.com/file.png")).resolves.toBeNull(); }); + + it("prefers the known attachment media type over the HTTP content-type header", async () => { + // Telegram's file endpoint frequently returns `application/octet-stream` + // (or no content-type) for photos. The channel already knows the photo is + // `image/jpeg` from the inbound message; that known type must win so vision + // models receive a recognized image media type. + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify({ ok: true, result: { file_path: "photos/file.jpg" } }), { + headers: { "content-type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response("JFIF", { headers: { "content-type": "application/octet-stream" } }), + ); + + const fetchFile = createTelegramFetchFile({ + api: { apiBaseUrl: "https://telegram.example", fetch: fetchMock }, + credentials: { botToken: "123456:ABCDEF" }, + policy: { allowedMediaTypes: ["image/*"], maxBytes: 1024 }, + }); + + const result = await fetchFile( + String( + createTelegramFileUrl({ + fileId: "photo-id", + filename: "photo.jpg", + mediaType: "image/jpeg", + }), + ), + ); + + expect(result?.mediaType).toBe("image/jpeg"); + }); }); diff --git a/packages/eve/src/public/channels/telegram/attachments.ts b/packages/eve/src/public/channels/telegram/attachments.ts index a18e0e022..7c993ec73 100644 --- a/packages/eve/src/public/channels/telegram/attachments.ts +++ b/packages/eve/src/public/channels/telegram/attachments.ts @@ -90,8 +90,13 @@ export function createTelegramFetchFile(input: { } const bytes = Buffer.from(await response.arrayBuffer()); + // Prefer the media type the channel already knows (from the attachment + // metadata, e.g. `image/jpeg` for photos) over the HTTP `content-type` + // header returned by Telegram's file endpoint, which is frequently + // `application/octet-stream` or unset. Using the latter breaks image + // recognition in vision models that key off the declared media type. const mediaType = - response.headers.get("content-type") ?? ref.mediaType ?? "application/octet-stream"; + ref.mediaType ?? response.headers.get("content-type") ?? "application/octet-stream"; const result: FetchFileResult = { bytes, filename: ref.filename,