Просмотр исходного кода

feat: add moonshot provider (#6046)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: CellenLee <[email protected]>
Matt Rubens 5 месяцев назад
Родитель
Сommit
df6c57d293
32 измененных файлов с 522 добавлено и 0 удалено
  1. 1 0
      packages/types/src/global-settings.ts
  2. 10 0
      packages/types/src/provider-settings.ts
  3. 1 0
      packages/types/src/providers/index.ts
  4. 22 0
      packages/types/src/providers/moonshot.ts
  5. 4 0
      src/api/index.ts
  6. 297 0
      src/api/providers/__tests__/moonshot.spec.ts
  7. 1 0
      src/api/providers/index.ts
  8. 39 0
      src/api/providers/moonshot.ts
  9. 1 0
      src/shared/__tests__/checkExistApiConfig.spec.ts
  10. 7 0
      webview-ui/src/components/settings/ApiOptions.tsx
  11. 3 0
      webview-ui/src/components/settings/constants.ts
  12. 73 0
      webview-ui/src/components/settings/providers/Moonshot.tsx
  13. 1 0
      webview-ui/src/components/settings/providers/index.ts
  14. 8 0
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  15. 3 0
      webview-ui/src/i18n/locales/ca/settings.json
  16. 3 0
      webview-ui/src/i18n/locales/de/settings.json
  17. 3 0
      webview-ui/src/i18n/locales/en/settings.json
  18. 3 0
      webview-ui/src/i18n/locales/es/settings.json
  19. 3 0
      webview-ui/src/i18n/locales/fr/settings.json
  20. 3 0
      webview-ui/src/i18n/locales/hi/settings.json
  21. 3 0
      webview-ui/src/i18n/locales/id/settings.json
  22. 3 0
      webview-ui/src/i18n/locales/it/settings.json
  23. 3 0
      webview-ui/src/i18n/locales/ja/settings.json
  24. 3 0
      webview-ui/src/i18n/locales/ko/settings.json
  25. 3 0
      webview-ui/src/i18n/locales/nl/settings.json
  26. 3 0
      webview-ui/src/i18n/locales/pl/settings.json
  27. 3 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  28. 3 0
      webview-ui/src/i18n/locales/ru/settings.json
  29. 3 0
      webview-ui/src/i18n/locales/tr/settings.json
  30. 3 0
      webview-ui/src/i18n/locales/vi/settings.json
  31. 3 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  32. 3 0
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 1 - 0
packages/types/src/global-settings.ts

@@ -159,6 +159,7 @@ export const SECRET_STATE_KEYS = [
 	"geminiApiKey",
 	"openAiNativeApiKey",
 	"deepSeekApiKey",
+	"moonshotApiKey",
 	"mistralApiKey",
 	"unboundApiKey",
 	"requestyApiKey",

+ 10 - 0
packages/types/src/provider-settings.ts

@@ -22,6 +22,7 @@ export const providerNames = [
 	"gemini-cli",
 	"openai-native",
 	"mistral",
+	"moonshot",
 	"deepseek",
 	"unbound",
 	"requesty",
@@ -187,6 +188,13 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({
 	deepSeekApiKey: z.string().optional(),
 })
 
+const moonshotSchema = apiModelIdProviderModelSchema.extend({
+	moonshotBaseUrl: z
+		.union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")])
+		.optional(),
+	moonshotApiKey: z.string().optional(),
+})
+
 const unboundSchema = baseProviderSettingsSchema.extend({
 	unboundApiKey: z.string().optional(),
 	unboundModelId: z.string().optional(),
@@ -241,6 +249,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })),
 	mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })),
 	deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })),
+	moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })),
 	unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })),
 	requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })),
 	humanRelaySchema.merge(z.object({ apiProvider: z.literal("human-relay") })),
