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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Notable additions, fixes, or breaking changes to the Freeplay SDK.

## [0.6.1] - 2026-03-10

### Fixed

- **`openai_responses` adapter**: Content blocks now use Responses API native types (`input_text`, `input_image`, `input_file`) instead of Chat Completions types (`text`, `image_url`, `file`) which OpenAI rejects.

## [0.6.0] - 2026-03-09

### Added
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "freeplay",
"version": "0.6.0",
"version": "0.6.1",
"description": "Node.js/Typescript SDK for Freeplay AI",
"type": "module",
"typesVersions": {
Expand Down
54 changes: 50 additions & 4 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,14 +350,60 @@ export class OpenAILLMAdapter implements ILLMAdapter<ProviderMessage[]> {
}
}

export class OpenAIResponsesAdapter extends OpenAILLMAdapter {
export class OpenAIResponsesAdapter implements ILLMAdapter<ProviderMessage[]> {
roleSupport = OPENAI_RESPONSES_ROLE_SUPPORT;

provider(): string {
return "openai";
}

toLLMSyntax(messages: ProviderMessage[]): ProviderMessage[] {
const formatted = super.toLLMSyntax(messages);
return formatted
return messages
.filter((message) => message.role !== "system")
.map((message) => ({ type: "message", ...message }));
.map((message) => {
if (Array.isArray(message.content)) {
const newContent = message.content.map((item) => {
if (isTextContent(item)) {
return { type: "input_text", text: item.text };
} else if (isMediaContentUrl(item)) {
if (item.slot_type !== "image") {
throw freeplayError(
"Message contains a non-image URL, but OpenAI Responses API only supports image URLs.",
);
}
return { type: "input_image", image_url: item.url };
} else if (isMediaContentBase64(item)) {
return this.formatBase64Content(item);
}
return item;
});

return { type: "message", ...message, content: newContent };
}

return { type: "message", ...message };
});
}

private formatBase64Content(
item: MediaContentBase64,
): Record<string, any> {
if (item.slot_type === "audio") {
throw freeplayError(
"Audio content is not yet supported by the OpenAI Responses API.",
);
} else if (item.slot_type === "file") {
return {
type: "input_file",
filename: `${item.slot_name}.${item.content_type.split("/")[1]}`,
file_data: `data:${item.content_type};base64,${item.data}`,
};
} else {
return {
type: "input_image",
image_url: `data:${item.content_type};base64,${item.data}`,
};
}
}
Comment thread
callingmedic911 marked this conversation as resolved.
}

Expand Down
163 changes: 163 additions & 0 deletions test/openaiResponses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,169 @@ describe("OpenAIResponsesAdapter", () => {
});
});

describe("OpenAIResponsesAdapter - content block types", () => {
const adapter = new OpenAIResponsesAdapter();

test("formats text content as input_text", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{ content_part_type: "text", text: "Hello world" },
],
},
];

const result = adapter.toLLMSyntax(messages);

expect(result).toEqual([
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Hello world" }],
},
]);
});

test("formats image URL as input_image with flat image_url", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{ content_part_type: "text", text: "Describe this" },
{
content_part_type: "media_url",
url: "http://example.com/img.png",
slot_name: "image-1",
slot_type: "image",
},
],
},
];

const result = adapter.toLLMSyntax(messages);

expect(result).toEqual([
{
type: "message",
role: "user",
content: [
{ type: "input_text", text: "Describe this" },
{ type: "input_image", image_url: "http://example.com/img.png" },
],
},
]);
});

test("formats base64 image as input_image with data URI", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{
content_part_type: "media_base64",
content_type: "image/png",
data: "abc123",
slot_name: "img",
slot_type: "image",
},
],
},
];

const result = adapter.toLLMSyntax(messages);

expect(result).toEqual([
{
type: "message",
role: "user",
content: [
{ type: "input_image", image_url: "data:image/png;base64,abc123" },
],
},
]);
});

test("formats base64 file as input_file", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{
content_part_type: "media_base64",
content_type: "application/pdf",
data: "pdfdata",
slot_name: "doc",
slot_type: "file",
},
],
},
];

const result = adapter.toLLMSyntax(messages);

expect(result).toEqual([
{
type: "message",
role: "user",
content: [
{
type: "input_file",
filename: "doc.pdf",
file_data: "data:application/pdf;base64,pdfdata",
},
],
},
]);
});

test("throws for audio base64 content", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{
content_part_type: "media_base64",
content_type: "audio/mpeg",
data: "audiodata",
slot_name: "clip",
slot_type: "audio",
},
],
},
];

expect(() => adapter.toLLMSyntax(messages)).toThrow(
"Audio content is not yet supported by the OpenAI Responses API.",
);
});

test("passes through history content blocks unchanged", () => {
const messages: ProviderMessage[] = [
{
role: "user",
content: [
{ type: "input_text", text: "already formatted" },
{ type: "input_image", image_url: "http://example.com/img.png" },
],
},
];

const result = adapter.toLLMSyntax(messages);

expect(result).toEqual([
{
type: "message",
role: "user",
content: [
{ type: "input_text", text: "already formatted" },
{ type: "input_image", image_url: "http://example.com/img.png" },
],
},
]);
});
});

describe("prepareMessages - role coercion", () => {
test("developer passes through for openai_responses", () => {
const adapter = new OpenAIResponsesAdapter();
Expand Down