Initial commit

Co-authored-by: scott-oai <142930063+scott-oai@users.noreply.github.com>
Co-authored-by: Zhuohan Li <zhuohan@openai.com>
This commit is contained in:
Dominik Kundel 2025-08-05 08:25:17 -07:00 committed by Scott Lessans
commit 253cdca537
70 changed files with 15013 additions and 0 deletions

44
demo/harmony-demo/.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
public/*.wasm
public/*.tiktoken

View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -0,0 +1,30 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack(config, { isServer, dev, webpack }) {
// Use the client static directory in the server bundle and prod mode
// Fixes `Error occurred prerendering page "/"`
config.output.webassemblyModuleFilename =
isServer && !dev
? "../static/[modulehash].wasm"
: "static/[modulehash].wasm";
// Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually
config.experiments = { ...config.experiments, asyncWebAssembly: true };
// https://nextjs.org/docs/app/building-your-application/optimizing/memory-usage#disable-webpack-cache
// This just stops building altogether:
// if (config.cache && !dev) {
// config.cache = Object.freeze({
// type: 'memory',
// })
// }
// Deubbing (vercel/next.js/issues/27650)
config.infrastructureLogging = { debug: /PackFileCache/ };
return config;
},
};
export default nextConfig;

2376
demo/harmony-demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
{
"name": "harmony-demo",
"version": "0.1.0",
"private": true,
"scripts": {
"postinstall": "cp ./node_modules/@openai/harmony/dist/web/*.wasm ./public/ && curl -L https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken -o ./public/o200k_base.tiktoken",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@openai/harmony": "file:../../javascript",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.525.0",
"next": "15.4.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.6",
"typescript": "^5"
}
}

View file

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View file

@ -0,0 +1,21 @@
<svg width="127" height="127" viewBox="0 0 127 127" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2048_18)">
<circle cx="63.5" cy="63.5" r="63.5" fill="white"/>
<mask id="mask0_2048_18" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="121" height="121">
<path d="M123.934 3.01678H3.10181V123.849H123.934V3.01678Z" fill="white"/>
</mask>
<g mask="url(#mask0_2048_18)">
<mask id="mask1_2048_18" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="22" y="23" width="83" height="81">
<path d="M104.146 23.1317H22.8965V103.656H104.146V23.1317Z" fill="white"/>
</mask>
<g mask="url(#mask1_2048_18)">
<path d="M54.0593 52.4423V44.7925C54.0593 44.1482 54.3011 43.6649 54.8645 43.3432L70.2451 34.4855C72.3387 33.2777 74.835 32.7143 77.4114 32.7143C87.0741 32.7143 93.1945 40.2032 93.1945 48.1748C93.1945 48.7383 93.1945 49.3826 93.1137 50.0269L77.1698 40.6859C76.2036 40.1225 75.237 40.1225 74.2708 40.6859L54.0593 52.4423ZM89.9731 82.2365V63.9572C89.9731 62.8296 89.4896 62.0243 88.5236 61.4608L68.3121 49.7043L74.9151 45.9194C75.4786 45.5977 75.9619 45.5977 76.5255 45.9194L91.9059 54.7771C96.3351 57.3542 99.3141 62.8296 99.3141 68.1438C99.3141 74.2635 95.6908 79.9005 89.9731 82.2359V82.2365ZM49.3084 66.1318L42.7054 62.2668C42.142 61.9451 41.9002 61.4616 41.9002 60.8174V43.1022C41.9002 34.4864 48.5032 27.9634 57.4416 27.9634C60.824 27.9634 63.9638 29.091 66.6218 31.1041L50.7585 40.2841C49.7926 40.8475 49.3092 41.6527 49.3092 42.7805V66.1325L49.3084 66.1318ZM63.5211 74.345L54.0593 69.0306V57.7576L63.5211 52.4432L72.9823 57.7576V69.0306L63.5211 74.345ZM69.6006 98.8248C66.2183 98.8248 63.0786 97.6972 60.4206 95.6843L76.2837 86.5041C77.2498 85.9407 77.7331 85.1355 77.7331 84.0077V60.6557L84.417 64.5207C84.9804 64.8424 85.2222 65.3257 85.2222 65.9702V83.6854C85.2222 92.3012 78.5384 98.8241 69.6006 98.8241V98.8248ZM50.5162 80.8679L35.1356 72.0104C30.7064 69.4332 27.7274 63.958 27.7274 58.6436C27.7274 52.4432 31.4316 46.8871 37.1485 44.5517V62.9111C37.1485 64.0387 37.632 64.8439 38.598 65.4075L58.7294 77.0831L52.1265 80.8679C51.5631 81.1897 51.0796 81.1897 50.5162 80.8679ZM49.6309 94.0739C40.5316 94.0739 33.8479 87.2293 33.8479 78.7742C33.8479 78.1299 33.9286 77.4857 34.0087 76.8414L49.8719 86.0214C50.8379 86.585 51.8047 86.585 52.7707 86.0214L72.9823 74.3459V81.9957C72.9823 82.64 72.7406 83.1233 72.177 83.445L56.7966 92.3027C54.7029 93.5105 52.2065 94.0739 49.6301 94.0739H49.6309ZM69.6006 103.656C79.3442 103.656 87.4767 96.731 89.3295 87.551C98.3481 85.2156 104.146 76.7605 104.146 68.1447C104.146 62.5077 101.73 57.0325 97.382 53.0866C97.7846 51.3955 98.0262 49.7043 98.0262 48.014C98.0262 36.4992 88.6852 27.8825 77.8948 27.8825C75.7211 27.8825 73.6274 28.2043 71.5336 28.9294C67.9095 25.3862 62.9169 23.1317 57.4416 23.1317C47.6981 23.1317 39.5656 30.0563 37.7129 39.2364C28.6942 41.5718 22.8965 50.0269 22.8965 58.6427C22.8965 64.2797 25.312 69.7549 29.6604 73.7008C29.2578 75.3919 29.0161 77.0831 29.0161 78.7735C29.0161 90.2883 38.3571 98.9048 49.1474 98.9048C51.3212 98.9048 53.415 98.5831 55.5088 97.858C59.132 101.401 64.1246 103.656 69.6006 103.656Z" fill="black"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_2048_18">
<rect width="127" height="127" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,196 @@
@import "tailwindcss";
@import "tw-animate-css";
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 400;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Regular.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 400;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-RegularItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 500;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Medium.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 500;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-MediumItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 600;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Semibold.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 600;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-SemiboldItalic.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: normal;
font-weight: 700;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-Bold.woff2")
format("woff2");
}
@font-face {
font-display: swap;
font-family: "OpenAI Sans";
font-style: italic;
font-weight: 700;
src: url("https://cdn.openai.com/common/fonts/openai-sans/OpenAISans-BoldItalic.woff2")
format("woff2");
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--default-font-family: "OpenAI Sans";
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--default-font-family: "OpenAI Sans";
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Harmony Demo",
description: "Harmony Demo",
icons: {
icon: "/openai_logo.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`antialiased`}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,14 @@
"use client";
import HarmonyDemo from "@/components/HarmonyDemo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function Home() {
return (
<QueryClientProvider client={queryClient}>
<HarmonyDemo />
</QueryClientProvider>
);
}

View file

@ -0,0 +1,487 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
load_harmony_encoding,
type JsHarmonyEncoding,
initSync as initHarmony,
JsStreamableParser,
} from "@openai/harmony";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
import { ChevronsUpDown, PlusIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
const SPECIAL_TOKENS_VALUES = {
"<|startoftext|>": 199998,
"<|endoftext|>": 199999,
"<|return|>": 200002,
"<|constrain|>": 200003,
"<|channel|>": 200005,
"<|start|>": 200006,
"<|end|>": 200007,
"<|message|>": 200008,
"<|call|>": 200012,
};
// Map from token id to special token string
const TOKEN_ID_TO_SPECIAL_TOKEN: Record<number, string> = Object.entries(
SPECIAL_TOKENS_VALUES
).reduce((acc, [key, value]) => {
acc[value] = key;
return acc;
}, {} as Record<number, string>);
const TOKEN_COLORS = [
"bg-red-300/30 data-[active=true]:bg-red-300",
"bg-teal-300/30 data-[active=true]:bg-teal-300",
"bg-amber-300/30 data-[active=true]:bg-amber-300",
"bg-indigo-300/30 data-[active=true]:bg-indigo-300",
"bg-pink-300/30 data-[active=true]:bg-pink-300",
"bg-green-300/30 data-[active=true]:bg-green-300",
"bg-orange-300/30 data-[active=true]:bg-orange-300",
"bg-purple-300/30 data-[active=true]:bg-purple-300",
"bg-yellow-300/30 data-[active=true]:bg-yellow-300",
"bg-cyan-300/30 data-[active=true]:bg-cyan-300",
"bg-lime-300/30 data-[active=true]:bg-lime-300",
"bg-violet-300/30 data-[active=true]:bg-violet-300",
"bg-emerald-300/30 data-[active=true]:bg-emerald-300",
"bg-rose-300/30 data-[active=true]:bg-rose-300",
"bg-sky-300/30 data-[active=true]:bg-sky-300",
"bg-fuchsia-300/30 data-[active=true]:bg-fuchsia-300",
"bg-blue-300/30 data-[active=true]:bg-blue-300",
];
const isSpecialToken = (token: string | number) => {
if (typeof token === "number") {
return Object.values(SPECIAL_TOKENS_VALUES).includes(token);
}
return Object.keys(SPECIAL_TOKENS_VALUES).includes(token);
};
const getSpecialTokenDescription = (token: number | string) => {
const tokenName =
typeof token === "number" ? TOKEN_ID_TO_SPECIAL_TOKEN[token] : token;
switch (tokenName) {
case "<|return|>":
return "Stop token. Indicates the model is done sampling its response.";
case "<|constrain|>":
return "Part of the message header to indicate the content type of the message. Primarily used for function calls to constrain the response to JSON.";
case "<|channel|>":
return "Part of the message header to indicate the channel the model's response is intended for.";
case "<|start|>":
return "Indicates the start of a new message. Tokens following this token are part of the header.";
case "<|end|>":
return "Indicates the end of a message. The next token is the start of the next message. This should not end sampling.";
case "<|message|>":
return "Completes the message header. Tokens following this token are part of the message content.";
case "<|call|>":
return "Stop token. Indicates the model is done sampling its response and is ready to perform the function call.";
default:
return `Unknown special token: ${tokenName}`;
}
};
function WrappedSpecialToken({
token,
children,
}: {
token: string | number;
children: React.ReactNode;
}) {
if (isSpecialToken(token)) {
return (
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>{getSpecialTokenDescription(token)}</TooltipContent>
</Tooltip>
);
}
return children;
}
function HighlightedTokens({
tokens,
highlightedIndex,
onHover,
wrapWhitespace,
highlightTokens,
editable,
onEdit,
}: {
tokens: (number | string)[];
highlightedIndex?: number;
onHover?: (index?: number) => void;
wrapWhitespace?: boolean;
highlightTokens?: boolean;
editable?: boolean;
onEdit?: (text: string | number[]) => void;
}) {
const areTokenIds = tokens.every((token) => typeof token === "number");
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(
areTokenIds ? JSON.stringify(tokens) : tokens.join("")
);
const editorRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (editorRef.current) {
editorRef.current.focus();
editorRef.current.select();
}
}, [editorRef.current, isEditing]);
useEffect(() => {
if (editable) {
setText(areTokenIds ? JSON.stringify(tokens) : tokens.join(""));
}
}, [tokens]);
if (isEditing) {
return (
<div className="aspect-video rounded-xl p-4 font-mono bg-gray-50 border border-gray-200">
<textarea
className="w-full h-full font-mono text-sm"
value={text}
onChange={(e) => setText(e.target.value)}
onBlur={() => {
setIsEditing(false);
onEdit?.(text);
}}
ref={editorRef}
/>
</div>
);
}
return (
<div
className="aspect-video rounded-xl p-4 font-mono bg-gray-50 border border-gray-200"
onClick={() => editable && setIsEditing(true)}
>
<pre
className={cn(
"text-sm max-h-40 overflow-y-auto min-h-full",
wrapWhitespace ? "whitespace-pre-wrap break-all" : "whitespace-pre"
)}
>
{areTokenIds && "["}
{tokens.map((token, index) => (
<WrappedSpecialToken token={token} key={index}>
<code
className={cn(
// highlightTokens ? "rounded-[3px] px-1 ml-1 mr-1" : "",
areTokenIds ? "ml-1 mr-1" : "",
// highlightedIndex === index ? "bg-blue-500 text-white" : "",
isSpecialToken(token) && !areTokenIds
? "font-semibold"
: isSpecialToken(token)
? "underline decoration-dotted"
: "",
highlightTokens ? TOKEN_COLORS[index % TOKEN_COLORS.length] : ""
)}
onMouseEnter={() => onHover?.(index)}
onMouseLeave={() => onHover?.(undefined)}
data-active={highlightedIndex === index}
>
{token}
</code>
{areTokenIds && index < tokens.length - 1 && ","}
</WrappedSpecialToken>
))}
{areTokenIds && "]"}
</pre>
</div>
);
}
type Message = {
role: "assistant" | "user" | "system" | "developer" | "tool";
channel?: string;
recipient?: string;
content_type?: string;
content: {
type: "text";
text: string;
}[];
};
function MessageItem({ message }: { message: Message }) {
return (
<div className="text-sm border rounded-md p-1">
<div className="border border-dashed border-gray-300 rounded-md">
<Collapsible defaultOpen>
<div className="flex items-center justify-between">
<span className="bg-gray-300 text-xs rounded-tl-sm rounded-br-sm px-2 py-1 -ml-[1px]">
header
</span>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="icon" className="size-4 px-4">
<ChevronsUpDown />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<div className="p-2 flex flex-col gap-1 mt-1">
<dl className="text-xs flex gap-1">
<dt>role:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.role}
</dd>
</dl>
{message.channel && (
<dl className="text-xs flex gap-1">
<dt>content:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.channel}
</dd>
</dl>
)}
{message.recipient && (
<dl className="text-xs flex gap-1">
<dt>recipient:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.recipient}
</dd>
</dl>
)}
{message.content_type && (
<dl className="text-xs flex gap-1">
<dt>content_type:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.content_type}
</dd>
</dl>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="text-sm flex flex-col gap-4 mt-3 mb-1">
{message.content.map((content: any, idx: number) => (
<pre
className="bg-gray-100/50 p-4 rounded-md text-xs overflow-x-scroll"
key={idx}
>
{content.text}
</pre>
))}
</div>
</div>
);
}
function useHarmony() {
const [harmony, setHarmony] = useState<JsHarmonyEncoding | null>(null);
useEffect(() => {
const fetchHarmony = async () => {
const response = await fetch("/openai_harmony_bg.wasm");
const wasm = await response.arrayBuffer();
initHarmony({ module: wasm });
console.log("wasm", wasm);
const encoding = await load_harmony_encoding(
"HarmonyGptOss",
typeof window !== "undefined" ? `${window.location.origin}/` : "/"
);
setHarmony(encoding);
};
fetchHarmony();
}, []);
return harmony;
}
function useTokens(text: string | undefined) {
const [highlightedIndex, setHighlightedIndex] = useState<number | undefined>(
undefined
);
const encoding = useHarmony();
const [tokenData, setTokenData] = useState<{
tokens: number[];
tokenTexts: string[];
messages: Message[];
}>({ tokens: [], tokenTexts: [], messages: [] });
useEffect(() => {
if (!text || !encoding) return;
const tokens = Array.from(encoding.encode(text, encoding.specialTokens()));
// @ts-ignore
const tokenTexts = tokens.map((t) => encoding.decodeUtf8([t]));
const parser = new JsStreamableParser(encoding, "user");
for (const token of tokens) {
parser.process(token);
}
const messages = JSON.parse(parser.messages).map((m: any) => ({
...m,
content:
typeof m.content === "string"
? [{ type: "text", text: m.content }]
: m.content,
}));
console.log("messages", typeof messages, messages);
setTokenData({
tokens: Array.from(tokens),
tokenTexts: Array.from(tokenTexts),
messages: messages,
});
}, [text, encoding]);
return {
...tokenData,
highlightedIndex,
setHighlightedIndex,
};
}
function App() {
const [text, setText] = useState(
`<|start|>user<|message|>What is the weather in SF?<|end|><|start|>assistant<|channel|>analysis<|message|>User asks: “What is the weather in SF?” We need to use lookup_weather tool.<|end|><|start|>assistant to=functions.lookup_weather<|channel|>commentary <|constrain|>json<|message|>{"location": "San Francisco"}<|end|><|start|>assistant`
);
const {
tokens,
tokenTexts,
highlightedIndex,
setHighlightedIndex,
messages,
} = useTokens(text);
const [wrapWhitespace, setWrapWhitespace] = useState(true);
const [highlightTokens, setHighlightTokens] = useState(true);
return (
<div className="p-8 pt-6 max-w-screen-xl mx-auto">
<h1 className="text-3xl font-bold mb-4 tracking-tight">
OpenAI harmony response format
</h1>
<p className="text-sm text-gray-500 mb-4">
The{" "}
<a
href="https://github.com/openai/open-models"
className="text-blue-500 focus:underline hover:underline"
target="_blank"
>
gpt-oss
</a>{" "}
models were trained on the harmony response format for defining
conversation structures, generating reasoning output and structuring
function calls. If you are not using gpt-oss directly but through an API
or a provider like{" "}
<a
href="https://ollama.ai"
className="text-blue-500"
target="_blank"
rel="noopener noreferrer"
>
Ollama
</a>
, you will not have to be concerned about this as your inference
solution will handle the formatting.
</p>
<p className="text-sm text-gray-500 mb-8">
If you are trying to count input tokens for text to an OpenAI model,
visit the{" "}
<a
href="https://platform.openai.com/tokenizer"
className="text-blue-500 focus:underline hover:underline"
target="_blank"
>
OpenAI Tokenizer
</a>{" "}
instead.
</p>
<div className="grid md:grid-cols-2 gap-4 mt-8">
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">Messages</h3>
<div className="flex flex-col gap-4">
{messages.map((message: Message, idx: number) => (
<MessageItem key={idx} message={message} />
))}
{/* <Button
variant="outline"
className="border border-dashed border-gray-300 rounded-md text-center py-8 text-sm text-gray-500 cursor-pointer"
>
<PlusIcon /> Add new message
</Button> */}
</div>
</div>
<div className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Decoded tokens{" "}
<span className="text-xs text-gray-500 font-light">
{/* (click to edit) */}
</span>
</h3>
<HighlightedTokens
tokens={tokenTexts}
highlightedIndex={highlightedIndex}
onHover={setHighlightedIndex}
wrapWhitespace={wrapWhitespace}
highlightTokens={highlightTokens}
editable={true}
onEdit={(text) => typeof text === "string" && setText(text)}
/>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Token IDs
</h3>
<HighlightedTokens
tokens={tokens}
highlightedIndex={highlightedIndex}
onHover={setHighlightedIndex}
wrapWhitespace={wrapWhitespace}
highlightTokens={highlightTokens}
/>
</div>
{/* <div className="flex flex-col md:flex-row gap-2">
<div className="flex items-center space-x-2">
<Switch
id="wrap-whitespace"
checked={wrapWhitespace}
onCheckedChange={() => setWrapWhitespace((val) => !val)}
/>
<Label htmlFor="wrap-whitespace">Wrap Whitespace</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="highlight-tokens"
checked={highlightTokens}
onCheckedChange={() => setHighlightTokens((val) => !val)}
/>
<Label htmlFor="highlight-tokens">Highlight Tokens</Label>
</div>
</div> */}
</div>
</div>
</div>
);
}
const HarmonyDemo = dynamic(() => Promise.resolve(App), {
ssr: false,
});
export default HarmonyDemo;

View file

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View file

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View file

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View file

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}