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 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- fix: omit parallel_tool_calls in Go OpenAI SDK if it is set to true [#849](https://github.com/hypermodeinc/modus/pull/849)
- feat: use embedded postgres on Windows [#851](https://github.com/hypermodeinc/modus/pull/851)
- feat: add functions for parsing chat messages [#853](https://github.com/hypermodeinc/modus/pull/853)

## 2025-05-19 - Go SDK 0.18.0-alpha.2

Expand Down
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