openai-harmony-js
Version:
TypeScript/JavaScript utilities for the GPT‑OSS Harmony format: renderers, parsers, tokenizers, and streaming helpers
611 lines • 23.1 kB
JavaScript
const DEFAULT_DELIMS = {
start: "<|start|>",
message: "<|message|>",
end: "<|end|>",
};
export class HarmonyError extends Error {
constructor(code, message, details) {
super(message);
this.code = code;
this.details = details;
this.name = "HarmonyError";
}
}
function isHarmonyRole(value) {
return (value === "system" ||
value === "developer" ||
value === "user" ||
value === "assistant" ||
value === "tool");
}
function isHarmonyChannel(value) {
return (value === "message" ||
value === "reasoning" ||
value === "tool" ||
value === "function" ||
value === "error");
}
/**
* Renders a conversation into an array of Harmony tokens suitable for completion models.
*
* @param conversation The conversation to render.
* @param options Optional rendering options, including custom delimiters.
* @returns The list of tokens representing the conversation.
*/
export function renderConversation(conversation, options) {
const delims = options?.delimiters ?? DEFAULT_DELIMS;
const tokens = [];
for (const message of conversation.messages) {
tokens.push(delims.start + message.role);
for (const chunk of message.content) {
tokens.push(delims.message);
if (chunk.type === "text") {
tokens.push("text:" + chunk.channel + ":" + chunk.text);
}
else {
const encodedArgs = JSON.stringify(chunk.call.arguments);
tokens.push("tool:" + chunk.call.namespace + ":" + chunk.call.name + ":" + encodedArgs);
}
}
tokens.push(delims.end);
}
return tokens;
}
/**
* Creates a stateful parser for Harmony tokens.
*
* The returned object exposes two methods:
* - push(token, delimiters?): feed one token at a time
* - finish(): finalize parsing and return the accumulated conversation
*/
export function createParser() {
const state = { messages: [], current: undefined };
return {
push(token, delimiters) {
const delims = delimiters ?? DEFAULT_DELIMS;
if (token.startsWith(delims.start)) {
const roleRaw = token.slice(delims.start.length);
if (!isHarmonyRole(roleRaw)) {
const role = roleRaw;
throw new HarmonyError("INVALID_ROLE", "Unknown role: " + role, {
token,
role,
});
}
if (state.current !== undefined) {
const currentMessage = state.current;
state.messages.push(currentMessage);
}
state.current = { role: roleRaw, content: [] };
delete state.lastStreamingChannel;
state.expectingPayload = false;
return;
}
// Handle streaming-style channel declaration tokens produced by tokenizer
if (token.startsWith("<|channel|>")) {
const channelName = token.slice("<|channel|>".length);
if (channelName.length > 0) {
state.lastStreamingChannel = channelName;
}
return;
}
if (token === delims.message) {
// marker for following payload; handled when the next token arrives
state.expectingPayload = true;
return;
}
if (token === delims.end) {
if (state.current !== undefined) {
const currentMessage = state.current;
state.messages.push(currentMessage);
state.current = undefined;
}
delete state.lastStreamingChannel;
state.expectingPayload = false;
return;
}
if (state.current === undefined) {
throw new HarmonyError("UNEXPECTED_TOKEN", "Content outside of a message: " + token, {
token,
});
}
if (token.startsWith("text:")) {
const after = token.slice("text:".length);
const sep = after.indexOf(":");
if (sep < 0) {
throw new HarmonyError("INVALID_TEXT", "Missing channel in text token", {
token,
});
}
const channelRaw = after.slice(0, sep);
const text = after.slice(sep + 1);
if (!isHarmonyChannel(channelRaw)) {
throw new HarmonyError("INVALID_CHANNEL", "Unknown channel: " + channelRaw, {
token,
channel: channelRaw,
});
}
const textChunk = {
type: "text",
channel: channelRaw,
text,
};
state.current.content.push(textChunk);
state.expectingPayload = false;
return;
}
if (token.startsWith("tool:")) {
const rest = token.slice("tool:".length);
const first = rest.indexOf(":");
const second = rest.indexOf(":", first + 1);
if (first < 0 || second < 0) {
throw new HarmonyError("INVALID_TOOL", "Malformed tool token: missing separators", {
token,
});
}
const namespace = rest.slice(0, first);
const name = rest.slice(first + 1, second);
const argsRaw = rest.slice(second + 1);
let parsed;
try {
parsed = JSON.parse(argsRaw);
}
catch (e) {
throw new HarmonyError("INVALID_TOOL_ARGS", "Invalid JSON args", {
token,
args: argsRaw,
});
}
const toolChunk = {
type: "tool_call",
channel: "tool",
call: { namespace, name, arguments: parsed },
};
state.current.content.push(toolChunk);
state.expectingPayload = false;
return;
}
// If we reached here, token is a raw payload. Accept it only for streaming-style
// channel/message sequences; otherwise, keep strict error behavior.
if (state.expectingPayload && typeof state.lastStreamingChannel === "string") {
// We intentionally ignore embedding this content into structured messages to
// remain type-safe with HarmonyChannel; this path is to tolerate streaming
// strings being parsed with parseTokens/parseConversationFromString without errors.
state.expectingPayload = false;
return;
}
throw new HarmonyError("UNKNOWN_TOKEN", "Unknown token prefix: " + token, { token });
},
finish() {
if (state.current !== undefined) {
const currentMessage = state.current;
state.messages.push(currentMessage);
state.current = undefined;
}
return { messages: state.messages };
},
};
}
/**
* Parses a list of tokens into a Harmony conversation.
*
* @param tokens The tokens to parse.
* @param delimiters Optional custom delimiters if tokens were produced with non-default markers.
* @returns The parsed conversation.
*/
export function parseTokens(tokens, delimiters) {
const parser = createParser();
for (const t of tokens) {
parser.push(t, delimiters);
}
return parser.finish();
}
/**
* Safe parse variant that does not throw on errors. Instead, returns a discriminated result.
*/
export function tryParseTokens(tokens, delimiters) {
try {
const value = parseTokens(tokens, delimiters);
return { ok: true, value };
}
catch (err) {
if (err instanceof HarmonyError) {
return { ok: false, error: err };
}
let message = "Error";
if (err instanceof Error) {
message = err.message;
}
else if (typeof err === "string" && err.length > 0) {
message = err;
}
const unknown = new HarmonyError("UNKNOWN_ERROR", message);
return { ok: false, error: unknown };
}
}
// ---- String helpers for completion strings ----
/**
* Tokenizes a raw completion string containing Harmony markers into token units.
*
* The tokenizer is resilient to partial streams and custom delimiters.
*
* @param input The raw completion string.
* @param delimiters Optional custom delimiters.
* @returns An array of tokens.
*/
export function tokenizeCompletionString(input, delimiters) {
const delims = delimiters ?? DEFAULT_DELIMS;
const tokens = [];
let i = 0;
while (i < input.length) {
if (input.startsWith(delims.start, i)) {
// read role until next delimiter occurrence (message/end) or end of string
const roleStart = i + delims.start.length;
// role is contiguous letters
let j = roleStart;
while (j < input.length && /[a-z]/.test(input[j] ?? "")) {
j += 1;
}
tokens.push(input.slice(i, j));
i = j;
continue;
}
if (input.startsWith(delims.message, i)) {
tokens.push(delims.message);
i += delims.message.length;
continue;
}
if (input.startsWith(delims.end, i)) {
tokens.push(delims.end);
i += delims.end.length;
continue;
}
// read until next delimiter occurrence or end
const nextIdx = findNextDelimiterIndex(input, i, delims);
const payload = input.slice(i, nextIdx);
if (payload.length > 0) {
tokens.push(payload);
}
i = nextIdx;
}
return tokens;
}
/**
* Finds the next index in `input` at or after `from` that matches any delimiter.
*/
function findNextDelimiterIndex(input, from, delims) {
const indices = [
input.indexOf(delims.start, from),
input.indexOf(delims.message, from),
input.indexOf(delims.end, from),
];
const idxs = indices.filter((x) => x >= 0);
if (idxs.length === 0)
return input.length;
let min = idxs[0];
for (let k = 1; k < idxs.length; k += 1) {
const candidate = idxs[k];
if (candidate < min)
min = candidate;
}
return min;
}
/**
* Convenience helper that tokenizes and then parses a Harmony-formatted string.
*
* @param input The raw completion string.
* @param delimiters Optional custom delimiters.
* @returns The parsed conversation.
*/
export function parseConversationFromString(input, delimiters) {
return parseTokens(tokenizeCompletionString(input, delimiters), delimiters);
}
/**
* Detects whether the given input likely uses Harmony formatting.
*
* Accepts both complete and streaming (partial) strings.
*
* @param input The string to inspect.
* @param delimiters Optional custom delimiters.
* @returns True if the string appears to be Harmony-formatted.
*/
export function isHarmonyFormat(input, delimiters) {
if (typeof input !== "string" || input.length === 0)
return false;
const delims = delimiters ?? DEFAULT_DELIMS;
const hasStart = input.includes(delims.start);
const hasChannel = input.includes("<|channel|>");
const hasMessage = input.includes(delims.message);
const hasEnd = input.includes(delims.end);
return (hasStart || hasChannel) && hasChannel && (hasMessage || hasEnd);
}
/**
* Extracts the latest text for the `analysis` channel from a Harmony-formatted string.
* Returns an empty string if the input is not Harmony-formatted or lacks analysis content.
*/
export function extractReasoningContent(input) {
if (!isHarmonyFormat(input))
return "";
return safeExtractChannel(input, "analysis");
}
/**
* Extracts the latest text for the `final` channel from a Harmony-formatted string.
* Falls back to `commentary` if `final` is not present.
* For non-Harmony content, returns the original input unchanged.
*/
export function extractFinalContent(input) {
if (!isHarmonyFormat(input))
return input;
const finalText = safeExtractChannel(input, "final");
if (finalText.length > 0)
return finalText;
const commentaryText = safeExtractChannel(input, "commentary");
if (commentaryText.length > 0)
return commentaryText;
return "";
}
/**
* Extracts the latest text for the `commentary` channel from a Harmony-formatted string.
* Returns an empty string if the input is not Harmony-formatted or lacks commentary content.
*/
export function extractCommentaryContent(input) {
if (!isHarmonyFormat(input))
return "";
return safeExtractChannel(input, "commentary");
}
function safeExtractChannel(input, channel) {
// Use the LAST occurrence while streaming
const channelTag = "<|channel|>" + channel;
const idxChannel = input.lastIndexOf(channelTag);
if (idxChannel < 0)
return "";
const idxMsg = input.indexOf("<|message|>", idxChannel);
if (idxMsg < 0)
return "";
const start = idxMsg + "<|message|>".length;
// Find the earliest terminator after start
const nextEnd = input.indexOf("<|end|>", start);
const nextStart = input.indexOf("<|start|>", start);
const nextChannel = input.indexOf("<|channel|>", start);
let end = input.length;
if (nextEnd >= 0)
end = Math.min(end, nextEnd);
if (nextStart >= 0)
end = Math.min(end, nextStart);
if (nextChannel >= 0)
end = Math.min(end, nextChannel);
let text = input.slice(start, end);
// Remove any complete token markers that slipped in
text = text.replace(/<\|(start|message|end|channel)\|>/g, "");
// Remove any trailing partial token start like "<|c", "<|ch", or just "<|"
text = text.replace(/<\|[a-zA-Z]*$/g, "");
return text.trim();
}
// High-level helpers to mirror Python docs
/** A readonly list of all supported roles. */
export const AllRoles = [
"system",
"developer",
"user",
"assistant",
"tool",
];
export const Message = {
/**
* Builds a `HarmonyMessage` from a role and content.
*
* @param role The message role.
* @param content Either a raw string, a single content chunk, or an array of chunks.
* @returns A `HarmonyMessage` instance.
*/
fromRoleAndContent(role, content) {
const chunks = Array.isArray(content)
? content
: typeof content === "string"
? [{ type: "text", channel: "message", text: content }]
: [content];
return { role, content: chunks };
},
};
export const Conversation = {
/**
* Builds a `HarmonyConversation` from an array of messages.
*/
fromMessages(messages) {
return { messages };
},
};
/**
* Loads an encoder/decoder implementation for the given encoding name.
*
* Currently only `HARMONY_GPT_OSS` is supported.
*/
export function loadHarmonyEncoding(name) {
if (name !== "HARMONY_GPT_OSS") {
throw new HarmonyError("UNKNOWN_ENCODING", "Unknown encoding: " + name);
}
return {
renderConversationForCompletion(conversation) {
return renderConversation(conversation);
},
parseMessagesFromCompletionTokens(tokens) {
return parseTokens(tokens);
},
};
}
/**
* Incremental parser for Harmony-formatted streaming content.
*
* Feed content chunks with `addContent`, inspect the latest state, and call `reset` to reuse.
*/
export class HarmonyStreamParser {
constructor() {
this.buffer = "";
this.currentChannel = undefined;
}
/** Adds content to the internal buffer and returns the latest parse state. */
addContent(content) {
this.buffer += content;
return this.parseCurrentBuffer();
}
/** Resets internal state and clears the buffer. */
reset() {
this.buffer = "";
this.currentChannel = undefined;
}
/** Returns the full raw buffer accumulated so far. */
getBuffer() {
return this.buffer;
}
parseCurrentBuffer() {
const hasHarmonyMarkers = this.buffer.includes("<|start|>") ||
this.buffer.includes("<|channel|>") ||
this.buffer.includes("<|message|>");
if (!hasHarmonyMarkers) {
return {
isComplete: false,
currentAnalysis: "",
currentFinal: this.buffer,
currentCommentary: "",
bufferContent: this.buffer,
};
}
this.detectCurrentChannel();
// Build a window covering only the last assistant message (complete or in-progress)
// so earlier messages (e.g., previous finals) don't leak into current state.
const lastStartIdx = this.buffer.lastIndexOf("<|start|>");
let windowStart = 0;
if (lastStartIdx >= 0)
windowStart = lastStartIdx;
// If the role isn't assistant, we still keep the same logic; tests depend on last segment only
const endAfterStartIdx = this.buffer.indexOf("<|end|>", windowStart);
const windowEnd = endAfterStartIdx >= 0 ? endAfterStartIdx + "<|end|>".length : this.buffer.length;
const messageWindow = this.buffer.slice(windowStart, windowEnd);
let currentAnalysis = this.collectChannelText(messageWindow, "analysis");
let currentFinal = this.collectChannelText(messageWindow, "final");
let currentCommentary = this.collectChannelText(messageWindow, "commentary");
// If there's in-progress content (no end after last message marker), append to the active channel
const incompleteContent = this.extractIncompleteContentFromWindow(messageWindow);
if (incompleteContent.length > 0 && this.currentChannel) {
if (this.currentChannel === "analysis") {
currentAnalysis = currentAnalysis.length
? currentAnalysis + "\n" + incompleteContent
: incompleteContent;
}
else if (this.currentChannel === "final") {
currentFinal = currentFinal.length
? currentFinal + "\n" + incompleteContent
: incompleteContent;
}
else if (this.currentChannel === "commentary") {
currentCommentary = currentCommentary.length
? currentCommentary + "\n" + incompleteContent
: incompleteContent;
}
}
// Prefer the most recent segment for the currently active channel
if (typeof this.currentChannel === "string") {
const lastOnly = this.collectLastChannelText(messageWindow, this.currentChannel);
if (this.currentChannel === "analysis" && lastOnly.length > 0) {
currentAnalysis = lastOnly;
}
else if (this.currentChannel === "final" && lastOnly.length > 0) {
currentFinal = lastOnly;
}
else if (this.currentChannel === "commentary" && lastOnly.length > 0) {
currentCommentary = lastOnly;
}
}
const isComplete = (this.buffer.match(/<\|start\|>/g) || []).length ===
(this.buffer.match(/<\|end\|>/g) || []).length &&
(this.buffer.match(/<\|start\|>/g) || []).length > 0;
const result = {
isComplete,
currentAnalysis,
currentFinal,
currentCommentary,
bufferContent: this.buffer,
};
if (typeof this.currentChannel === "string") {
result.lastChannelDetected = this.currentChannel;
}
return result;
}
detectCurrentChannel() {
const matches = [...this.buffer.matchAll(/<\|channel\|>([^<\s]+)/g)];
if (matches.length > 0) {
const last = matches[matches.length - 1];
const channel = last[1];
if (typeof channel === "string" && channel.length > 0) {
this.currentChannel = channel;
}
}
}
extractCompleteMessages() {
const completeBlocks = [];
const startPattern = /<\|start\|>/g;
const endPattern = /<\|end\|>/g;
const starts = [];
const ends = [];
let m;
while ((m = startPattern.exec(this.buffer)) !== null) {
if (typeof m.index === "number") {
starts.push(m.index);
}
}
startPattern.lastIndex = 0;
while ((m = endPattern.exec(this.buffer)) !== null) {
if (typeof m.index === "number") {
ends.push(m.index + m[0].length);
}
}
const count = Math.min(starts.length, ends.length);
for (let i = 0; i < count; i += 1) {
const s = starts[i];
const e = ends[i];
if (s < e) {
completeBlocks.push(this.buffer.slice(s, e));
}
}
return completeBlocks.join("");
}
extractIncompleteContentFromWindow(win) {
const all = [...win.matchAll(/<\|message\|>/g)];
if (all.length === 0)
return "";
const last = all[all.length - 1];
const idxBase = typeof last.index === "number" ? last.index : -1;
const tokenLength = typeof last[0] === "string" ? last[0].length : 0;
const idx = idxBase >= 0 ? idxBase + tokenLength : -1;
if (idx < 0)
return "";
const after = win.slice(idx);
const endMatch = after.match(/<\|end\|>/);
if (endMatch)
return "";
return after.trim();
}
collectChannelText(input, channel) {
const re = new RegExp(String.raw `<\|channel\|>${channel}<\|message\|>(.*?)(?:(?=<\|channel\|>)|<\|end\|>|$)`, "gs");
const parts = [];
let m;
while ((m = re.exec(input)) !== null) {
const text = typeof m[1] === "string" ? m[1].trim() : "";
if (text.length > 0)
parts.push(text);
}
return parts.join("\n");
}
collectLastChannelText(input, channel) {
const re = new RegExp(String.raw `<\|channel\|>${channel}<\|message\|>(.*?)(?:(?=<\|channel\|>)|<\|end\|>|$)`, "gs");
let last = "";
let m;
while ((m = re.exec(input)) !== null) {
const text = typeof m[1] === "string" ? m[1].trim() : "";
if (text.length > 0)
last = text;
}
return last;
}
}
//# sourceMappingURL=index.js.map