|
| 1 | +import React from "react"; |
| 2 | +import type { BaseLanguageModelParams } from "@langchain/core/language_models/base"; |
| 3 | +import type { AnthropicInput } from "@langchain/anthropic"; |
| 4 | +import type { OpenAIChatInput } from "@langchain/openai"; |
| 5 | + |
| 6 | +import { IconExternalLink } from "#/ui/icons"; |
| 7 | +import LangchainBase from "./base"; |
| 8 | +import LLMProviderInterface, { LLMConfig } from "../interface"; |
| 9 | +import { HeaderEditor, ModelsHandler } from "../utils"; |
| 10 | +import { Dropdown, Input, Message, SettingItem, useGlobal } from "../refs"; |
| 11 | +import { |
| 12 | + calculateMiniMaxPrice, |
| 13 | + getMiniMaxEndpoint, |
| 14 | + getMiniMaxModelKwargs, |
| 15 | + MINIMAX_DOCS, |
| 16 | + MINIMAX_MODELS, |
| 17 | + MiniMaxProtocol, |
| 18 | + MiniMaxRegion, |
| 19 | + MiniMaxServiceTier, |
| 20 | + MiniMaxThinkingMode, |
| 21 | + normalizeMiniMaxMessages, |
| 22 | + wrapMiniMaxAnthropicFetch, |
| 23 | +} from "./minimaxConfig"; |
| 24 | + |
| 25 | +const commonDefaultValues = { |
| 26 | + apiRegion: "Global" as MiniMaxRegion, |
| 27 | + model: MINIMAX_MODELS.m3, |
| 28 | + thinkingMode: "adaptive" as const, |
| 29 | + serviceTier: "standard" as const, |
| 30 | +}; |
| 31 | + |
| 32 | +const openAIDefaultValues = { |
| 33 | + ...commonDefaultValues, |
| 34 | + basePath: getMiniMaxEndpoint("Global", "openai"), |
| 35 | +}; |
| 36 | + |
| 37 | +const anthropicDefaultValues = { |
| 38 | + ...commonDefaultValues, |
| 39 | + basePath: getMiniMaxEndpoint("Global", "anthropic"), |
| 40 | +}; |
| 41 | + |
| 42 | +type MiniMaxDefaultValues = { |
| 43 | + apiRegion: MiniMaxRegion; |
| 44 | + model: string; |
| 45 | + thinkingMode: MiniMaxThinkingMode; |
| 46 | + serviceTier: MiniMaxServiceTier; |
| 47 | + basePath: string; |
| 48 | +}; |
| 49 | + |
| 50 | +function MiniMaxSettings({ |
| 51 | + props, |
| 52 | + protocol, |
| 53 | + defaultValues, |
| 54 | +}: { |
| 55 | + props: Parameters<LLMProviderInterface["RenderSettings"]>[0]; |
| 56 | + protocol: MiniMaxProtocol; |
| 57 | + defaultValues: MiniMaxDefaultValues; |
| 58 | +}) { |
| 59 | + const global = useGlobal(); |
| 60 | + const id = props.self.id; |
| 61 | + const config = (global.plugin.settings.LLMProviderOptions[id] ??= { |
| 62 | + ...defaultValues, |
| 63 | + }); |
| 64 | + const isM3 = (config.model || defaultValues.model) === MINIMAX_MODELS.m3; |
| 65 | + |
| 66 | + return ( |
| 67 | + <> |
| 68 | + <SettingItem |
| 69 | + name="API Key" |
| 70 | + register={props.register} |
| 71 | + sectionId={props.sectionId} |
| 72 | + > |
| 73 | + <Input |
| 74 | + type="password" |
| 75 | + value={config.api_key || ""} |
| 76 | + setValue={async (value) => { |
| 77 | + config.api_key = value; |
| 78 | + global.triggerReload(); |
| 79 | + global.plugin.encryptAllKeys(); |
| 80 | + await global.plugin.saveSettings(); |
| 81 | + }} |
| 82 | + /> |
| 83 | + </SettingItem> |
| 84 | + |
| 85 | + <SettingItem |
| 86 | + name="API Region" |
| 87 | + register={props.register} |
| 88 | + sectionId={props.sectionId} |
| 89 | + > |
| 90 | + <Dropdown |
| 91 | + value={config.apiRegion || defaultValues.apiRegion} |
| 92 | + values={["Global", "China"]} |
| 93 | + setValue={async (value) => { |
| 94 | + const region = value as MiniMaxRegion; |
| 95 | + config.apiRegion = region; |
| 96 | + config.basePath = getMiniMaxEndpoint(region, protocol); |
| 97 | + global.triggerReload(); |
| 98 | + await global.plugin.saveSettings(); |
| 99 | + }} |
| 100 | + /> |
| 101 | + </SettingItem> |
| 102 | + |
| 103 | + <SettingItem |
| 104 | + name="Base Path" |
| 105 | + description={`${ |
| 106 | + protocol === "openai" ? "OpenAI" : "Anthropic" |
| 107 | + }-compatible API endpoint`} |
| 108 | + register={props.register} |
| 109 | + sectionId={props.sectionId} |
| 110 | + > |
| 111 | + <Input |
| 112 | + value={config.basePath || defaultValues.basePath} |
| 113 | + placeholder="Enter your API Base Path" |
| 114 | + setValue={async (value) => { |
| 115 | + config.basePath = value || defaultValues.basePath; |
| 116 | + global.triggerReload(); |
| 117 | + await global.plugin.saveSettings(); |
| 118 | + }} |
| 119 | + /> |
| 120 | + </SettingItem> |
| 121 | + |
| 122 | + <ModelsHandler |
| 123 | + register={props.register} |
| 124 | + sectionId={props.sectionId} |
| 125 | + llmProviderId={props.self.originalId || id} |
| 126 | + default_values={defaultValues} |
| 127 | + config={config} |
| 128 | + /> |
| 129 | + |
| 130 | + {isM3 && ( |
| 131 | + <> |
| 132 | + <SettingItem |
| 133 | + name="Thinking Mode" |
| 134 | + register={props.register} |
| 135 | + sectionId={props.sectionId} |
| 136 | + > |
| 137 | + <Dropdown |
| 138 | + value={config.thinkingMode || defaultValues.thinkingMode} |
| 139 | + values={["adaptive", "disabled"]} |
| 140 | + setValue={async (value) => { |
| 141 | + config.thinkingMode = value; |
| 142 | + global.triggerReload(); |
| 143 | + await global.plugin.saveSettings(); |
| 144 | + }} |
| 145 | + /> |
| 146 | + </SettingItem> |
| 147 | + |
| 148 | + <SettingItem |
| 149 | + name="Service Tier" |
| 150 | + register={props.register} |
| 151 | + sectionId={props.sectionId} |
| 152 | + > |
| 153 | + <Dropdown |
| 154 | + value={config.serviceTier || defaultValues.serviceTier} |
| 155 | + values={["standard", "priority"]} |
| 156 | + setValue={async (value) => { |
| 157 | + config.serviceTier = value; |
| 158 | + global.triggerReload(); |
| 159 | + await global.plugin.saveSettings(); |
| 160 | + }} |
| 161 | + /> |
| 162 | + </SettingItem> |
| 163 | + </> |
| 164 | + )} |
| 165 | + |
| 166 | + <HeaderEditor |
| 167 | + enabled={!!config.headers} |
| 168 | + setEnabled={async (value) => { |
| 169 | + config.headers = value ? "{}" : undefined; |
| 170 | + global.triggerReload(); |
| 171 | + await global.plugin.saveSettings(); |
| 172 | + }} |
| 173 | + headers={config.headers} |
| 174 | + setHeaders={async (value) => { |
| 175 | + config.headers = value; |
| 176 | + global.triggerReload(); |
| 177 | + await global.plugin.saveSettings(); |
| 178 | + }} |
| 179 | + /> |
| 180 | + |
| 181 | + <div className="plug-tg-flex plug-tg-flex-col plug-tg-gap-2"> |
| 182 | + <div className="plug-tg-text-lg plug-tg-opacity-70">Useful links</div> |
| 183 | + {(["Global", "China"] as MiniMaxRegion[]).map((region) => ( |
| 184 | + <a key={region} href={MINIMAX_DOCS[region][protocol]}> |
| 185 | + <SettingItem |
| 186 | + name={`${region} API documentation`} |
| 187 | + className="plug-tg-text-xs plug-tg-opacity-50 hover:plug-tg-opacity-100" |
| 188 | + register={props.register} |
| 189 | + sectionId={props.sectionId} |
| 190 | + > |
| 191 | + <IconExternalLink /> |
| 192 | + </SettingItem> |
| 193 | + </a> |
| 194 | + ))} |
| 195 | + </div> |
| 196 | + </> |
| 197 | + ); |
| 198 | +} |
| 199 | + |
| 200 | +abstract class LangchainMiniMaxBaseProvider extends LangchainBase { |
| 201 | + abstract protocol: MiniMaxProtocol; |
| 202 | + abstract default_values: MiniMaxDefaultValues; |
| 203 | + abstract getConfig(options: LLMConfig): any; |
| 204 | + |
| 205 | + RenderSettings(props: Parameters<LLMProviderInterface["RenderSettings"]>[0]) { |
| 206 | + return ( |
| 207 | + <MiniMaxSettings |
| 208 | + props={props} |
| 209 | + protocol={this.protocol} |
| 210 | + defaultValues={this.default_values} |
| 211 | + /> |
| 212 | + ); |
| 213 | + } |
| 214 | + |
| 215 | + generate( |
| 216 | + messages: Message[], |
| 217 | + reqParams: Partial<Omit<LLMConfig, "n">>, |
| 218 | + onToken?: ( |
| 219 | + token: string, |
| 220 | + first: boolean |
| 221 | + ) => Promise<string | void | null | undefined>, |
| 222 | + customConfig?: any |
| 223 | + ) { |
| 224 | + return super.generate( |
| 225 | + normalizeMiniMaxMessages(messages, this.protocol), |
| 226 | + reqParams, |
| 227 | + onToken, |
| 228 | + customConfig |
| 229 | + ); |
| 230 | + } |
| 231 | + |
| 232 | + generateMultiple(messages: Message[], reqParams: Partial<LLMConfig>) { |
| 233 | + return super.generateMultiple( |
| 234 | + normalizeMiniMaxMessages(messages, this.protocol), |
| 235 | + reqParams |
| 236 | + ); |
| 237 | + } |
| 238 | + |
| 239 | + async calcPrice(tokens: number, reqParams: Partial<LLMConfig>) { |
| 240 | + return calculateMiniMaxPrice( |
| 241 | + tokens, |
| 242 | + reqParams.max_tokens || 100, |
| 243 | + reqParams.model || MINIMAX_MODELS.m3, |
| 244 | + reqParams.serviceTier || "standard" |
| 245 | + ); |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +export default class LangchainMiniMaxOpenAIProvider |
| 250 | + extends LangchainMiniMaxBaseProvider |
| 251 | + implements LLMProviderInterface |
| 252 | +{ |
| 253 | + static provider = "Langchain" as const; |
| 254 | + static id = "MiniMax OpenAI (Langchain)" as const; |
| 255 | + static slug = "minimaxOpenAI" as const; |
| 256 | + static displayName = "MiniMax OpenAI"; |
| 257 | + |
| 258 | + protocol = "openai" as const; |
| 259 | + streamable = true; |
| 260 | + id = LangchainMiniMaxOpenAIProvider.id; |
| 261 | + provider = LangchainMiniMaxOpenAIProvider.provider; |
| 262 | + originalId = LangchainMiniMaxOpenAIProvider.id; |
| 263 | + default_values = openAIDefaultValues; |
| 264 | + |
| 265 | + async load() { |
| 266 | + const { ChatOpenAI } = await import("@langchain/openai"); |
| 267 | + this.llmClass = ChatOpenAI; |
| 268 | + } |
| 269 | + |
| 270 | + getConfig(options: LLMConfig): Partial<OpenAIChatInput> { |
| 271 | + return this.cleanConfig({ |
| 272 | + apiKey: options.api_key, |
| 273 | + openAIApiKey: options.api_key, |
| 274 | + modelKwargs: getMiniMaxModelKwargs( |
| 275 | + options.model, |
| 276 | + options.thinkingMode, |
| 277 | + options.serviceTier, |
| 278 | + options.modelKwargs |
| 279 | + ), |
| 280 | + modelName: options.model, |
| 281 | + maxTokens: +options.max_tokens, |
| 282 | + temperature: +options.temperature, |
| 283 | + frequencyPenalty: +options.frequency_penalty || 0, |
| 284 | + presencePenalty: +options.presence_penalty || 0, |
| 285 | + n: options.n || 1, |
| 286 | + stop: options.stop || undefined, |
| 287 | + streaming: options.stream || false, |
| 288 | + maxRetries: 3, |
| 289 | + headers: options.headers || undefined, |
| 290 | + } as Partial<OpenAIChatInput>); |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +export class LangchainMiniMaxAnthropicProvider |
| 295 | + extends LangchainMiniMaxBaseProvider |
| 296 | + implements LLMProviderInterface |
| 297 | +{ |
| 298 | + static provider = "Langchain" as const; |
| 299 | + static id = "MiniMax Anthropic (Langchain)" as const; |
| 300 | + static slug = "minimaxAnthropic" as const; |
| 301 | + static displayName = "MiniMax Anthropic"; |
| 302 | + |
| 303 | + protocol = "anthropic" as const; |
| 304 | + streamable = true; |
| 305 | + corsBypass = true; |
| 306 | + id = LangchainMiniMaxAnthropicProvider.id; |
| 307 | + provider = LangchainMiniMaxAnthropicProvider.provider; |
| 308 | + originalId = LangchainMiniMaxAnthropicProvider.id; |
| 309 | + default_values = anthropicDefaultValues; |
| 310 | + |
| 311 | + async load() { |
| 312 | + const { ChatAnthropic } = await import("@langchain/anthropic"); |
| 313 | + this.llmClass = class MiniMaxChatAnthropic extends ChatAnthropic { |
| 314 | + constructor(fields: any) { |
| 315 | + const { configuration = {}, ...rest } = fields; |
| 316 | + const fetchImpl = configuration.fetch || globalThis.fetch; |
| 317 | + super({ |
| 318 | + ...rest, |
| 319 | + clientOptions: { |
| 320 | + ...rest.clientOptions, |
| 321 | + ...configuration, |
| 322 | + fetch: wrapMiniMaxAnthropicFetch(fetchImpl), |
| 323 | + }, |
| 324 | + }); |
| 325 | + } |
| 326 | + }; |
| 327 | + } |
| 328 | + |
| 329 | + getConfig( |
| 330 | + options: LLMConfig |
| 331 | + ): Partial<AnthropicInput & BaseLanguageModelParams> { |
| 332 | + const modelKwargs = getMiniMaxModelKwargs( |
| 333 | + options.model, |
| 334 | + options.thinkingMode, |
| 335 | + options.serviceTier, |
| 336 | + options.modelKwargs |
| 337 | + ); |
| 338 | + const { thinking, service_tier, ...invocationKwargs } = modelKwargs as any; |
| 339 | + |
| 340 | + return this.cleanConfig({ |
| 341 | + anthropicApiKey: options.api_key, |
| 342 | + anthropicApiUrl: options.basePath, |
| 343 | + modelName: options.model, |
| 344 | + maxTokens: options.max_tokens, |
| 345 | + temperature: |
| 346 | + thinking?.type === "adaptive" ? undefined : options.temperature, |
| 347 | + stopSequences: options.stop, |
| 348 | + streaming: options.stream, |
| 349 | + maxRetries: 3, |
| 350 | + thinking, |
| 351 | + invocationKwargs: { |
| 352 | + ...invocationKwargs, |
| 353 | + ...(service_tier ? { service_tier } : {}), |
| 354 | + }, |
| 355 | + } as any); |
| 356 | + } |
| 357 | +} |
0 commit comments