Skip to content

Commit cdbbcb6

Browse files
committed
feat: claude 3.7 model support
1 parent b7e26ba commit cdbbcb6

File tree

11 files changed

+125
-7
lines changed

11 files changed

+125
-7
lines changed

app/client/platforms/anthropic.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ import { ANTHROPIC_BASE_URL } from "@/app/constant";
2525
import {
2626
getMessageTextContent,
2727
getWebReferenceMessageTextContent,
28+
isClaudeThinkingModel,
2829
isVisionModel,
2930
} from "@/app/utils";
30-
import { preProcessImageContent, stream } from "@/app/utils/chat";
31+
import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
3132
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
3233
import { RequestPayload } from "./openai";
3334
import { fetch } from "@/app/utils/stream";
@@ -62,6 +63,10 @@ export interface AnthropicChatRequest {
6263
top_k?: number; // Only sample from the top K options for each subsequent token.
6364
metadata?: object; // An object describing metadata about the request.
6465
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
66+
thinking?: {
67+
type: "enabled";
68+
budget_tokens: number;
69+
};
6570
}
6671

6772
export interface ChatRequest {
@@ -269,10 +274,9 @@ export class ClaudeApi implements LLMApi {
269274
return res?.content?.[0]?.text;
270275
}
271276
async chat(options: ChatOptions): Promise<void> {
277+
const thinkingModel = isClaudeThinkingModel(options.config.model);
272278
const visionModel = isVisionModel(options.config.model);
273-
274279
const accessStore = useAccessStore.getState();
275-
276280
const shouldStream = !!options.config.stream;
277281

278282
const modelConfig = {
@@ -376,6 +380,21 @@ export class ClaudeApi implements LLMApi {
376380
top_k: 5,
377381
};
378382

383+
// extended-thinking
384+
// https://docs.anthropic.com/zh-CN/docs/build-with-claude/extended-thinking
385+
if (
386+
thinkingModel &&
387+
useChatStore.getState().currentSession().mask.claudeThinking
388+
) {
389+
requestBody.thinking = {
390+
type: "enabled",
391+
budget_tokens: modelConfig.budget_tokens,
392+
};
393+
requestBody.temperature = undefined;
394+
requestBody.top_p = undefined;
395+
requestBody.top_k = undefined;
396+
}
397+
379398
const path = this.path(Anthropic.ChatPath);
380399

381400
const controller = new AbortController();
@@ -390,7 +409,7 @@ export class ClaudeApi implements LLMApi {
390409
// .getAsTools(
391410
// useChatStore.getState().currentSession().mask?.plugin || [],
392411
// );
393-
return stream(
412+
return streamWithThink(
394413
path,
395414
requestBody,
396415
{
@@ -418,8 +437,9 @@ export class ClaudeApi implements LLMApi {
418437
name: string;
419438
};
420439
delta?: {
421-
type: "text_delta" | "input_json_delta";
440+
type: "text_delta" | "input_json_delta" | "thinking_delta";
422441
text?: string;
442+
thinking?: string;
423443
partial_json?: string;
424444
};
425445
index: number;
@@ -447,7 +467,24 @@ export class ClaudeApi implements LLMApi {
447467
runTools[index]["function"]["arguments"] +=
448468
chunkJson?.delta?.partial_json;
449469
}
450-
return chunkJson?.delta?.text;
470+
471+
console.log("chunkJson", chunkJson);
472+
473+
const isThinking = chunkJson?.delta?.type === "thinking_delta";
474+
const content = isThinking
475+
? chunkJson?.delta?.thinking
476+
: chunkJson?.delta?.text;
477+
478+
if (!content || content.trim().length === 0) {
479+
return {
480+
isThinking: false,
481+
content: "",
482+
};
483+
}
484+
return {
485+
isThinking,
486+
content,
487+
};
451488
},
452489
// processToolMessage, include tool_calls message and tool call results
453490
(

app/components/chat.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import ReloadIcon from "../icons/reload.svg";
5454
import HeadphoneIcon from "../icons/headphone.svg";
5555
import SearchCloseIcon from "../icons/search_close.svg";
5656
import SearchOpenIcon from "../icons/search_open.svg";
57+
import EnableThinkingIcon from "../icons/thinking_enable.svg";
58+
import DisableThinkingIcon from "../icons/thinking_disable.svg";
5759
import {
5860
ChatMessage,
5961
SubmitKey,
@@ -82,6 +84,7 @@ import {
8284
isSupportRAGModel,
8385
isFunctionCallModel,
8486
isFirefox,
87+
isClaudeThinkingModel,
8588
} from "../utils";
8689

8790
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@@ -511,6 +514,14 @@ export function ChatActions(props: {
511514
const pluginStore = usePluginStore();
512515
const session = chatStore.currentSession();
513516

517+
// switch thinking mode
518+
const claudeThinking = chatStore.currentSession().mask.claudeThinking;
519+
function switchClaudeThinking() {
520+
chatStore.updateTargetSession(session, (session) => {
521+
session.mask.claudeThinking = !session.mask.claudeThinking;
522+
});
523+
}
524+
514525
// switch web search
515526
const webSearch = chatStore.currentSession().mask.webSearch;
516527
function switchWebSearch() {
@@ -741,6 +752,7 @@ export function ChatActions(props: {
741752
text={currentModelName}
742753
icon={<RobotIcon />}
743754
/>
755+
744756
{!isFunctionCallModel(currentModel) && isEnableWebSearch && (
745757
<ChatAction
746758
onClick={switchWebSearch}
@@ -753,6 +765,20 @@ export function ChatActions(props: {
753765
/>
754766
)}
755767

768+
{isClaudeThinkingModel(currentModel) && (
769+
<ChatAction
770+
onClick={switchClaudeThinking}
771+
text={
772+
claudeThinking
773+
? Locale.Chat.InputActions.DisableThinking
774+
: Locale.Chat.InputActions.EnableThinking
775+
}
776+
icon={
777+
claudeThinking ? <EnableThinkingIcon /> : <DisableThinkingIcon />
778+
}
779+
/>
780+
)}
781+
756782
{showModelSelector && (
757783
<SearchSelector
758784
defaultSelectedValue={`${currentModel}@${currentProviderName}`}

app/components/model-config.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,29 @@ export function ModelConfigList(props: {
110110
></input>
111111
</ListItem>
112112

113+
{props.modelConfig?.providerName === ServiceProvider.Anthropic && (
114+
<ListItem
115+
title={Locale.Settings.BudgetTokens.Title}
116+
subTitle={Locale.Settings.BudgetTokens.SubTitle}
117+
>
118+
<input
119+
aria-label={Locale.Settings.BudgetTokens.Title}
120+
type="number"
121+
min={1024}
122+
max={32000}
123+
value={props.modelConfig.budget_tokens}
124+
onChange={(e) =>
125+
props.updateConfig(
126+
(config) =>
127+
(config.budget_tokens = ModalConfigValidator.budget_tokens(
128+
e.currentTarget.valueAsNumber,
129+
)),
130+
)
131+
}
132+
></input>
133+
</ListItem>
134+
)}
135+
113136
{props.modelConfig?.providerName == ServiceProvider.Google ? null : (
114137
<>
115138
<ListItem

app/constant.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ const googleModels = [
381381
];
382382

383383
const anthropicModels = [
384-
"claude-instant-1.2",
385384
"claude-2.0",
386385
"claude-2.1",
387386
"claude-3-sonnet-20240229",
@@ -393,6 +392,8 @@ const anthropicModels = [
393392
"claude-3-5-sonnet-20240620",
394393
"claude-3-5-sonnet-20241022",
395394
"claude-3-5-sonnet-latest",
395+
"claude-3-7-sonnet-20250219",
396+
"claude-3-7-sonnet-latest",
396397
];
397398

398399
const baiduModels = [

app/icons/thinking_disable.svg

Lines changed: 1 addition & 0 deletions
Loading

app/icons/thinking_enable.svg

Lines changed: 1 addition & 0 deletions
Loading

app/locales/cn.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ const cn = {
7575
UploadFle: "上传文件",
7676
OpenWebSearch: "开启联网",
7777
CloseWebSearch: "关闭联网",
78+
EnableThinking: "开启思考",
79+
DisableThinking: "关闭思考",
7880
},
7981
Rename: "重命名对话",
8082
Typing: "正在输入…",
@@ -545,6 +547,11 @@ const cn = {
545547
Title: "单次回复限制 (max_tokens)",
546548
SubTitle: "单次交互所用的最大 Token 数",
547549
},
550+
BudgetTokens: {
551+
Title: "扩展思考预算限制 (budget_tokens)",
552+
SubTitle:
553+
"内部推理过程中允许使用的最大令牌数,budget_tokens 必须始终小于 max_tokens。",
554+
},
548555
PresencePenalty: {
549556
Title: "话题新鲜度 (presence_penalty)",
550557
SubTitle: "值越大,越有可能扩展到新话题",

app/locales/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ const en: LocaleType = {
7777
UploadFle: "Upload Files",
7878
OpenWebSearch: "Enable Web Search",
7979
CloseWebSearch: "Disable Web Search",
80+
EnableThinking: "Enable Thinking",
81+
DisableThinking: "Disable Thinking",
8082
},
8183
Rename: "Rename Chat",
8284
Typing: "Typing…",
@@ -550,6 +552,11 @@ const en: LocaleType = {
550552
Title: "Max Tokens",
551553
SubTitle: "Maximum length of input tokens and generated tokens",
552554
},
555+
BudgetTokens: {
556+
Title: "Budget Tokens",
557+
SubTitle:
558+
"The budget_tokens parameter determines the maximum number of tokens Claude is allowed use for its internal reasoning process. budget_tokens must always be less than the max_tokens specified.",
559+
},
553560
PresencePenalty: {
554561
Title: "Presence Penalty",
555562
SubTitle:

app/store/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const DEFAULT_CONFIG = {
7373
temperature: 0.5,
7474
top_p: 1,
7575
max_tokens: 4000,
76+
budget_tokens: 1024,
7677
presence_penalty: 0,
7778
frequency_penalty: 0,
7879
sendMemory: true,
@@ -170,6 +171,9 @@ export const ModalConfigValidator = {
170171
max_tokens(x: number) {
171172
return limitNumber(x, 0, 512000, 1024);
172173
},
174+
budget_tokens(x: number) {
175+
return limitNumber(x, 0, 32000, 1024);
176+
},
173177
presence_penalty(x: number) {
174178
return limitNumber(x, -2, 2, 0);
175179
},

app/store/mask.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type Mask = {
1919
builtin: boolean;
2020
usePlugins?: boolean;
2121
webSearch?: boolean;
22+
claudeThinking?: boolean;
2223
// 上游插件业务参数
2324
plugin?: string[];
2425
enableArtifacts?: boolean;

0 commit comments

Comments
 (0)