From 7398b258a4b8282659b7992dd6ddbb5057dc977b Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Sun, 8 Mar 2026 19:02:04 -0700 Subject: [PATCH 1/4] feat: add blob attachment type for inline base64 data Add support for a new 'blob' attachment type that allows sending base64-encoded content (e.g. images) directly without disk I/O. Generated types will be updated automatically when the runtime publishes the new schema to @github/copilot. This commit includes: - Add blob variant to Node.js and Python hand-written types - Export attachment types from Python SDK public API - Update docs: image-input.md, all language READMEs, streaming-events.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/image-input.md | 201 ++++++++++++++++++- docs/features/streaming-events.md | 2 +- dotnet/README.md | 20 +- go/README.md | 16 +- nodejs/README.md | 15 +- nodejs/src/types.ts | 8 +- python/README.md | 15 +- python/copilot/__init__.py | 10 + python/copilot/types.py | 13 +- test/scenarios/prompts/attachments/README.md | 21 +- 10 files changed, 302 insertions(+), 19 deletions(-) diff --git a/docs/features/image-input.md b/docs/features/image-input.md index aa3bf2f64..79c80d2b7 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -1,6 +1,9 @@ # Image Input -Send images to Copilot sessions by attaching them as file attachments. The runtime reads the file from disk, converts it to base64 internally, and sends it to the LLM as an image content block — no manual encoding required. +Send images to Copilot sessions as attachments. There are two ways to attach images: + +- **File attachment** (`type: "file"`) — provide an absolute path; the runtime reads the file from disk, converts it to base64, and sends it to the LLM. +- **Blob attachment** (`type: "blob"`) — provide base64-encoded data directly; useful when the image is already in memory (e.g., screenshots, generated images, or data from an API). ## Overview @@ -25,11 +28,12 @@ sequenceDiagram | Concept | Description | |---------|-------------| | **File attachment** | An attachment with `type: "file"` and an absolute `path` to an image on disk | -| **Automatic encoding** | The runtime reads the image, converts it to base64, and sends it as an `image_url` block | +| **Blob attachment** | An attachment with `type: "blob"`, base64-encoded `data`, and a `mimeType` — no disk I/O needed | +| **Automatic encoding** | For file attachments, the runtime reads the image and converts it to base64 automatically | | **Auto-resize** | The runtime automatically resizes or quality-reduces images that exceed model-specific limits | | **Vision capability** | The model must have `capabilities.supports.vision = true` to process images | -## Quick Start +## Quick Start — File Attachment Attach an image file to any message using the file attachment type. The path must be an absolute path to an image on disk. @@ -215,9 +219,190 @@ await session.SendAsync(new MessageOptions +## Quick Start — Blob Attachment + +When you already have image data in memory (e.g., a screenshot captured by your app, or an image fetched from an API), use a blob attachment to send it directly without writing to disk. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +const base64ImageData = "..."; // your base64-encoded image +await session.send({ + prompt: "Describe what you see in this image", + attachments: [ + { + type: "blob", + data: base64ImageData, + mimeType: "image/png", + displayName: "screenshot.png", + }, + ], +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import PermissionRequestResult + +client = CopilotClient() +await client.start() + +session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), +}) + +base64_image_data = "..." # your base64-encoded image +await session.send({ + "prompt": "Describe what you see in this image", + "attachments": [ + { + "type": "blob", + "data": base64_image_data, + "mimeType": "image/png", + "displayName": "screenshot.png", + }, + ], +}) +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + base64ImageData := "..." + mimeType := "image/png" + displayName := "screenshot.png" + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, + }) +} +``` + + +```go +mimeType := "image/png" +displayName := "screenshot.png" +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, // base64-encoded string + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, +}) +``` + +
+ +
+.NET + + +```csharp +using GitHub.Copilot.SDK; + +public static class BlobAttachmentExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + + var base64ImageData = "..."; + await session.SendAsync(new MessageOptions + { + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", + DisplayName = "screenshot.png", + }, + }, + }); + } +} +``` + + +```csharp +await session.SendAsync(new MessageOptions +{ + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", + DisplayName = "screenshot.png", + }, + }, +}); +``` + +
+ ## Supported Formats -Supported image formats include JPG, PNG, GIF, and other common image types. The runtime reads the image from disk and converts it as needed before sending to the LLM. Use PNG or JPEG for best results, as these are the most widely supported formats. +Supported image formats include JPG, PNG, GIF, and other common image types. For file attachments, the runtime reads the image from disk and converts it as needed. For blob attachments, you provide the base64 data and MIME type directly. Use PNG or JPEG for best results, as these are the most widely supported formats. The model's `capabilities.limits.vision.supported_media_types` field lists the exact MIME types it accepts. @@ -283,10 +468,10 @@ These image blocks appear in `tool.execution_complete` event results. See the [S |-----|---------| | **Use PNG or JPEG directly** | Avoids conversion overhead — these are sent to the LLM as-is | | **Keep images reasonably sized** | Large images may be quality-reduced, which can lose important details | -| **Use absolute paths** | The runtime reads files from disk; relative paths may not resolve correctly | -| **Check vision support first** | Sending images to a non-vision model wastes tokens on the file path without visual understanding | -| **Multiple images are supported** | Attach several file attachments in one message, up to the model's `max_prompt_images` limit | -| **Images are not base64 in your code** | You provide a file path — the runtime handles encoding, resizing, and format conversion | +| **Use absolute paths for file attachments** | The runtime reads files from disk; relative paths may not resolve correctly | +| **Use blob attachments for in-memory data** | When you already have base64 data (e.g., screenshots, API responses), blob avoids unnecessary disk I/O | +| **Check vision support first** | Sending images to a non-vision model wastes tokens without visual understanding | +| **Multiple images are supported** | Attach several attachments in one message, up to the model's `max_prompt_images` limit | | **SVG is not supported** | SVG files are text-based and excluded from image processing | ## See Also diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 81b27f80f..d03ed95fa 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -639,7 +639,7 @@ The user sent a message. Recorded for the session timeline. |------------|------|----------|-------------| | `content` | `string` | ✅ | The user's message text | | `transformedContent` | `string` | | Transformed version after preprocessing | -| `attachments` | `Attachment[]` | | File, directory, selection, or GitHub reference attachments | +| `attachments` | `Attachment[]` | | File, directory, selection, blob, or GitHub reference attachments | | `source` | `string` | | Message source identifier | | `agentMode` | `string` | | Agent mode: `"interactive"`, `"plan"`, `"autopilot"`, or `"shell"` | | `interactionId` | `string` | | CAPI interaction ID | diff --git a/dotnet/README.md b/dotnet/README.md index bdb3e8dab..c5b3857be 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -265,21 +265,35 @@ session.On(evt => ## Image Support -The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```csharp +// File attachment — runtime reads from disk await session.SendAsync(new MessageOptions { Prompt = "What's in this image?", Attachments = new List { - new UserMessageDataAttachmentsItem + new UserMessageDataAttachmentsItemFile { - Type = UserMessageDataAttachmentsItemType.File, Path = "/path/to/image.jpg" } } }); + +// Blob attachment — provide base64 data directly +await session.SendAsync(new MessageOptions +{ + Prompt = "What's in this image?", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", + } + } +}); ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/go/README.md b/go/README.md index 4cc73398c..6bccdbb10 100644 --- a/go/README.md +++ b/go/README.md @@ -178,9 +178,10 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec ## Image Support -The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path: +The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```go +// File attachment — runtime reads from disk _, err = session.Send(context.Background(), copilot.MessageOptions{ Prompt: "What's in this image?", Attachments: []copilot.Attachment{ @@ -190,6 +191,19 @@ _, err = session.Send(context.Background(), copilot.MessageOptions{ }, }, }) + +// Blob attachment — provide base64 data directly +mimeType := "image/png" +_, err = session.Send(context.Background(), copilot.MessageOptions{ + Prompt: "What's in this image?", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, + MIMEType: &mimeType, + }, + }, +}) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/nodejs/README.md b/nodejs/README.md index 78a535b76..8b1c585dc 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -297,9 +297,10 @@ See `SessionEvent` type in the source for full details. ## Image Support -The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```typescript +// File attachment — runtime reads from disk await session.send({ prompt: "What's in this image?", attachments: [ @@ -309,6 +310,18 @@ await session.send({ }, ], }); + +// Blob attachment — provide base64 data directly +await session.send({ + prompt: "What's in this image?", + attachments: [ + { + type: "blob", + data: base64ImageData, + mimeType: "image/png", + }, + ], +}); ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 99b9af75c..e01b810c3 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -856,7 +856,7 @@ export interface MessageOptions { prompt: string; /** - * File, directory, or selection attachments + * File, directory, selection, or blob attachments */ attachments?: Array< | { @@ -879,6 +879,12 @@ export interface MessageOptions { }; text?: string; } + | { + type: "blob"; + data: string; + mimeType: string; + displayName?: string; + } >; /** diff --git a/python/README.md b/python/README.md index 5b87bb04e..65b606ef6 100644 --- a/python/README.md +++ b/python/README.md @@ -234,9 +234,10 @@ async def edit_file(params: EditFileParams) -> str: ## Image Support -The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```python +# File attachment — runtime reads from disk await session.send({ "prompt": "What's in this image?", "attachments": [ @@ -246,6 +247,18 @@ await session.send({ } ] }) + +# Blob attachment — provide base64 data directly +await session.send({ + "prompt": "What's in this image?", + "attachments": [ + { + "type": "blob", + "data": base64_image_data, + "mimeType": "image/png", + } + ] +}) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index f5f7ed0b1..937a4ef5f 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -8,9 +8,13 @@ from .session import CopilotSession from .tools import define_tool from .types import ( + Attachment, AzureProviderOptions, + BlobAttachment, ConnectionState, CustomAgentConfig, + DirectoryAttachment, + FileAttachment, GetAuthStatusResponse, GetStatusResponse, MCPLocalServerConfig, @@ -27,6 +31,7 @@ PingResponse, ProviderConfig, ResumeSessionConfig, + SelectionAttachment, SessionConfig, SessionContext, SessionEvent, @@ -42,11 +47,15 @@ __version__ = "0.1.0" __all__ = [ + "Attachment", "AzureProviderOptions", + "BlobAttachment", "CopilotClient", "CopilotSession", "ConnectionState", "CustomAgentConfig", + "DirectoryAttachment", + "FileAttachment", "GetAuthStatusResponse", "GetStatusResponse", "MCPLocalServerConfig", @@ -63,6 +72,7 @@ "PingResponse", "ProviderConfig", "ResumeSessionConfig", + "SelectionAttachment", "SessionConfig", "SessionContext", "SessionEvent", diff --git a/python/copilot/types.py b/python/copilot/types.py index 33764e5d1..3ec5b6722 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -65,8 +65,19 @@ class SelectionAttachment(TypedDict): text: NotRequired[str] +class BlobAttachment(TypedDict): + """Inline base64-encoded content attachment (e.g. images).""" + + type: Literal["blob"] + data: str + """Base64-encoded content""" + mimeType: str + """MIME type of the inline data""" + displayName: NotRequired[str] + + # Attachment type - union of all attachment types -Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment +Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment # Options for creating a CopilotClient diff --git a/test/scenarios/prompts/attachments/README.md b/test/scenarios/prompts/attachments/README.md index 8c8239b23..d61a26e57 100644 --- a/test/scenarios/prompts/attachments/README.md +++ b/test/scenarios/prompts/attachments/README.md @@ -11,19 +11,36 @@ Demonstrates sending **file attachments** alongside a prompt using the Copilot S ## Attachment Format +### File Attachment + | Field | Value | Description | |-------|-------|-------------| | `type` | `"file"` | Indicates a local file attachment | | `path` | Absolute path to file | The SDK reads and sends the file content to the model | +### Blob Attachment + +| Field | Value | Description | +|-------|-------|-------------| +| `type` | `"blob"` | Indicates an inline data attachment | +| `data` | Base64-encoded string | The file content encoded as base64 | +| `mimeType` | MIME type string | The MIME type of the data (e.g., `"image/png"`) | +| `displayName` | *(optional)* string | User-facing display name for the attachment | + ### Language-Specific Usage -| Language | Attachment Syntax | -|----------|------------------| +| Language | File Attachment Syntax | +|----------|------------------------| | TypeScript | `attachments: [{ type: "file", path: sampleFile }]` | | Python | `"attachments": [{"type": "file", "path": sample_file}]` | | Go | `Attachments: []copilot.Attachment{{Type: "file", Path: sampleFile}}` | +| Language | Blob Attachment Syntax | +|----------|------------------------| +| TypeScript | `attachments: [{ type: "blob", data: base64Data, mimeType: "image/png" }]` | +| Python | `"attachments": [{"type": "blob", "data": base64_data, "mimeType": "image/png"}]` | +| Go | `Attachments: []copilot.Attachment{{Type: copilot.Blob, Data: &data, MIMEType: &mime}}` | + ## Sample Data The `sample-data.txt` file contains basic project metadata used as the attachment target: From eea6ea5327611a181a77ec4d1e3f54c609f594fd Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 12 Mar 2026 09:33:39 -0700 Subject: [PATCH 2/4] Update dotnet/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dotnet/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/README.md b/dotnet/README.md index c5b3857be..098a19e74 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -276,7 +276,8 @@ await session.SendAsync(new MessageOptions { new UserMessageDataAttachmentsItemFile { - Path = "/path/to/image.jpg" + Path = "/path/to/image.jpg", + DisplayName = "image.jpg", } } }); From ba04bdffaaccd9547eb88cdf24573002b5c3cdae Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 16 Mar 2026 10:20:37 -0700 Subject: [PATCH 3/4] Add E2E tests for blob attachments across all 4 SDKs Add blob attachment E2E tests for Node.js, Python, Go, and .NET SDKs. Each test sends a message with an inline base64-encoded PNG blob attachment and verifies the request is accepted by the replay proxy. - nodejs/test/e2e/session_config.test.ts: should accept blob attachments - python/e2e/test_session.py: test_should_accept_blob_attachments - go/internal/e2e/session_test.go: TestSessionBlobAttachment - dotnet/test/SessionTests.cs: Should_Accept_Blob_Attachments - test/snapshots/: request-only YAML snapshots for replay proxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/SessionTests.cs | 23 ++++++++++ go/internal/e2e/session_test.go | 42 +++++++++++++++++++ nodejs/test/e2e/session_config.test.ts | 19 +++++++++ python/e2e/test_session.py | 20 +++++++++ .../should_accept_blob_attachments.yaml | 8 ++++ .../should_accept_blob_attachments.yaml | 8 ++++ 6 files changed, 120 insertions(+) create mode 100644 test/snapshots/session/should_accept_blob_attachments.yaml create mode 100644 test/snapshots/session_config/should_accept_blob_attachments.yaml diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 8cd4c84e5..30a9135a5 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -538,6 +538,29 @@ public async Task DisposeAsync_From_Handler_Does_Not_Deadlock() await disposed.Task.WaitAsync(TimeSpan.FromSeconds(10)); } + [Fact] + public async Task Should_Accept_Blob_Attachments() + { + var session = await CreateSessionAsync(); + + await session.SendAsync(new MessageOptions + { + Prompt = "Describe this image", + Attachments = + [ + new UserMessageDataAttachmentsItemBlob + { + Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + MimeType = "image/png", + DisplayName = "test-pixel.png", + }, + ], + }); + + // Just verify send doesn't throw — blob attachment support varies by runtime + await session.DisposeAsync(); + } + private static async Task WaitForAsync(Func condition, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index c3c9cc009..052ae1580 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -938,6 +938,48 @@ func TestSetModelWithReasoningEffort(t *testing.T) { } } +func TestSessionBlobAttachment(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + t.Run("should accept blob attachments", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + mimeType := "image/png" + displayName := "test-pixel.png" + _, err = session.Send(t.Context(), copilot.MessageOptions{ + Prompt: "Describe this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &data, + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, + }) + if err != nil { + t.Fatalf("Send with blob attachment failed: %v", err) + } + + // Just verify send doesn't error — blob attachment support varies by runtime + session.Disconnect() + }) +} + func getToolNames(exchange testharness.ParsedHttpExchange) []string { var names []string for _, tool := range exchange.Request.Tools { diff --git a/nodejs/test/e2e/session_config.test.ts b/nodejs/test/e2e/session_config.test.ts index 2984c3c04..e27421ebf 100644 --- a/nodejs/test/e2e/session_config.test.ts +++ b/nodejs/test/e2e/session_config.test.ts @@ -43,6 +43,25 @@ describe("Session Configuration", async () => { } }); + it("should accept blob attachments", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + await session.send({ + prompt: "Describe this image", + attachments: [ + { + type: "blob", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + mimeType: "image/png", + displayName: "test-pixel.png", + }, + ], + }); + + // Just verify send doesn't throw — blob attachment support varies by runtime + await session.disconnect(); + }); + it("should accept message attachments", async () => { await writeFile(join(workDir, "attached.txt"), "This file is attached"); diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index a2bc33bdb..cb2063253 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -569,6 +569,26 @@ def on_event(event): assert event.data.new_model == "gpt-4.1" assert event.data.reasoning_effort == "high" + async def test_should_accept_blob_attachments(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + await session.send( + "Describe this image", + attachments=[ + { + "type": "blob", + "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "mimeType": "image/png", + "displayName": "test-pixel.png", + }, + ], + ) + + # Just verify send doesn't throw — blob attachment support varies by runtime + await session.disconnect() + def _get_system_message(exchange: dict) -> str: messages = exchange.get("request", {}).get("messages", []) diff --git a/test/snapshots/session/should_accept_blob_attachments.yaml b/test/snapshots/session/should_accept_blob_attachments.yaml new file mode 100644 index 000000000..89e5d47ed --- /dev/null +++ b/test/snapshots/session/should_accept_blob_attachments.yaml @@ -0,0 +1,8 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Describe this image diff --git a/test/snapshots/session_config/should_accept_blob_attachments.yaml b/test/snapshots/session_config/should_accept_blob_attachments.yaml new file mode 100644 index 000000000..89e5d47ed --- /dev/null +++ b/test/snapshots/session_config/should_accept_blob_attachments.yaml @@ -0,0 +1,8 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Describe this image From 166d89174bcb3367fa5f29fdd031e1a0ed290de5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 16 Mar 2026 11:07:21 -0700 Subject: [PATCH 4/4] fix(python): break long base64 string to satisfy ruff E501 line length Split the inline base64-encoded PNG data in the blob attachment E2E test into a local variable with implicit string concatenation so every line stays within the 100-character limit enforced by ruff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/e2e/test_session.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index cb2063253..272fd94a6 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -574,12 +574,19 @@ async def test_should_accept_blob_attachments(self, ctx: E2ETestContext): {"on_permission_request": PermissionHandler.approve_all} ) + # 1x1 transparent PNG pixel, base64-encoded + pixel_png = ( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY" + "AAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhg" + "GAWjR9awAAAABJRU5ErkJggg==" + ) + await session.send( "Describe this image", attachments=[ { "type": "blob", - "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "data": pixel_png, "mimeType": "image/png", "displayName": "test-pixel.png", },