@@ -269,6 +278,7 @@ export const providerSettingsSchema = z.object({
 	...openAiNativeSchema.shape,
 	...mistralSchema.shape,
 	...deepSeekSchema.shape,
+	...moonshotSchema.shape,
 	...unboundSchema.shape,
 	...requestySchema.shape,
 	...humanRelaySchema.shape,

+ 1 - 0
packages/types/src/providers/index.ts

@@ -9,6 +9,7 @@ export * from "./groq.js"
 export * from "./lite-llm.js"
 export * from "./lm-studio.js"
 export * from "./mistral.js"
+export * from "./moonshot.js"
 export * from "./ollama.js"
 export * from "./openai.js"
 export * from "./openrouter.js"

+ 22 - 0
packages/types/src/providers/moonshot.ts

@@ -0,0 +1,22 @@
+import type { ModelInfo } from "../model.js"
+
+// https://platform.moonshot.ai/
+export type MoonshotModelId = keyof typeof moonshotModels
+
+export const moonshotDefaultModelId: MoonshotModelId = "kimi-k2-0711-preview"
+
+export const moonshotModels = {
+	"kimi-k2-0711-preview": {
+		maxTokens: 32_000,
+		contextWindow: 131_072,
+		supportsImages: false,
+		supportsPromptCache: true,
+		inputPrice: 0.6, // $0.60 per million tokens (cache miss)
+		outputPrice: 2.5, // $2.50 per million tokens
+		cacheWritesPrice: 0, // $0 per million tokens (cache miss)
+		cacheReadsPrice: 0.15, // $0.15 per million tokens (cache hit)
+		description: `Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters.`,
+	},
+} as const satisfies Record<string, ModelInfo>
+
+export const MOONSHOT_DEFAULT_TEMPERATURE = 0.6

+ 4 - 0
src/api/index.ts

@@ -17,6 +17,7 @@ import {
 	GeminiHandler,
 	OpenAiNativeHandler,
 	DeepSeekHandler,
+	MoonshotHandler,
 	MistralHandler,
 	VsCodeLmHandler,
 	UnboundHandler,
@@ -89,6 +90,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new OpenAiNativeHandler(options)
 		case "deepseek":
 			return new DeepSeekHandler(options)
+		case "moonshot":
+			return new MoonshotHandler(options)
 		case "vscode-lm":
 			return new VsCodeLmHandler(options)
 		case "mistral":
@@ -110,6 +113,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 		case "litellm":
 			return new LiteLLMHandler(options)
 		default:
+			apiProvider satisfies "gemini-cli" | undefined
 			return new AnthropicHandler(options)
 	}
 }

+ 297 - 0
src/api/providers/__tests__/moonshot.spec.ts

@@ -0,0 +1,297 @@
+// Mocks must come first, before imports
+const mockCreate = vi.fn()
+vi.mock("openai", () => {
+	return {
+		__esModule: true,
+		default: vi.fn().mockImplementation(() => ({
+			chat: {
+				completions: {
+					create: mockCreate.mockImplementation(async (options) => {
+						if (!options.stream) {
+							return {
+								id: "test-completion",
+								choices: [
+									{
+										message: { role: "assistant", content: "Test response", refusal: null },
+										finish_reason: "stop",
+										index: 0,
+									},
+								],
+								usage: {
+									prompt_tokens: 10,
+									completion_tokens: 5,
+									total_tokens: 15,
+									cached_tokens: 2,
+								},
+							}
+						}
+
+						// Return async iterator for streaming
+						return {
+							[Symbol.asyncIterator]: async function* () {
+								yield {
+									choices: [
+										{
+											delta: { content: "Test response" },
+											index: 0,
+										},
+									],
+									usage: null,
+								}
+								yield {
+									choices: [
+										{
+											delta: {},
+											index: 0,
+										},
+									],
+									usage: {
+										prompt_tokens: 10,
+										completion_tokens: 5,
+										total_tokens: 15,
+										cached_tokens: 2,
+									},
+								}
+							},
+						}
+					}),
+				},
+			},
+		})),
+	}
+})
+
+import OpenAI from "openai"
+import type { Anthropic } from "@anthropic-ai/sdk"
+
+import { moonshotDefaultModelId } from "@roo-code/types"
+
+import type { ApiHandlerOptions } from "../../../shared/api"
+
+import { MoonshotHandler } from "../moonshot"
+
+describe("MoonshotHandler", () => {
+	let handler: MoonshotHandler
+	let mockOptions: ApiHandlerOptions
+
+	beforeEach(() => {
+		mockOptions = {
+			moonshotApiKey: "test-api-key",
+			apiModelId: "moonshot-chat",
+			moonshotBaseUrl: "https://api.moonshot.ai/v1",
+		}
+		handler = new MoonshotHandler(mockOptions)
+		vi.clearAllMocks()
+	})
+
+	describe("constructor", () => {
+		it("should initialize with provided options", () => {
+			expect(handler).toBeInstanceOf(MoonshotHandler)
+			expect(handler.getModel().id).toBe(mockOptions.apiModelId)
+		})
+
+		it.skip("should throw error if API key is missing", () => {
+			expect(() => {
+				new MoonshotHandler({
+					...mockOptions,
+					moonshotApiKey: undefined,
+				})
+			}).toThrow("Moonshot API key is required")
+		})
+
+		it("should use default model ID if not provided", () => {
+			const handlerWithoutModel = new MoonshotHandler({
+				...mockOptions,
+				apiModelId: undefined,
+			})
+			expect(handlerWithoutModel.getModel().id).toBe(moonshotDefaultModelId)
+		})
+
+		it("should use default base URL if not provided", () => {
+			const handlerWithoutBaseUrl = new MoonshotHandler({
+				...mockOptions,
+				moonshotBaseUrl: undefined,
+			})
+			expect(handlerWithoutBaseUrl).toBeInstanceOf(MoonshotHandler)
+			// The base URL is passed to OpenAI client internally
+			expect(OpenAI).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: "https://api.moonshot.ai/v1",
+				}),
+			)
+		})
+
+		it("should use chinese base URL if provided", () => {
+			const customBaseUrl = "https://api.moonshot.cn/v1"
+			const handlerWithCustomUrl = new MoonshotHandler({
+				...mockOptions,
+				moonshotBaseUrl: customBaseUrl,
+			})
+			expect(handlerWithCustomUrl).toBeInstanceOf(MoonshotHandler)
+			// The custom base URL is passed to OpenAI client
+			expect(OpenAI).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: customBaseUrl,
+				}),
+			)
+		})
+
+		it("should set includeMaxTokens to true", () => {
+			// Create a new handler and verify OpenAI client was called with includeMaxTokens
+			const _handler = new MoonshotHandler(mockOptions)
+			expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: mockOptions.moonshotApiKey }))
+		})
+	})
+
+	describe("getModel", () => {
+		it("should return model info for valid model ID", () => {
+			const model = handler.getModel()
+			expect(model.id).toBe(mockOptions.apiModelId)
+			expect(model.info).toBeDefined()
+			expect(model.info.maxTokens).toBe(32_000)
+			expect(model.info.contextWindow).toBe(131_072)
+			expect(model.info.supportsImages).toBe(false)
+			expect(model.info.supportsPromptCache).toBe(true) // Should be true now
+		})
+
+		it("should return provided model ID with default model info if model does not exist", () => {
+			const handlerWithInvalidModel = new MoonshotHandler({
+				...mockOptions,
+				apiModelId: "invalid-model",
+			})
+			const model = handlerWithInvalidModel.getModel()
+			expect(model.id).toBe("invalid-model") // Returns provided ID
+			expect(model.info).toBeDefined()
+			// With the current implementation, it's the same object reference when using default model info
+			expect(model.info).toBe(handler.getModel().info)
+			// Should have the same base properties
+			expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow)
+			// And should have supportsPromptCache set to true
+			expect(model.info.supportsPromptCache).toBe(true)
+		})
+
+		it("should return default model if no model ID is provided", () => {
+			const handlerWithoutModel = new MoonshotHandler({
+				...mockOptions,
+				apiModelId: undefined,
+			})
+			const model = handlerWithoutModel.getModel()
+			expect(model.id).toBe(moonshotDefaultModelId)
+			expect(model.info).toBeDefined()
+			expect(model.info.supportsPromptCache).toBe(true)
+		})
+
+		it("should include model parameters from getModelParams", () => {
+			const model = handler.getModel()
+			expect(model).toHaveProperty("temperature")
+			expect(model).toHaveProperty("maxTokens")
+		})
+	})
+
+	describe("createMessage", () => {
+		const systemPrompt = "You are a helpful assistant."
+		const messages: Anthropic.Messages.MessageParam[] = [
+			{
+				role: "user",
+				content: [
+					{
+						type: "text" as const,
+						text: "Hello!",
+					},
+				],
+			},
+		]
+
+		it("should handle streaming responses", async () => {
+			const stream = handler.createMessage(systemPrompt, messages)
+			const chunks: any[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			expect(chunks.length).toBeGreaterThan(0)
+			const textChunks = chunks.filter((chunk) => chunk.type === "text")
+			expect(textChunks).toHaveLength(1)
+			expect(textChunks[0].text).toBe("Test response")
+		})
+
+		it("should include usage information", async () => {
+			const stream = handler.createMessage(systemPrompt, messages)
+			const chunks: any[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+			expect(usageChunks.length).toBeGreaterThan(0)
+			expect(usageChunks[0].inputTokens).toBe(10)
+			expect(usageChunks[0].outputTokens).toBe(5)
+		})
+
+		it("should include cache metrics in usage information", async () => {
+			const stream = handler.createMessage(systemPrompt, messages)
+			const chunks: any[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+			expect(usageChunks.length).toBeGreaterThan(0)
+			expect(usageChunks[0].cacheWriteTokens).toBe(0)
+			expect(usageChunks[0].cacheReadTokens).toBe(2)
+		})
+	})
+
+	describe("processUsageMetrics", () => {
+		it("should correctly process usage metrics including cache information", () => {
+			// We need to access the protected method, so we'll create a test subclass
+			class TestMoonshotHandler extends MoonshotHandler {
+				public testProcessUsageMetrics(usage: any) {
+					return this.processUsageMetrics(usage)
+				}
+			}
+
+			const testHandler = new TestMoonshotHandler(mockOptions)
+
+			const usage = {
+				prompt_tokens: 100,
+				completion_tokens: 50,
+				total_tokens: 150,
+				cached_tokens: 20,
+			}
+
+			const result = testHandler.testProcessUsageMetrics(usage)
+
+			expect(result.type).toBe("usage")
+			expect(result.inputTokens).toBe(100)
+			expect(result.outputTokens).toBe(50)
+			expect(result.cacheWriteTokens).toBe(0)
+			expect(result.cacheReadTokens).toBe(20)
+		})
+
+		it("should handle missing cache metrics gracefully", () => {
+			class TestMoonshotHandler extends MoonshotHandler {
+				public testProcessUsageMetrics(usage: any) {
+					return this.processUsageMetrics(usage)
+				}
+			}
+
+			const testHandler = new TestMoonshotHandler(mockOptions)
+
+			const usage = {
+				prompt_tokens: 100,
+				completion_tokens: 50,
+				total_tokens: 150,
+				// No cached_tokens
+			}
+
+			const result = testHandler.testProcessUsageMetrics(usage)
+
+			expect(result.type).toBe("usage")
+			expect(result.inputTokens).toBe(100)
+			expect(result.outputTokens).toBe(50)
+			expect(result.cacheWriteTokens).toBe(0)
+			expect(result.cacheReadTokens).toBeUndefined()
+		})
+	})
+})

+ 1 - 0
src/api/providers/index.ts

@@ -4,6 +4,7 @@ export { AwsBedrockHandler } from "./bedrock"
 export { ChutesHandler } from "./chutes"
 export { ClaudeCodeHandler } from "./claude-code"
 export { DeepSeekHandler } from "./deepseek"
+export { MoonshotHandler } from "./moonshot"
 export { FakeAIHandler } from "./fake-ai"
 export { GeminiHandler } from "./gemini"
 export { GlamaHandler } from "./glama"

+ 39 - 0
src/api/providers/moonshot.ts

@@ -0,0 +1,39 @@
+import { moonshotModels, moonshotDefaultModelId } from "@roo-code/types"
+
+import type { ApiHandlerOptions } from "../../shared/api"
+
+import type { ApiStreamUsageChunk } from "../transform/stream"
+import { getModelParams } from "../transform/model-params"
+
+import { OpenAiHandler } from "./openai"
+
+export class MoonshotHandler extends OpenAiHandler {
+	constructor(options: ApiHandlerOptions) {
+		super({
+			...options,
+			openAiApiKey: options.moonshotApiKey ?? "not-provided",
+			openAiModelId: options.apiModelId ?? moonshotDefaultModelId,
+			openAiBaseUrl: options.moonshotBaseUrl ?? "https://api.moonshot.ai/v1",
+			openAiStreamingEnabled: true,
+			includeMaxTokens: true,
+		})
+	}
+
+	override getModel() {
+		const id = this.options.apiModelId ?? moonshotDefaultModelId
+		const info = moonshotModels[id as keyof typeof moonshotModels] || moonshotModels[moonshotDefaultModelId]
+		const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options })
+		return { id, info, ...params }
+	}
+
+	// Override to handle Moonshot's usage metrics, including caching.
+	protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
+		return {
+			type: "usage",
+			inputTokens: usage?.prompt_tokens || 0,
+			outputTokens: usage?.completion_tokens || 0,
+			cacheWriteTokens: 0,
+			cacheReadTokens: usage?.cached_tokens,
+		}
+	}
+}

