mirror of
https://github.com/openai/harmony.git
synced 2025-08-28 16:17:09 -04:00
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:
commit
253cdca537
70 changed files with 15013 additions and 0 deletions
BIN
demo/harmony-demo/src/app/favicon.ico
Normal file
BIN
demo/harmony-demo/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
196
demo/harmony-demo/src/app/globals.css
Normal file
196
demo/harmony-demo/src/app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
22
demo/harmony-demo/src/app/layout.tsx
Normal file
22
demo/harmony-demo/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
demo/harmony-demo/src/app/page.tsx
Normal file
14
demo/harmony-demo/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
487
demo/harmony-demo/src/components/HarmonyDemo.tsx
Normal file
487
demo/harmony-demo/src/components/HarmonyDemo.tsx
Normal 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;
|
59
demo/harmony-demo/src/components/ui/button.tsx
Normal file
59
demo/harmony-demo/src/components/ui/button.tsx
Normal 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 }
|
33
demo/harmony-demo/src/components/ui/collapsible.tsx
Normal file
33
demo/harmony-demo/src/components/ui/collapsible.tsx
Normal 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 }
|
24
demo/harmony-demo/src/components/ui/label.tsx
Normal file
24
demo/harmony-demo/src/components/ui/label.tsx
Normal 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 }
|
31
demo/harmony-demo/src/components/ui/switch.tsx
Normal file
31
demo/harmony-demo/src/components/ui/switch.tsx
Normal 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 }
|
61
demo/harmony-demo/src/components/ui/tooltip.tsx
Normal file
61
demo/harmony-demo/src/components/ui/tooltip.tsx
Normal 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 }
|
Loading…
Add table
Add a link
Reference in a new issue