Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 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
43 changes: 43 additions & 0 deletions sdk/assemblyscript/src/assembly/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2025 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2025 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import { expect, it, log, run } from "as-test";
import { JSON } from "json-as";

import {
SystemMessage,
UserMessage,
AssistantMessage,
RequestMessage,
parseMessages,
} from "../../models/openai/chat";

it("should round-trip chat messages", () => {
const msgs: RequestMessage[] = [
new SystemMessage("You are a helpful assistant."),
new UserMessage("What is the capital of France?"),
new AssistantMessage("The capital of France is Paris."),
];

const data = JSON.stringify(msgs);
const parsedMsgs = parseMessages(data);

expect(parsedMsgs.length).toBe(msgs.length);
for (let i = 0; i < msgs.length; i++) {
expect(msgs[i].role).toBe(parsedMsgs[i].role);
}

const roundTrip = JSON.stringify(parsedMsgs);

log(data);
log(roundTrip);
expect(roundTrip).toBe(data); // <----- this currently fails
});

run();
49 changes: 49 additions & 0 deletions sdk/assemblyscript/src/models/openai/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,7 @@ export class AssistantMessage<T> extends RequestMessage {
/**
* Data about a previous audio response from the model.
*/
@omitnull()
audio: AudioRef | null = null;

/**
Expand Down Expand Up @@ -1538,3 +1539,51 @@ export class AudioOutput {
*/
transcript!: string;
}

/**
* Represents a a raw message object, which is used when reconstructing messages from JSON.
* A raw message will round-trip all the JSON data, but does not expose the fields directly.
* (note, this type is not exported)
*/
class RawMessage extends RequestMessage {
constructor(data: string) {
const obj = JSON.parse<JSON.Obj>(data);
if (!obj.has("role")) {
throw new Error("Missing role field in message JSON.");
}

const role = obj.get("role")!.get<string>();
super(role);

this._data = data;
}

private _data: string;


@serializer
serialize(self: RawMessage): string {
return self._data;
}


@deserializer
deserialize(data: string): RawMessage {
return new RawMessage(data);
}
}

/**
* Parses a JSON-encoded request message into a list of RequestMessage objects.
* The resulting message objects are suitable for restoring a previous chat conversation.
* However, the original message types are not preserved.
*/
export function parseMessages(data: string): RequestMessage[] {
const messages = JSON.parse<JSON.Value[]>(data);
const results = new Array<RequestMessage>(messages.length);
for (let i = 0; i < messages.length; i++) {
const msg = messages[i].toString();
results[i] = new RawMessage(msg);
}
return results;
}
17 changes: 17 additions & 0 deletions sdk/assemblyscript/src/tests/openai.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2025 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import { readFileSync } from "fs";
import { instantiate } from "../build/openai.spec.js";
const binary = readFileSync("./build/openai.spec.wasm");
const module = new WebAssembly.Module(binary);
instantiate(module, {
env: {},
modus_models: {},
});
46 changes: 46 additions & 0 deletions sdk/go/pkg/models/openai/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -1234,3 +1234,49 @@ func (mi *ChatModelInput) MarshalJSON() ([]byte, error) {

return b, nil
}

// Represents a a raw message object, which is used when reconstructing messages from JSON.
// A raw message will round-trip all the JSON data, but does not expose the fields directly.
// (note, this type is not exported)
type rawMessage struct {
role string
data json.RawMessage
}

func (m *rawMessage) Role() string {
return m.role
}

func (m *rawMessage) MarshalJSON() ([]byte, error) {
if len(m.data) == 0 {
return nil, fmt.Errorf("missing data in message")
}
return m.data, nil
}

func (m *rawMessage) UnmarshalJSON(data []byte) error {
r := gjson.GetBytes(data, "role")
if !r.Exists() {
return fmt.Errorf("missing role field in message JSON")
}
m.role = r.String()
m.data = data
return nil
}

// Parses a JSON-encoded request message into a list of RequestMessage objects.
// The resulting message objects are suitable for restoring a previous chat conversation.
// However, the original message types are not preserved.
func ParseMessages(data []byte) ([]RequestMessage, error) {
var messages []rawMessage
if err := json.Unmarshal([]byte(data), &messages); err != nil {
return nil, err
}

results := make([]RequestMessage, len(messages))
for i := range messages {
results[i] = &messages[i]
}

return results, nil
}
53 changes: 53 additions & 0 deletions sdk/go/pkg/models/openai/chat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2025 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2025 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

package openai_test

import (
"bytes"
"encoding/json"
"testing"

"github.com/hypermodeinc/modus/sdk/go/pkg/models/openai"
)

func TestMessagesRoundTrip(t *testing.T) {
msgs := []openai.RequestMessage{
openai.NewSystemMessage("You are a helpful assistant."),
openai.NewUserMessage("What is the capital of France?"),
openai.NewAssistantMessage("The capital of France is Paris."),
}

data, err := json.Marshal(msgs)
if err != nil {
t.Fatalf("Failed to marshal messages: %v", err)
}

parsedMsgs, err := openai.ParseMessages(data)
if err != nil {
t.Fatalf("Failed to parse messages: %v", err)
}
if len(parsedMsgs) != len(msgs) {
t.Fatalf("Expected %d messages, but got %d", len(msgs), len(parsedMsgs))
}

for i, msg := range msgs {
if msg.Role() != parsedMsgs[i].Role() {
t.Errorf("Expected role %s for message %d, but got %s", msg.Role(), i, parsedMsgs[i].Role())
}
}

roundTrip, err := json.Marshal(parsedMsgs)
if err != nil {
t.Fatalf("Failed to marshal parsed messages: %v", err)
}
if !bytes.Equal(data, roundTrip) {
t.Fatalf("Expected original and parsed messages to have the same JSON representation, but they differ")
}
}
Loading