feat: claude 3.7 model support

This commit is contained in:
Hk-Gosuto 2025-02-25 23:33:44 +08:00
parent b7e26ba18f
commit cdbbcb6ac3
11 changed files with 125 additions and 7 deletions

View File

@ -25,9 +25,10 @@ import { ANTHROPIC_BASE_URL } from "@/app/constant";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
isClaudeThinkingModel,
isVisionModel,
} from "@/app/utils";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -62,6 +63,10 @@ export interface AnthropicChatRequest {
top_k?: number; // Only sample from the top K options for each subsequent token.
metadata?: object; // An object describing metadata about the request.
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
thinking?: {
type: "enabled";
budget_tokens: number;
};
}
export interface ChatRequest {
@ -269,10 +274,9 @@ export class ClaudeApi implements LLMApi {
return res?.content?.[0]?.text;
}
async chat(options: ChatOptions): Promise<void> {
const thinkingModel = isClaudeThinkingModel(options.config.model);
const visionModel = isVisionModel(options.config.model);
const accessStore = useAccessStore.getState();
const shouldStream = !!options.config.stream;
const modelConfig = {
@ -376,6 +380,21 @@ export class ClaudeApi implements LLMApi {
top_k: 5,
};
// extended-thinking
// https://docs.anthropic.com/zh-CN/docs/build-with-claude/extended-thinking
if (
thinkingModel &&
useChatStore.getState().currentSession().mask.claudeThinking
) {
requestBody.thinking = {
type: "enabled",
budget_tokens: modelConfig.budget_tokens,
};
requestBody.temperature = undefined;
requestBody.top_p = undefined;
requestBody.top_k = undefined;
}
const path = this.path(Anthropic.ChatPath);
const controller = new AbortController();
@ -390,7 +409,7 @@ export class ClaudeApi implements LLMApi {
// .getAsTools(
// useChatStore.getState().currentSession().mask?.plugin || [],
// );
return stream(
return streamWithThink(
path,
requestBody,
{
@ -418,8 +437,9 @@ export class ClaudeApi implements LLMApi {
name: string;
};
delta?: {
type: "text_delta" | "input_json_delta";
type: "text_delta" | "input_json_delta" | "thinking_delta";
text?: string;
thinking?: string;
partial_json?: string;
};
index: number;
@ -447,7 +467,24 @@ export class ClaudeApi implements LLMApi {
runTools[index]["function"]["arguments"] +=
chunkJson?.delta?.partial_json;
}
return chunkJson?.delta?.text;
console.log("chunkJson", chunkJson);
const isThinking = chunkJson?.delta?.type === "thinking_delta";
const content = isThinking
? chunkJson?.delta?.thinking
: chunkJson?.delta?.text;
if (!content || content.trim().length === 0) {
return {
isThinking: false,
content: "",
};
}
return {
isThinking,
content,
};
},
// processToolMessage, include tool_calls message and tool call results
(

View File

@ -54,6 +54,8 @@ import ReloadIcon from "../icons/reload.svg";
import HeadphoneIcon from "../icons/headphone.svg";
import SearchCloseIcon from "../icons/search_close.svg";
import SearchOpenIcon from "../icons/search_open.svg";
import EnableThinkingIcon from "../icons/thinking_enable.svg";
import DisableThinkingIcon from "../icons/thinking_disable.svg";
import {
ChatMessage,
SubmitKey,
@ -82,6 +84,7 @@ import {
isSupportRAGModel,
isFunctionCallModel,
isFirefox,
isClaudeThinkingModel,
} from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -511,6 +514,14 @@ export function ChatActions(props: {
const pluginStore = usePluginStore();
const session = chatStore.currentSession();
// switch thinking mode
const claudeThinking = chatStore.currentSession().mask.claudeThinking;
function switchClaudeThinking() {
chatStore.updateTargetSession(session, (session) => {
session.mask.claudeThinking = !session.mask.claudeThinking;
});
}
// switch web search
const webSearch = chatStore.currentSession().mask.webSearch;
function switchWebSearch() {
@ -741,6 +752,7 @@ export function ChatActions(props: {
text={currentModelName}
icon={<RobotIcon />}
/>
{!isFunctionCallModel(currentModel) && isEnableWebSearch && (
<ChatAction
onClick={switchWebSearch}
@ -753,6 +765,20 @@ export function ChatActions(props: {
/>
)}
{isClaudeThinkingModel(currentModel) && (
<ChatAction
onClick={switchClaudeThinking}
text={
claudeThinking
? Locale.Chat.InputActions.DisableThinking
: Locale.Chat.InputActions.EnableThinking
}
icon={
claudeThinking ? <EnableThinkingIcon /> : <DisableThinkingIcon />
}
/>
)}
{showModelSelector && (
<SearchSelector
defaultSelectedValue={`${currentModel}@${currentProviderName}`}

View File

@ -110,6 +110,29 @@ export function ModelConfigList(props: {
></input>
</ListItem>
{props.modelConfig?.providerName === ServiceProvider.Anthropic && (
<ListItem
title={Locale.Settings.BudgetTokens.Title}
subTitle={Locale.Settings.BudgetTokens.SubTitle}
>
<input
aria-label={Locale.Settings.BudgetTokens.Title}
type="number"
min={1024}
max={32000}
value={props.modelConfig.budget_tokens}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.budget_tokens = ModalConfigValidator.budget_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
</ListItem>
)}
{props.modelConfig?.providerName == ServiceProvider.Google ? null : (
<>
<ListItem

View File

@ -381,7 +381,6 @@ const googleModels = [
];
const anthropicModels = [
"claude-instant-1.2",
"claude-2.0",
"claude-2.1",
"claude-3-sonnet-20240229",
@ -393,6 +392,8 @@ const anthropicModels = [
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
];
const baiduModels = [

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M9.97308 18H14.0269C14.1589 16.7984 14.7721 15.8065 15.7676 14.7226C15.8797 14.6006 16.5988 13.8564 16.6841 13.7501C17.5318 12.6931 18 11.385 18 10C18 6.68629 15.3137 4 12 4C8.68629 4 6 6.68629 6 10C6 11.3843 6.46774 12.6917 7.31462 13.7484C7.40004 13.855 8.12081 14.6012 8.23154 14.7218C9.22766 15.8064 9.84103 16.7984 9.97308 18ZM14 20H10V21H14V20ZM5.75395 14.9992C4.65645 13.6297 4 11.8915 4 10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10C20 11.8925 19.3428 13.6315 18.2443 15.0014C17.624 15.7748 16 17 16 18.5V21C16 22.1046 15.1046 23 14 23H10C8.89543 23 8 22.1046 8 21V18.5C8 17 6.37458 15.7736 5.75395 14.9992ZM13 10.0048H15.5L11 16.0048V12.0048H8.5L13 6V10.0048Z"></path></svg>

After

Width:  |  Height:  |  Size: 804 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7.94101 18C7.64391 16.7274 6.30412 15.6857 5.75395 14.9992C4.65645 13.6297 4 11.8915 4 10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10C20 11.8925 19.3428 13.6315 18.2443 15.0014C17.6944 15.687 16.3558 16.7276 16.059 18H7.94101ZM16 20V21C16 22.1046 15.1046 23 14 23H10C8.89543 23 8 22.1046 8 21V20H16ZM13 10.0048V6L8.5 12.0048H11V16.0048L15.5 10.0048H13Z"></path></svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@ -75,6 +75,8 @@ const cn = {
UploadFle: "上传文件",
OpenWebSearch: "开启联网",
CloseWebSearch: "关闭联网",
EnableThinking: "开启思考",
DisableThinking: "关闭思考",
},
Rename: "重命名对话",
Typing: "正在输入…",
@ -545,6 +547,11 @@ const cn = {
Title: "单次回复限制 (max_tokens)",
SubTitle: "单次交互所用的最大 Token 数",
},
BudgetTokens: {
Title: "扩展思考预算限制 (budget_tokens)",
SubTitle:
"内部推理过程中允许使用的最大令牌数budget_tokens 必须始终小于 max_tokens。",
},
PresencePenalty: {
Title: "话题新鲜度 (presence_penalty)",
SubTitle: "值越大,越有可能扩展到新话题",

View File

@ -77,6 +77,8 @@ const en: LocaleType = {
UploadFle: "Upload Files",
OpenWebSearch: "Enable Web Search",
CloseWebSearch: "Disable Web Search",
EnableThinking: "Enable Thinking",
DisableThinking: "Disable Thinking",
},
Rename: "Rename Chat",
Typing: "Typing…",
@ -550,6 +552,11 @@ const en: LocaleType = {
Title: "Max Tokens",
SubTitle: "Maximum length of input tokens and generated tokens",
},
BudgetTokens: {
Title: "Budget Tokens",
SubTitle:
"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.",
},
PresencePenalty: {
Title: "Presence Penalty",
SubTitle:

View File

@ -73,6 +73,7 @@ export const DEFAULT_CONFIG = {
temperature: 0.5,
top_p: 1,
max_tokens: 4000,
budget_tokens: 1024,
presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true,
@ -170,6 +171,9 @@ export const ModalConfigValidator = {
max_tokens(x: number) {
return limitNumber(x, 0, 512000, 1024);
},
budget_tokens(x: number) {
return limitNumber(x, 0, 32000, 1024);
},
presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0);
},

View File

@ -19,6 +19,7 @@ export type Mask = {
builtin: boolean;
usePlugins?: boolean;
webSearch?: boolean;
claudeThinking?: boolean;
// 上游插件业务参数
plugin?: string[];
enableArtifacts?: boolean;

View File

@ -401,6 +401,8 @@ export function isFunctionCallModel(modelName: string) {
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
"claude-3-5-haiku-latest",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
];
if (specialModels.some((keyword) => modelName === keyword)) return true;
return DEFAULT_MODELS.filter(
@ -408,6 +410,14 @@ export function isFunctionCallModel(modelName: string) {
).some((model) => model.name === modelName);
}
export function isClaudeThinkingModel(modelName: string) {
const specialModels = [
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
];
return specialModels.some((keyword) => modelName === keyword);
}
export function fetch(
url: string,
options?: Record<string, unknown>,