+ 1 - 0
src/shared/__tests__/checkExistApiConfig.spec.ts

@@ -53,6 +53,7 @@ describe("checkExistKey", () => {
 			geminiApiKey: undefined,
 			openAiNativeApiKey: undefined,
 			deepSeekApiKey: undefined,
+			moonshotApiKey: undefined,
 			mistralApiKey: undefined,
 			vsCodeLmModelSelector: undefined,
 			requestyApiKey: undefined,

+ 7 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -18,6 +18,7 @@ import {
 	claudeCodeDefaultModelId,
 	geminiDefaultModelId,
 	deepSeekDefaultModelId,
+	moonshotDefaultModelId,
 	mistralDefaultModelId,
 	xaiDefaultModelId,
 	groqDefaultModelId,
@@ -61,6 +62,7 @@ import {
 	LMStudio,
 	LiteLLM,
 	Mistral,
+	Moonshot,
 	Ollama,
 	OpenAI,
 	OpenAICompatible,
@@ -287,6 +289,7 @@ const ApiOptions = ({
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
 				gemini: { field: "apiModelId", default: geminiDefaultModelId },
 				deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
+				moonshot: { field: "apiModelId", default: moonshotDefaultModelId },
 				mistral: { field: "apiModelId", default: mistralDefaultModelId },
 				xai: { field: "apiModelId", default: xaiDefaultModelId },
 				groq: { field: "apiModelId", default: groqDefaultModelId },
@@ -464,6 +467,10 @@ const ApiOptions = ({
 				<DeepSeek apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 
+			{selectedProvider === "moonshot" && (
+				<Moonshot apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+			)}
+
 			{selectedProvider === "vscode-lm" && (
 				<VSCodeLM apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}

+ 3 - 0
webview-ui/src/components/settings/constants.ts

@@ -5,6 +5,7 @@ import {
 	bedrockModels,
 	claudeCodeModels,
 	deepSeekModels,
+	moonshotModels,
 	geminiModels,
 	mistralModels,
 	openAiNativeModels,
@@ -19,6 +20,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
 	"claude-code": claudeCodeModels,
 	bedrock: bedrockModels,
 	deepseek: deepSeekModels,
+	moonshot: moonshotModels,
 	gemini: geminiModels,
 	mistral: mistralModels,
 	"openai-native": openAiNativeModels,
@@ -34,6 +36,7 @@ export const PROVIDERS = [
 	{ value: "claude-code", label: "Claude Code" },
 	{ value: "gemini", label: "Google Gemini" },
 	{ value: "deepseek", label: "DeepSeek" },
+	{ value: "moonshot", label: "Moonshot" },
 	{ value: "openai-native", label: "OpenAI" },
 	{ value: "openai", label: "OpenAI Compatible" },
 	{ value: "vertex", label: "GCP Vertex AI" },

+ 73 - 0
webview-ui/src/components/settings/providers/Moonshot.tsx

@@ -0,0 +1,73 @@
+import { useCallback } from "react"
+import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
+
+import type { ProviderSettings } from "@roo-code/types"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
+
+import { inputEventTransform } from "../transforms"
+import { cn } from "@/lib/utils"
+
+type MoonshotProps = {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+}
+
+export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: MoonshotProps) => {
+	const { t } = useAppTranslation()
+
+	const handleInputChange = useCallback(
+		<K extends keyof ProviderSettings, E>(
+			field: K,
+			transform: (event: E) => ProviderSettings[K] = inputEventTransform,
+		) =>
+			(event: E | Event) => {
+				setApiConfigurationField(field, transform(event as E))
+			},
+		[setApiConfigurationField],
+	)
+
+	return (
+		<>
+			<div>
+				<label className="block font-medium mb-1">{t("settings:providers.moonshotBaseUrl")}</label>
+				<VSCodeDropdown
+					value={apiConfiguration.moonshotBaseUrl}
+					onChange={handleInputChange("moonshotBaseUrl")}
+					className={cn("w-full")}>
+					<VSCodeOption value="https://api.moonshot.ai/v1" className="p-2">
+						api.moonshot.ai
+					</VSCodeOption>
+					<VSCodeOption value="https://api.moonshot.cn/v1" className="p-2">
+						api.moonshot.cn
+					</VSCodeOption>
+				</VSCodeDropdown>
+			</div>
+			<div>
+				<VSCodeTextField
+					value={apiConfiguration?.moonshotApiKey || ""}
+					type="password"
+					onInput={handleInputChange("moonshotApiKey")}
+					placeholder={t("settings:placeholders.apiKey")}
+					className="w-full">
+					<label className="block font-medium mb-1">{t("settings:providers.moonshotApiKey")}</label>
+				</VSCodeTextField>
+				<div className="text-sm text-vscode-descriptionForeground">
+					{t("settings:providers.apiKeyStorageNotice")}
+				</div>
+				{!apiConfiguration?.moonshotApiKey && (
+					<VSCodeButtonLink
+						href={
+							apiConfiguration.moonshotBaseUrl === "https://api.moonshot.cn/v1"
+								? "https://platform.moonshot.cn/console/api-keys"
+								: "https://platform.moonshot.ai/console/api-keys"
+						}
+						appearance="secondary">
+						{t("settings:providers.getMoonshotApiKey")}
+					</VSCodeButtonLink>
+				)}
+			</div>
+		</>
+	)
+}

+ 1 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -8,6 +8,7 @@ export { Glama } from "./Glama"
 export { Groq } from "./Groq"
 export { LMStudio } from "./LMStudio"
 export { Mistral } from "./Mistral"
+export { Moonshot } from "./Moonshot"
 export { Ollama } from "./Ollama"
 export { OpenAI } from "./OpenAI"
 export { OpenAICompatible } from "./OpenAICompatible"

+ 8 - 0
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -8,6 +8,8 @@ import {
 	bedrockModels,
 	deepSeekDefaultModelId,
 	deepSeekModels,
+	moonshotDefaultModelId,
+	moonshotModels,
 	geminiDefaultModelId,
 	geminiModels,
 	mistralDefaultModelId,
@@ -162,6 +164,11 @@ function getSelectedModel({
 			const info = deepSeekModels[id as keyof typeof deepSeekModels]
 			return { id, info }
 		}
+		case "moonshot": {
+			const id = apiConfiguration.apiModelId ?? moonshotDefaultModelId
+			const info = moonshotModels[id as keyof typeof moonshotModels]
+			return { id, info }
+		}
 		case "openai-native": {
 			const id = apiConfiguration.apiModelId ?? openAiNativeDefaultModelId
 			const info = openAiNativeModels[id as keyof typeof openAiNativeModels]
@@ -211,6 +218,7 @@ function getSelectedModel({
 		// case "human-relay":
 		// case "fake-ai":
 		default: {
+			provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai"
 			const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
 			const info = anthropicModels[id as keyof typeof anthropicModels]
 			return { id, info }

+ 3 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Obtenir clau API de Chutes",
 		"deepSeekApiKey": "Clau API de DeepSeek",
 		"getDeepSeekApiKey": "Obtenir clau API de DeepSeek",
+		"moonshotApiKey": "Clau API de Moonshot",
+		"getMoonshotApiKey": "Obtenir clau API de Moonshot",
+		"moonshotBaseUrl": "Punt d'entrada de Moonshot",
 		"geminiApiKey": "Clau API de Gemini",
 		"getGroqApiKey": "Obtenir clau API de Groq",
 		"groqApiKey": "Clau API de Groq",

+ 3 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes API-Schlüssel erhalten",
 		"deepSeekApiKey": "DeepSeek API-Schlüssel",
 		"getDeepSeekApiKey": "DeepSeek API-Schlüssel erhalten",
+		"moonshotApiKey": "Moonshot API-Schlüssel",
+		"getMoonshotApiKey": "Moonshot API-Schlüssel erhalten",
+		"moonshotBaseUrl": "Moonshot-Einstiegspunkt",
 		"geminiApiKey": "Gemini API-Schlüssel",
 		"getGroqApiKey": "Groq API-Schlüssel erhalten",
 		"groqApiKey": "Groq API-Schlüssel",

+ 3 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Get Chutes API Key",
 		"deepSeekApiKey": "DeepSeek API Key",
 		"getDeepSeekApiKey": "Get DeepSeek API Key",
+		"moonshotApiKey": "Moonshot API Key",
+		"getMoonshotApiKey": "Get Moonshot API Key",
+		"moonshotBaseUrl": "Moonshot Entrypoint",
 		"geminiApiKey": "Gemini API Key",
 		"getGroqApiKey": "Get Groq API Key",
 		"groqApiKey": "Groq API Key",

+ 3 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Obtener clave API de Chutes",
 		"deepSeekApiKey": "Clave API de DeepSeek",
 		"getDeepSeekApiKey": "Obtener clave API de DeepSeek",
+		"moonshotApiKey": "Clave API de Moonshot",
+		"getMoonshotApiKey": "Obtener clave API de Moonshot",
+		"moonshotBaseUrl": "Punto de entrada de Moonshot",
 		"geminiApiKey": "Clave API de Gemini",
 		"getGroqApiKey": "Obtener clave API de Groq",
 		"groqApiKey": "Clave API de Groq",

+ 3 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Obtenir la clé API Chutes",
 		"deepSeekApiKey": "Clé API DeepSeek",
 		"getDeepSeekApiKey": "Obtenir la clé API DeepSeek",
+		"moonshotApiKey": "Clé API Moonshot",
+		"getMoonshotApiKey": "Obtenir la clé API Moonshot",
+		"moonshotBaseUrl": "Point d'entrée Moonshot",
 		"geminiApiKey": "Clé API Gemini",
 		"getGroqApiKey": "Obtenir la clé API Groq",
 		"groqApiKey": "Clé API Groq",

+ 3 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes API कुंजी प्राप्त करें",
 		"deepSeekApiKey": "DeepSeek API कुंजी",
 		"getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें",
+		"moonshotApiKey": "Moonshot API कुंजी",
+		"getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें",
+		"moonshotBaseUrl": "Moonshot प्रवेश बिंदु",
 		"geminiApiKey": "Gemini API कुंजी",
 		"getGroqApiKey": "Groq API कुंजी प्राप्त करें",
 		"groqApiKey": "Groq API कुंजी",

+ 3 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -257,6 +257,9 @@
 		"getChutesApiKey": "Dapatkan Chutes API Key",
 		"deepSeekApiKey": "DeepSeek API Key",
 		"getDeepSeekApiKey": "Dapatkan DeepSeek API Key",
+		"moonshotApiKey": "Kunci API Moonshot",
+		"getMoonshotApiKey": "Dapatkan Kunci API Moonshot",
+		"moonshotBaseUrl": "Titik Masuk Moonshot",
 		"geminiApiKey": "Gemini API Key",
 		"getGroqApiKey": "Dapatkan Groq API Key",
 		"groqApiKey": "Groq API Key",

+ 3 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Ottieni chiave API Chutes",
 		"deepSeekApiKey": "Chiave API DeepSeek",
 		"getDeepSeekApiKey": "Ottieni chiave API DeepSeek",
+		"moonshotApiKey": "Chiave API Moonshot",
+		"getMoonshotApiKey": "Ottieni chiave API Moonshot",
+		"moonshotBaseUrl": "Punto di ingresso Moonshot",
 		"geminiApiKey": "Chiave API Gemini",
 		"getGroqApiKey": "Ottieni chiave API Groq",
 		"groqApiKey": "Chiave API Groq",

+ 3 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes APIキーを取得",
 		"deepSeekApiKey": "DeepSeek APIキー",
 		"getDeepSeekApiKey": "DeepSeek APIキーを取得",
+		"moonshotApiKey": "Moonshot APIキー",
+		"getMoonshotApiKey": "Moonshot APIキーを取得",
+		"moonshotBaseUrl": "Moonshot エントリーポイント",
 		"geminiApiKey": "Gemini APIキー",
 		"getGroqApiKey": "Groq APIキーを取得",
 		"groqApiKey": "Groq APIキー",

+ 3 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes API 키 받기",
 		"deepSeekApiKey": "DeepSeek API 키",
 		"getDeepSeekApiKey": "DeepSeek API 키 받기",
+		"moonshotApiKey": "Moonshot API 키",
+		"getMoonshotApiKey": "Moonshot API 키 받기",
+		"moonshotBaseUrl": "Moonshot 엔트리포인트",
 		"geminiApiKey": "Gemini API 키",
 		"getGroqApiKey": "Groq API 키 받기",
 		"groqApiKey": "Groq API 키",

+ 3 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes API-sleutel ophalen",
 		"deepSeekApiKey": "DeepSeek API-sleutel",
 		"getDeepSeekApiKey": "DeepSeek API-sleutel ophalen",
+		"moonshotApiKey": "Moonshot API-sleutel",
+		"getMoonshotApiKey": "Moonshot API-sleutel ophalen",
+		"moonshotBaseUrl": "Moonshot-ingangspunt",
 		"geminiApiKey": "Gemini API-sleutel",
 		"getGroqApiKey": "Groq API-sleutel ophalen",
 		"groqApiKey": "Groq API-sleutel",

+ 3 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Uzyskaj klucz API Chutes",
 		"deepSeekApiKey": "Klucz API DeepSeek",
 		"getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek",
+		"moonshotApiKey": "Klucz API Moonshot",
+		"getMoonshotApiKey": "Uzyskaj klucz API Moonshot",
+		"moonshotBaseUrl": "Punkt wejścia Moonshot",
 		"geminiApiKey": "Klucz API Gemini",
 		"getGroqApiKey": "Uzyskaj klucz API Groq",
 		"groqApiKey": "Klucz API Groq",

+ 3 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Obter chave de API Chutes",
 		"deepSeekApiKey": "Chave de API DeepSeek",
 		"getDeepSeekApiKey": "Obter chave de API DeepSeek",
+		"moonshotApiKey": "Chave de API Moonshot",
+		"getMoonshotApiKey": "Obter chave de API Moonshot",
+		"moonshotBaseUrl": "Ponto de entrada Moonshot",
 		"geminiApiKey": "Chave de API Gemini",
 		"getGroqApiKey": "Obter chave de API Groq",
 		"groqApiKey": "Chave de API Groq",

+ 3 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Получить Chutes API-ключ",
 		"deepSeekApiKey": "DeepSeek API-ключ",
 		"getDeepSeekApiKey": "Получить DeepSeek API-ключ",
+		"moonshotApiKey": "Moonshot API-ключ",
+		"getMoonshotApiKey": "Получить Moonshot API-ключ",
+		"moonshotBaseUrl": "Точка входа Moonshot",
 		"geminiApiKey": "Gemini API-ключ",
 		"getGroqApiKey": "Получить Groq API-ключ",
 		"groqApiKey": "Groq API-ключ",

+ 3 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Chutes API Anahtarı Al",
 		"deepSeekApiKey": "DeepSeek API Anahtarı",
 		"getDeepSeekApiKey": "DeepSeek API Anahtarı Al",
+		"moonshotApiKey": "Moonshot API Anahtarı",
+		"getMoonshotApiKey": "Moonshot API Anahtarı Al",
+		"moonshotBaseUrl": "Moonshot Giriş Noktası",
 		"geminiApiKey": "Gemini API Anahtarı",
 		"getGroqApiKey": "Groq API Anahtarı Al",
 		"groqApiKey": "Groq API Anahtarı",

+ 3 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "Lấy khóa API Chutes",
 		"deepSeekApiKey": "Khóa API DeepSeek",
 		"getDeepSeekApiKey": "Lấy khóa API DeepSeek",
+		"moonshotApiKey": "Khóa API Moonshot",
+		"getMoonshotApiKey": "Lấy khóa API Moonshot",
+		"moonshotBaseUrl": "Điểm vào Moonshot",
 		"geminiApiKey": "Khóa API Gemini",
 		"getGroqApiKey": "Lấy khóa API Groq",
 		"groqApiKey": "Khóa API Groq",

+ 3 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "获取 Chutes API 密钥",
 		"deepSeekApiKey": "DeepSeek API 密钥",
 		"getDeepSeekApiKey": "获取 DeepSeek API 密钥",
+		"moonshotApiKey": "Moonshot API 密钥",
+		"getMoonshotApiKey": "获取 Moonshot API 密钥",
+		"moonshotBaseUrl": "Moonshot 服务站点",
 		"geminiApiKey": "Gemini API 密钥",
 		"getGroqApiKey": "获取 Groq API 密钥",
 		"groqApiKey": "Groq API 密钥",

+ 3 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -253,6 +253,9 @@
 		"getChutesApiKey": "取得 Chutes API 金鑰",
 		"deepSeekApiKey": "DeepSeek API 金鑰",
 		"getDeepSeekApiKey": "取得 DeepSeek API 金鑰",
+		"moonshotApiKey": "Moonshot API 金鑰",
+		"getMoonshotApiKey": "取得 Moonshot API 金鑰",
+		"moonshotBaseUrl": "Moonshot 服務站點",
 		"geminiApiKey": "Gemini API 金鑰",
 		"getGroqApiKey": "取得 Groq API 金鑰",
 		"groqApiKey": "Groq API 金鑰",