fix(agent): strip reasoning_content from history, default to Llama 3.3 70B
Browse filesReasoning models (gpt-oss-*, DeepSeek R1, ...) emit a `reasoning` UI part
alongside their text answer. The AI SDK persists those parts and on the
next turn round-trips them as `reasoning_content` on the assistant
message - which some HF Inference Providers (notably Cerebras, the
default for gpt-oss) reject with 400 wrong_api_format. The model doesn't
need to see its own prior reasoning to keep the conversation going, so
filter those parts out before convertToModelMessages.
While we're at it, swap the default model from gpt-oss-120b to
meta-llama/Llama-3.3-70B-Instruct: it doesn't emit reasoning, works
across every provider exposing it, and is a more conservative starting
point for first-time forkers.
Co-authored-by: Cursor <cursoragent@cursor.com>
- backend/.env.example +2 -2
- backend/src/agent/chat.ts +1 -1
- backend/src/agent/stream-handler.ts +27 -2
- docs/SPECIFICATION.md +3 -2
backend/.env.example
CHANGED
|
@@ -87,8 +87,8 @@ OAUTH_CLIENT_SECRET=
|
|
| 87 |
# Override the default model id used by the chat agent. The list of
|
| 88 |
# supported models is in backend/src/agent/chat.ts (AVAILABLE_MODELS), but
|
| 89 |
# any model exposed by HF Inference Providers with tool-calling support
|
| 90 |
-
# works. Defaults to "
|
| 91 |
-
# HF_INFERENCE_MODEL=
|
| 92 |
|
| 93 |
# -----------------------------------------------------------------------------
|
| 94 |
# Publishing
|
|
|
|
| 87 |
# Override the default model id used by the chat agent. The list of
|
| 88 |
# supported models is in backend/src/agent/chat.ts (AVAILABLE_MODELS), but
|
| 89 |
# any model exposed by HF Inference Providers with tool-calling support
|
| 90 |
+
# works. Defaults to "meta-llama/Llama-3.3-70B-Instruct".
|
| 91 |
+
# HF_INFERENCE_MODEL=meta-llama/Llama-3.3-70B-Instruct
|
| 92 |
|
| 93 |
# -----------------------------------------------------------------------------
|
| 94 |
# Publishing
|
backend/src/agent/chat.ts
CHANGED
|
@@ -16,9 +16,9 @@ import type { Request, Response } from "express";
|
|
| 16 |
* own rates, see the docs for the source of truth.
|
| 17 |
*/
|
| 18 |
export const AVAILABLE_MODELS = [
|
|
|
|
| 19 |
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", context: "131K", cost: "$$" },
|
| 20 |
{ id: "openai/gpt-oss-20b", label: "GPT-OSS 20B", context: "131K", cost: "$" },
|
| 21 |
-
{ id: "meta-llama/Llama-3.3-70B-Instruct", label: "Llama 3.3 70B", context: "128K", cost: "$" },
|
| 22 |
{ id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", label: "Qwen3 Coder 480B", context: "262K", cost: "$$" },
|
| 23 |
{ id: "deepseek-ai/DeepSeek-V3.1", label: "DeepSeek V3.1", context: "128K", cost: "$$" },
|
| 24 |
];
|
|
|
|
| 16 |
* own rates, see the docs for the source of truth.
|
| 17 |
*/
|
| 18 |
export const AVAILABLE_MODELS = [
|
| 19 |
+
{ id: "meta-llama/Llama-3.3-70B-Instruct", label: "Llama 3.3 70B", context: "128K", cost: "$" },
|
| 20 |
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", context: "131K", cost: "$$" },
|
| 21 |
{ id: "openai/gpt-oss-20b", label: "GPT-OSS 20B", context: "131K", cost: "$" },
|
|
|
|
| 22 |
{ id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", label: "Qwen3 Coder 480B", context: "262K", cost: "$$" },
|
| 23 |
{ id: "deepseek-ai/DeepSeek-V3.1", label: "DeepSeek V3.1", context: "128K", cost: "$$" },
|
| 24 |
];
|
backend/src/agent/stream-handler.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
import { streamText, convertToModelMessages } from "ai";
|
|
|
|
| 2 |
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
| 3 |
import type { Request, Response } from "express";
|
| 4 |
import { extractToken } from "../auth.js";
|
| 5 |
|
| 6 |
-
export const DEFAULT_MODEL = "
|
| 7 |
|
| 8 |
/**
|
| 9 |
* Hugging Face Inference Providers exposes an OpenAI-compatible chat
|
|
@@ -44,6 +45,30 @@ function createProvider(apiKey: string) {
|
|
| 44 |
});
|
| 45 |
}
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
interface StreamChatOptions {
|
| 48 |
systemPrompt: string;
|
| 49 |
tools: Parameters<typeof streamText>[0]["tools"];
|
|
@@ -76,7 +101,7 @@ export async function streamChatResponse(
|
|
| 76 |
|
| 77 |
const provider = createProvider(apiKey);
|
| 78 |
const modelId = model || process.env.HF_INFERENCE_MODEL || DEFAULT_MODEL;
|
| 79 |
-
const modelMessages = await convertToModelMessages(messages);
|
| 80 |
|
| 81 |
const result = streamText({
|
| 82 |
model: provider.chatModel(modelId),
|
|
|
|
| 1 |
import { streamText, convertToModelMessages } from "ai";
|
| 2 |
+
import type { UIMessage } from "ai";
|
| 3 |
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
| 4 |
import type { Request, Response } from "express";
|
| 5 |
import { extractToken } from "../auth.js";
|
| 6 |
|
| 7 |
+
export const DEFAULT_MODEL = "meta-llama/Llama-3.3-70B-Instruct";
|
| 8 |
|
| 9 |
/**
|
| 10 |
* Hugging Face Inference Providers exposes an OpenAI-compatible chat
|
|
|
|
| 45 |
});
|
| 46 |
}
|
| 47 |
|
| 48 |
+
/**
|
| 49 |
+
* Drop `reasoning` parts from past assistant messages.
|
| 50 |
+
*
|
| 51 |
+
* Reasoning models (gpt-oss, DeepSeek R1, ...) emit a `reasoning` part
|
| 52 |
+
* alongside their text. The AI SDK persists those parts in the UI
|
| 53 |
+
* message history, and on the next turn they round-trip back to the
|
| 54 |
+
* provider as `reasoning_content` on the assistant message.
|
| 55 |
+
*
|
| 56 |
+
* Some HF Inference Providers (notably Cerebras for gpt-oss) reject
|
| 57 |
+
* that field with a 400 `wrong_api_format`. Since the model doesn't
|
| 58 |
+
* need to see its own prior reasoning to keep the conversation going,
|
| 59 |
+
* the safest fix is to strip those parts before conversion.
|
| 60 |
+
*/
|
| 61 |
+
function stripReasoningParts(messages: UIMessage[]): UIMessage[] {
|
| 62 |
+
return messages.map((m) => {
|
| 63 |
+
if (m.role !== "assistant" || !Array.isArray(m.parts)) return m;
|
| 64 |
+
const filtered = m.parts.filter(
|
| 65 |
+
(p) => typeof p.type !== "string" || p.type !== "reasoning",
|
| 66 |
+
);
|
| 67 |
+
if (filtered.length === m.parts.length) return m;
|
| 68 |
+
return { ...m, parts: filtered };
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
interface StreamChatOptions {
|
| 73 |
systemPrompt: string;
|
| 74 |
tools: Parameters<typeof streamText>[0]["tools"];
|
|
|
|
| 101 |
|
| 102 |
const provider = createProvider(apiKey);
|
| 103 |
const modelId = model || process.env.HF_INFERENCE_MODEL || DEFAULT_MODEL;
|
| 104 |
+
const modelMessages = await convertToModelMessages(stripReasoningParts(messages));
|
| 105 |
|
| 106 |
const result = streamText({
|
| 107 |
model: provider.chatModel(modelId),
|
docs/SPECIFICATION.md
CHANGED
|
@@ -127,9 +127,10 @@ flowchart LR
|
|
| 127 |
|
| 128 |
### 4.6 AI Agent
|
| 129 |
|
| 130 |
-
- Provider: Hugging Face Inference Providers (`https://router.huggingface.co/v1`), default model `
|
| 131 |
- Auth: per-request bearer token resolved from the editor's OAuth cookie when available, falling back to the server-side `HF_TOKEN`. On a HF Space with `inference-api` scope, no extra secret is needed - the logged-in user pays for their own inference under their HF quota.
|
| 132 |
- Streaming via Vercel AI SDK `streamText` over `@ai-sdk/openai-compatible`
|
|
|
|
| 133 |
- **Context**: document text, current selection, frontmatter (sent by frontend with each message)
|
| 134 |
- **Tools** (declarative, executed client-side by the frontend):
|
| 135 |
- `replaceSelection` - replace selected text
|
|
@@ -282,7 +283,7 @@ The publisher reads these same CSS files server-side and injects them inline int
|
|
| 282 |
| `OAUTH_SCOPES` | No (default `openid profile`) | OAuth scopes. Add `manage-repos` for dataset persistence and `inference-api` to power AI features with the user's token |
|
| 283 |
| `HF_DATASET_ID` | No | Override dataset name (default: `{SPACE_ID}-data`) |
|
| 284 |
| `HF_TOKEN` | For AI chat in local dev | Fallback Hub token for HF API + Inference Providers. Needs the "Make calls to Inference Providers" permission |
|
| 285 |
-
| `HF_INFERENCE_MODEL` | No (default `
|
| 286 |
| `ENABLE_PDF` | No (default true) | Toggle PDF/thumbnail generation |
|
| 287 |
|
| 288 |
### 6.4 Local Development
|
|
|
|
| 127 |
|
| 128 |
### 4.6 AI Agent
|
| 129 |
|
| 130 |
+
- Provider: Hugging Face Inference Providers (`https://router.huggingface.co/v1`), default model `meta-llama/Llama-3.3-70B-Instruct`
|
| 131 |
- Auth: per-request bearer token resolved from the editor's OAuth cookie when available, falling back to the server-side `HF_TOKEN`. On a HF Space with `inference-api` scope, no extra secret is needed - the logged-in user pays for their own inference under their HF quota.
|
| 132 |
- Streaming via Vercel AI SDK `streamText` over `@ai-sdk/openai-compatible`
|
| 133 |
+
- Reasoning parts from prior assistant turns are stripped before re-sending the history: providers like Cerebras reject `reasoning_content` on round-trip, and the model doesn't need to see its own past reasoning to continue the conversation.
|
| 134 |
- **Context**: document text, current selection, frontmatter (sent by frontend with each message)
|
| 135 |
- **Tools** (declarative, executed client-side by the frontend):
|
| 136 |
- `replaceSelection` - replace selected text
|
|
|
|
| 283 |
| `OAUTH_SCOPES` | No (default `openid profile`) | OAuth scopes. Add `manage-repos` for dataset persistence and `inference-api` to power AI features with the user's token |
|
| 284 |
| `HF_DATASET_ID` | No | Override dataset name (default: `{SPACE_ID}-data`) |
|
| 285 |
| `HF_TOKEN` | For AI chat in local dev | Fallback Hub token for HF API + Inference Providers. Needs the "Make calls to Inference Providers" permission |
|
| 286 |
+
| `HF_INFERENCE_MODEL` | No (default `meta-llama/Llama-3.3-70B-Instruct`) | Default chat-completion model id served by HF Inference Providers |
|
| 287 |
| `ENABLE_PDF` | No (default true) | Toggle PDF/thumbnail generation |
|
| 288 |
|
| 289 |
### 6.4 Local Development
|