fireworks-ai 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fireworks-ai might be problematic. Click here for more details.

package/README.md CHANGED
@@ -71,6 +71,21 @@ app.route("/", createAgentRouter({ sessions, translator }));
71
71
  serve({ fetch: app.fetch, port: 3000 });
72
72
  ```
73
73
 
74
+ ### Extended thinking
75
+
76
+ Enable Claude's chain-of-thought reasoning by setting `thinking` in your session config:
77
+
78
+ ```typescript
79
+ const sessions = new SessionManager(() => ({
80
+ context: {},
81
+ model: "claude-sonnet-4-5-20250929",
82
+ systemPrompt: "You are a helpful assistant.",
83
+ thinking: { type: "enabled", budgetTokens: 10000 },
84
+ }));
85
+ ```
86
+
87
+ When enabled, thinking blocks stream to the client as `thinking_delta` SSE events and render as collapsible cards in `MessageList`. Set `{ type: "disabled" }` to explicitly turn thinking off (it's off by default).
88
+
74
89
  This gives you four endpoints:
75
90
 
76
91
  | Method | Path | Description |
@@ -338,6 +353,7 @@ Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme
338
353
  | `PendingPermissions` | Renders pending tool approval and user question cards |
339
354
  | `ToolApprovalCard` | Tool approval card with Allow/Deny buttons |
340
355
  | `UserQuestionCard` | Structured question card with option selection |
356
+ | `ThinkingBlock` | Collapsible card displaying extended thinking text |
341
357
  | `ThinkingIndicator` | Animated dots shown while agent is generating |
342
358
  | `CollapsibleCard` | Expandable card wrapper |
343
359
  | `StatusDot` | Phase-colored status indicator |
@@ -351,7 +367,7 @@ Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme
351
367
  | Type | Description |
352
368
  |------|-------------|
353
369
  | `SSEEvent` | `{ event: string, data: string }` |
354
- | `ChatMessage` | `{ id, role, content, toolCalls? }` |
370
+ | `ChatMessage` | `{ id, role, content, thinking?, toolCalls? }` |
355
371
  | `ToolCallInfo` | `{ id, name, input, partialInput?, result?, error?, status }` |
356
372
  | `ToolCallPhase` | `"pending" \| "streaming_input" \| "running" \| "complete" \| "error"` |
357
373
  | `WidgetProps<TResult>` | Props passed to widget components |
@@ -374,6 +390,8 @@ Events emitted by the server, handled automatically by `useAgent`:
374
390
  | Event | Payload | Description |
375
391
  |-------|---------|-------------|
376
392
  | `message_start` | `{}` | Agent began generating a response |
393
+ | `thinking_start` | `{}` | Extended thinking block began |
394
+ | `thinking_delta` | `{ text }` | Streaming thinking text chunk |
377
395
  | `text_delta` | `{ text }` | Streaming text chunk |
378
396
  | `tool_start` | `{ id, name }` | Agent began calling a tool |
379
397
  | `tool_input_delta` | `{ id, partialJson }` | Streaming tool input JSON |
@@ -1,15 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useCallback, useRef, useState } from "react";
3
3
  import { cn } from "./cn.js";
4
4
  export function ChatInput({ onSend, placeholder = "Type a message...", disabled, className, }) {
5
5
  const [text, setText] = useState("");
6
+ const textareaRef = useRef(null);
6
7
  const isDisabled = disabled ?? false;
8
+ const MAX_H = 160; // matches max-h-40 (10rem)
9
+ const autoResize = useCallback(() => {
10
+ const el = textareaRef.current;
11
+ if (!el)
12
+ return;
13
+ el.style.height = "auto";
14
+ el.style.height = `${el.scrollHeight}px`;
15
+ el.style.overflowY = el.scrollHeight > MAX_H ? "auto" : "hidden";
16
+ }, []);
7
17
  function handleSend() {
8
18
  const trimmed = text.trim();
9
19
  if (!trimmed || isDisabled)
10
20
  return;
11
21
  onSend(trimmed);
12
22
  setText("");
23
+ requestAnimationFrame(() => {
24
+ const el = textareaRef.current;
25
+ if (el) {
26
+ el.style.height = "auto";
27
+ }
28
+ });
13
29
  }
14
30
  function handleKeyDown(e) {
15
31
  if (e.key === "Enter" && !e.shiftKey) {
@@ -17,5 +33,8 @@ export function ChatInput({ onSend, placeholder = "Type a message...", disabled,
17
33
  handleSend();
18
34
  }
19
35
  }
20
- return (_jsxs("div", { className: cn("flex items-end gap-2 p-3", className), children: [_jsx("textarea", { value: text, onChange: (e) => setText(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" }), _jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", "aria-hidden": "true", children: [_jsx("path", { d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" }), _jsx("path", { d: "m21.854 2.147-10.94 10.939" })] }) })] }));
36
+ return (_jsxs("div", { className: cn("flex items-end gap-2 p-3", className), children: [_jsx("textarea", { ref: textareaRef, value: text, onChange: (e) => {
37
+ setText(e.target.value);
38
+ autoResize();
39
+ }, onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring max-h-40 overflow-y-hidden" }), _jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", "aria-hidden": "true", children: [_jsx("path", { d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" }), _jsx("path", { d: "m21.854 2.147-10.94 10.939" })] }) })] }));
21
40
  }
@@ -4,17 +4,19 @@ import { useChatStore } from "./AgentProvider.js";
4
4
  import { cn } from "./cn.js";
5
5
  import { PendingPermissions } from "./PendingPermissions.js";
6
6
  import { TextMessage } from "./TextMessage.js";
7
+ import { ThinkingBlock } from "./ThinkingBlock.js";
7
8
  import { ThinkingIndicator } from "./ThinkingIndicator.js";
8
9
  import { ToolCallCard } from "./ToolCallCard.js";
9
10
  export function MessageList({ className }) {
10
11
  const messages = useChatStore((s) => s.messages);
11
12
  const streamingText = useChatStore((s) => s.streamingText);
13
+ const streamingThinking = useChatStore((s) => s.streamingThinking);
12
14
  const isThinking = useChatStore((s) => s.isThinking);
13
15
  const bottomRef = useRef(null);
14
16
  // biome-ignore lint/correctness/useExhaustiveDependencies: scroll on content changes
15
17
  useEffect(() => {
16
18
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
17
- }, [messages, streamingText, isThinking]);
19
+ }, [messages, streamingText, streamingThinking, isThinking]);
18
20
  return (_jsx("div", { className: cn("flex-1 overflow-y-auto", className), children: _jsxs("div", { className: "flex flex-col gap-3 p-4 text-sm", children: [messages.map((msg) => {
19
21
  if (msg.toolCalls?.length) {
20
22
  return (_jsx("div", { className: "flex flex-col gap-1.5", children: msg.toolCalls.map((tc) => (_jsx(ToolCallCard, { toolCall: tc }, tc.id))) }, msg.id));
@@ -22,9 +24,9 @@ export function MessageList({ className }) {
22
24
  if (msg.role === "system") {
23
25
  return (_jsx("div", { className: "text-center text-xs text-destructive", children: msg.content }, msg.id));
24
26
  }
25
- if (msg.content) {
26
- return (_jsx(TextMessage, { role: msg.role, content: msg.content }, msg.id));
27
+ if (msg.content || msg.thinking) {
28
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [msg.thinking && _jsx(ThinkingBlock, { thinking: msg.thinking }), msg.content && (_jsx(TextMessage, { role: msg.role, content: msg.content }))] }, msg.id));
27
29
  }
28
30
  return null;
29
- }), streamingText && _jsx(TextMessage, { role: "assistant", content: streamingText }), _jsx(PendingPermissions, {}), isThinking && _jsx(ThinkingIndicator, {}), _jsx("div", { ref: bottomRef })] }) }));
31
+ }), streamingThinking && _jsx(ThinkingBlock, { thinking: streamingThinking, streaming: true }), streamingText && _jsx(TextMessage, { role: "assistant", content: streamingText }), _jsx(PendingPermissions, {}), isThinking && _jsx(ThinkingIndicator, {}), _jsx("div", { ref: bottomRef })] }) }));
30
32
  }
@@ -0,0 +1,5 @@
1
+ export declare function ThinkingBlock({ thinking, streaming, className, }: {
2
+ thinking: string;
3
+ streaming?: boolean;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
3
+ import Markdown from "react-markdown";
4
+ import { cn } from "./cn.js";
5
+ const STORAGE_KEY = "fireworks-thinking-open";
6
+ function readPref() {
7
+ try {
8
+ return localStorage.getItem(STORAGE_KEY) === "true";
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function writePref(open) {
15
+ try {
16
+ localStorage.setItem(STORAGE_KEY, String(open));
17
+ }
18
+ catch {
19
+ /* SSR / restricted env */
20
+ }
21
+ }
22
+ export function ThinkingBlock({ thinking, streaming = false, className, }) {
23
+ const [open, setOpen] = useState(readPref);
24
+ const toggle = useCallback(() => {
25
+ setOpen((prev) => {
26
+ const next = !prev;
27
+ writePref(next);
28
+ return next;
29
+ });
30
+ }, []);
31
+ return (_jsx("div", { className: cn("flex justify-start", className), children: _jsxs("div", { className: "rounded-md border border-border/40 bg-accent/20 overflow-hidden", children: [_jsxs("button", { type: "button", onClick: toggle, className: "flex w-full items-center gap-2 px-2.5 py-1.5 text-xs text-muted-foreground/60 hover:bg-accent/30 transition-colors", children: [_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: cn("h-3 w-3 transition-transform", open && "rotate-90"), "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }), streaming && !open && (_jsx("span", { className: "h-2 w-2 shrink-0 rounded-full bg-yellow-500 animate-pulse" })), _jsx("span", { className: "truncate min-w-0", children: "Thinking" })] }), open && (_jsx("div", { className: "px-2.5 pt-1 pb-2", children: _jsx(ThinkingContent, { children: thinking }) }))] }) }));
32
+ }
33
+ function ThinkingContent({ children }) {
34
+ return (_jsx("div", { className: "text-xs text-muted-foreground/70 italic [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: _jsx(Markdown, { components: {
35
+ p: ({ children }) => _jsx("p", { className: "mb-2 last:mb-0", children: children }),
36
+ ul: ({ children }) => _jsx("ul", { className: "mb-2 ml-4 list-disc last:mb-0", children: children }),
37
+ ol: ({ children }) => _jsx("ol", { className: "mb-2 ml-4 list-decimal last:mb-0", children: children }),
38
+ li: ({ children }) => _jsx("li", { className: "mb-0.5", children: children }),
39
+ strong: ({ children }) => _jsx("strong", { className: "font-semibold", children: children }),
40
+ }, children: children }) }));
41
+ }
@@ -11,6 +11,7 @@ export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
11
11
  export { StatusDot } from "./StatusDot.js";
12
12
  export { type ChatStore, type ChatStoreShape, createChatStore } from "./store.js";
13
13
  export { TextMessage } from "./TextMessage.js";
14
+ export { ThinkingBlock } from "./ThinkingBlock.js";
14
15
  export { ThinkingIndicator } from "./ThinkingIndicator.js";
15
16
  export { ToolApprovalCard } from "./ToolApprovalCard.js";
16
17
  export { ToolCallCard } from "./ToolCallCard.js";
@@ -11,6 +11,7 @@ export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
11
11
  export { StatusDot } from "./StatusDot.js";
12
12
  export { createChatStore } from "./store.js";
13
13
  export { TextMessage } from "./TextMessage.js";
14
+ export { ThinkingBlock } from "./ThinkingBlock.js";
14
15
  export { ThinkingIndicator } from "./ThinkingIndicator.js";
15
16
  export { ToolApprovalCard } from "./ToolApprovalCard.js";
16
17
  export { ToolCallCard } from "./ToolCallCard.js";
@@ -6,13 +6,16 @@ interface ChatStoreState {
6
6
  isStreaming: boolean;
7
7
  isThinking: boolean;
8
8
  streamingText: string;
9
+ streamingThinking: string;
9
10
  pendingPermissions: PermissionRequest[];
10
11
  }
11
12
  interface ChatStoreActions {
12
13
  setSessionId: (id: string) => void;
13
14
  addUserMessage: (text: string) => void;
14
15
  appendStreamingText: (text: string) => void;
16
+ appendStreamingThinking: (text: string) => void;
15
17
  flushStreamingText: () => void;
18
+ flushStreamingThinking: () => void;
16
19
  addSystemMessage: (text: string) => void;
17
20
  startToolCall: (toolUseId: string, name: string) => void;
18
21
  appendToolInput: (toolUseId: string, partialJson: string) => void;
@@ -19,6 +19,7 @@ export function createChatStore() {
19
19
  isStreaming: false,
20
20
  isThinking: false,
21
21
  streamingText: "",
22
+ streamingThinking: "",
22
23
  pendingPermissions: [],
23
24
  setSessionId: (id) => set((s) => {
24
25
  s.sessionId = id;
@@ -40,6 +41,26 @@ export function createChatStore() {
40
41
  appendStreamingText: (text) => set((s) => {
41
42
  s.streamingText += text;
42
43
  }),
44
+ appendStreamingThinking: (text) => set((s) => {
45
+ s.streamingThinking += text;
46
+ }),
47
+ flushStreamingThinking: () => set((s) => {
48
+ if (s.streamingThinking) {
49
+ const last = s.messages[s.messages.length - 1];
50
+ if (last?.role === "assistant" && !last.toolCalls?.length) {
51
+ last.thinking = (last.thinking ?? "") + s.streamingThinking;
52
+ }
53
+ else {
54
+ s.messages.push({
55
+ id: `msg-${++nextId}`,
56
+ role: "assistant",
57
+ content: "",
58
+ thinking: s.streamingThinking,
59
+ });
60
+ }
61
+ s.streamingThinking = "";
62
+ }
63
+ }),
43
64
  flushStreamingText: () => set((s) => {
44
65
  if (s.streamingText) {
45
66
  const last = s.messages[s.messages.length - 1];
@@ -113,6 +134,7 @@ export function createChatStore() {
113
134
  s.isStreaming = false;
114
135
  s.isThinking = false;
115
136
  s.streamingText = "";
137
+ s.streamingThinking = "";
116
138
  s.pendingPermissions = [];
117
139
  }),
118
140
  })));
@@ -25,13 +25,19 @@ export function useAgent(store, config) {
25
25
  es.addEventListener("message_start", () => {
26
26
  store.getState().setThinking(true);
27
27
  });
28
+ es.addEventListener("thinking_delta", (e) => {
29
+ const { text } = JSON.parse(e.data);
30
+ store.getState().appendStreamingThinking(text);
31
+ });
28
32
  es.addEventListener("text_delta", (e) => {
29
33
  store.getState().setThinking(false);
34
+ store.getState().flushStreamingThinking();
30
35
  const { text } = JSON.parse(e.data);
31
36
  store.getState().appendStreamingText(text);
32
37
  });
33
38
  es.addEventListener("tool_start", (e) => {
34
39
  store.getState().setThinking(false);
40
+ store.getState().flushStreamingThinking();
35
41
  store.getState().flushStreamingText();
36
42
  const { id, name } = JSON.parse(e.data);
37
43
  store.getState().startToolCall(id, name);
@@ -57,6 +63,7 @@ export function useAgent(store, config) {
57
63
  store.getState().addPermissionRequest(request);
58
64
  });
59
65
  es.addEventListener("session_error", (e) => {
66
+ store.getState().flushStreamingThinking();
60
67
  store.getState().flushStreamingText();
61
68
  store.getState().setThinking(false);
62
69
  const { subtype } = JSON.parse(e.data);
@@ -64,12 +71,14 @@ export function useAgent(store, config) {
64
71
  store.getState().setStreaming(false);
65
72
  });
66
73
  es.addEventListener("turn_complete", () => {
74
+ store.getState().flushStreamingThinking();
67
75
  store.getState().flushStreamingText();
68
76
  store.getState().setThinking(false);
69
77
  store.getState().setStreaming(false);
70
78
  });
71
79
  es.addEventListener("error", () => {
72
80
  if (es.readyState === EventSource.CLOSED) {
81
+ store.getState().flushStreamingThinking();
73
82
  store.getState().flushStreamingText();
74
83
  store.getState().setStreaming(false);
75
84
  }
@@ -10,6 +10,12 @@ export interface SessionInit<TCtx> {
10
10
  allowedTools?: string[];
11
11
  maxTurns?: number;
12
12
  permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
13
+ thinking?: {
14
+ type: "enabled";
15
+ budgetTokens: number;
16
+ } | {
17
+ type: "disabled";
18
+ };
13
19
  }
14
20
  export interface Session<TCtx> {
15
21
  id: string;
@@ -72,6 +72,7 @@ export class SessionManager {
72
72
  includePartialMessages: true,
73
73
  abortController,
74
74
  ...(canUseTool ? { canUseTool } : {}),
75
+ ...(init.thinking ? { thinking: init.thinking } : {}),
75
76
  },
76
77
  });
77
78
  const session = {
@@ -6,6 +6,7 @@ export interface TranslatorConfig<TCtx> {
6
6
  export declare class MessageTranslator<TCtx> {
7
7
  private config;
8
8
  private toolNames;
9
+ private hadStreamThinking;
9
10
  constructor(config?: TranslatorConfig<TCtx>);
10
11
  translate(message: Record<string, unknown>, session: Session<TCtx>): SSEEvent[];
11
12
  private lastToolId;
@@ -2,6 +2,7 @@ import { PushChannel } from "./push-channel.js";
2
2
  export class MessageTranslator {
3
3
  config;
4
4
  toolNames = new Map();
5
+ hadStreamThinking = false;
5
6
  constructor(config) {
6
7
  this.config = config ?? {};
7
8
  }
@@ -15,6 +16,7 @@ export class MessageTranslator {
15
16
  const event = message.event;
16
17
  switch (event.type) {
17
18
  case "message_start": {
19
+ this.hadStreamThinking = false;
18
20
  events.push({ event: "message_start", data: "{}" });
19
21
  break;
20
22
  }
@@ -32,6 +34,10 @@ export class MessageTranslator {
32
34
  });
33
35
  break;
34
36
  }
37
+ case "thinking": {
38
+ events.push({ event: "thinking_start", data: "{}" });
39
+ break;
40
+ }
35
41
  case "web_search_tool_result": {
36
42
  const toolUseId = block.tool_use_id;
37
43
  const toolName = this.toolNames.get(toolUseId);
@@ -52,7 +58,14 @@ export class MessageTranslator {
52
58
  }
53
59
  case "content_block_delta": {
54
60
  const delta = event.delta;
55
- if (delta.type === "text_delta" && typeof delta.text === "string") {
61
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
62
+ this.hadStreamThinking = true;
63
+ events.push({
64
+ event: "thinking_delta",
65
+ data: JSON.stringify({ text: delta.thinking }),
66
+ });
67
+ }
68
+ else if (delta.type === "text_delta" && typeof delta.text === "string") {
56
69
  events.push({
57
70
  event: "text_delta",
58
71
  data: JSON.stringify({ text: delta.text }),
@@ -78,6 +91,15 @@ export class MessageTranslator {
78
91
  if (msg?.content) {
79
92
  for (const block of msg.content) {
80
93
  switch (block.type) {
94
+ case "thinking": {
95
+ if (!this.hadStreamThinking && typeof block.thinking === "string") {
96
+ events.push({
97
+ event: "thinking_delta",
98
+ data: JSON.stringify({ text: block.thinking }),
99
+ });
100
+ }
101
+ break;
102
+ }
81
103
  case "tool_use":
82
104
  case "server_tool_use": {
83
105
  const name = block.name;
package/dist/types.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface ChatMessage {
21
21
  id: string;
22
22
  role: "user" | "assistant" | "system";
23
23
  content: string;
24
+ thinking?: string;
24
25
  toolCalls?: ToolCallInfo[];
25
26
  }
26
27
  export interface WidgetProps<TResult = unknown> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fireworks-ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React + Hono toolkit for building chat UIs on top of the Claude Agent SDK",
5
5
  "license": "MIT",
6
6
  "author": "Dan Leeper",