diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/OpenAIChatModel.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/OpenAIChatModel.java
index 6f424148bd..56fce2b0ad 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/OpenAIChatModel.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/OpenAIChatModel.java
@@ -62,6 +62,8 @@
*
* - {@link OpenAIChatFormatter} - Standard OpenAI GPT models
* - {@link io.agentscope.extensions.model.openai.compat.deepseek.DeepSeekFormatter} - DeepSeek Chat models
+ * - {@link io.agentscope.extensions.model.openai.formatter.GLMFormatter} - Zhipu GLM models
+ * - {@link io.agentscope.extensions.model.openai.compat.kimi.KimiFormatter} - Kimi (Moonshot AI) models
* - {@link io.agentscope.extensions.model.openai.compat.glm.GLMFormatter} - Zhipu GLM models
*
*/
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatter.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatter.java
index 68d9af53f7..c8e91eedd2 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatter.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatter.java
@@ -15,6 +15,275 @@
*/
package io.agentscope.extensions.model.openai.compat.kimi;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.ToolChoice;
+import io.agentscope.extensions.model.openai.dto.OpenAIRequest;
import io.agentscope.extensions.model.openai.formatter.OpenAIChatFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-public class KimiFormatter extends OpenAIChatFormatter {}
+/**
+ * Formatter for Kimi (Moonshot AI) models (kimi-k3, kimi-k2.7-code, kimi-k2.6, kimi-k2.5,
+ * moonshot-v1 series).
+ *
+ * Adapted to the latest Kimi open platform Chat Completions API
+ * ({@code https://api.moonshot.cn/v1/chat/completions}):
+ *
+ * - On {@code kimi-*} models, {@code temperature} / {@code top_p} /
+ * {@code frequency_penalty} / {@code presence_penalty} are fixed by the platform and the
+ * API rejects requests carrying other values; these parameters are stripped from the
+ * request. The {@code moonshot-v1} series still accepts them, so they are kept there.
+ * - {@code reasoning_effort} ({@code "low"} / {@code "high"} / {@code "max"}, default
+ * {@code "max"}) is only supported by {@code kimi-k3}; it is stripped for other models
+ * - {@code thinking_budget} is not a Kimi parameter and is always stripped; K2.x thinking is
+ * controlled through the {@code thinking} body parameter instead (see below)
+ * - Only {@code max_tokens} is documented; {@code max_completion_tokens} is mapped to
+ * {@code max_tokens} when the latter is not set. Reasoning tokens count towards
+ * {@code max_tokens}, so the official guide recommends {@code max_tokens >= 16000} for
+ * thinking models
+ * - {@code tool_choice} supports {@code auto} / {@code none} on all models;
+ * {@code required} is only supported by {@code kimi-k3} and is degraded to {@code auto} on
+ * the K2.x series; forcing a specific function is incompatible with thinking enabled
+ * (HTTP 400), so it is degraded to {@code auto} on always-thinking models
+ * ({@code kimi-k3}, {@code kimi-k2.7-code})
+ * - The {@code strict} parameter in tool definitions is not documented by Kimi and is not
+ * sent
+ * - Assistant {@code reasoning_content} is preserved in the request history (inherited from
+ * the base message converter), as required by Preserved Thinking on {@code kimi-k3} /
+ * {@code kimi-k2.7-code} and by {@code thinking.keep = "all"} on {@code kimi-k2.6}
+ *
+ *
+ * Thinking mode: {@code kimi-k2.6} / {@code kimi-k2.5} accept a {@code thinking} body
+ * parameter (e.g. {@code {"type": "disabled"}}), which can be passed through
+ * {@code GenerateOptions.additionalBodyParam("thinking", Map.of("type", "disabled"))}.
+ * {@code kimi-k3} uses the top-level {@code reasoning_effort} option
+ * ({@code GenerateOptions.reasoningEffort}) instead. JSON mode is enabled via
+ * {@code response_format = {"type": "json_object"}}; {@code json_schema} is not supported.
+ *
+ *
Usage:
+ *
{@code
+ * OpenAIChatModel.builder()
+ * .formatter(new KimiFormatter())
+ * .modelName("kimi-k3")
+ * .baseUrl("https://api.moonshot.cn/v1")
+ * .apiKey(apiKey)
+ * .build();
+ * }
+ *
+ * @see Kimi API overview
+ * @see Kimi model parameter
+ * reference
+ * @see Kimi thinking
+ * mode guide
+ * @see Kimi tool choice guide
+ */
+public class KimiFormatter extends OpenAIChatFormatter {
+
+ private static final Logger log = LoggerFactory.getLogger(KimiFormatter.class);
+
+ public KimiFormatter() {
+ super();
+ }
+
+ @Override
+ protected boolean supportsStrict() {
+ return false;
+ }
+
+ @Override
+ public void applyOptions(
+ OpenAIRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
+ super.applyOptions(request, options, defaultOptions);
+ sanitizeKimiRequest(request);
+ }
+
+ @Override
+ public void applyToolChoice(OpenAIRequest request, ToolChoice toolChoice) {
+ applyKimiToolChoice(request, toolChoice);
+ }
+
+ /**
+ * Whether the model belongs to the Kimi K series ({@code kimi-k3}, {@code kimi-k2.7-code},
+ * {@code kimi-k2.6}, ...) whose sampling parameters are fixed by the platform.
+ *
+ * @param model the model name from the request (may be null)
+ * @return true for {@code kimi-*} models
+ */
+ static boolean hasFixedSamplingParams(String model) {
+ return model != null && model.startsWith("kimi-");
+ }
+
+ /**
+ * Whether the model always runs with thinking enabled and cannot disable it
+ * ({@code kimi-k3} and the {@code kimi-k2.7-code} series).
+ *
+ * @param model the model name from the request (may be null)
+ * @return true for always-thinking Kimi models
+ */
+ static boolean isAlwaysThinkingModel(String model) {
+ return model != null && (model.startsWith("kimi-k3") || model.startsWith("kimi-k2.7-code"));
+ }
+
+ /**
+ * Whether the model supports the top-level {@code reasoning_effort} parameter.
+ * Per the official parameter reference, only {@code kimi-k3} supports it.
+ *
+ * @param model the model name from the request (may be null)
+ * @return true if {@code reasoning_effort} is supported
+ */
+ static boolean supportsReasoningEffort(String model) {
+ return model != null && model.startsWith("kimi-k3");
+ }
+
+ /**
+ * Whether the model supports {@code tool_choice = "required"}.
+ * Per the official parameter reference, {@code kimi-k3} supports it while the K2.x series
+ * ({@code kimi-k2.6}, {@code kimi-k2.7-code}, ...) rejects it.
+ *
+ * @param model the model name from the request (may be null)
+ * @return true if {@code required} is supported
+ */
+ static boolean supportsRequiredToolChoice(String model) {
+ return model == null || !model.startsWith("kimi-k2");
+ }
+
+ /**
+ * Sanitize the request according to the Kimi Chat Completions API reference.
+ *
+ * Applied adaptations:
+ *
+ * - {@code temperature} / {@code top_p} / {@code frequency_penalty} /
+ * {@code presence_penalty} are removed on {@code kimi-*} models (fixed by the
+ * platform; other values are rejected)
+ * - {@code reasoning_effort} is removed on models other than {@code kimi-k3}
+ * - {@code thinking_budget} is always removed (not a Kimi parameter)
+ * - {@code max_completion_tokens} is mapped to {@code max_tokens} when the latter is
+ * not set
+ *
+ *
+ * This method is static to allow sharing with {@link KimiMultiAgentFormatter}.
+ *
+ * @param request the request to sanitize
+ */
+ protected static void sanitizeKimiRequest(OpenAIRequest request) {
+ String model = request.getModel();
+
+ if (hasFixedSamplingParams(model)) {
+ if (request.getTemperature() != null) {
+ log.debug(
+ "Kimi model {} does not allow overriding temperature, removing it", model);
+ request.setTemperature(null);
+ }
+ if (request.getTopP() != null) {
+ log.debug("Kimi model {} does not allow overriding top_p, removing it", model);
+ request.setTopP(null);
+ }
+ if (request.getFrequencyPenalty() != null) {
+ log.debug(
+ "Kimi model {} does not allow overriding frequency_penalty, removing it",
+ model);
+ request.setFrequencyPenalty(null);
+ }
+ if (request.getPresencePenalty() != null) {
+ log.debug(
+ "Kimi model {} does not allow overriding presence_penalty, removing it",
+ model);
+ request.setPresencePenalty(null);
+ }
+ }
+
+ if (request.getReasoningEffort() != null && !supportsReasoningEffort(model)) {
+ log.debug(
+ "Kimi model {} does not support reasoning_effort (kimi-k3 only), removing it;"
+ + " use the 'thinking' body param on K2.x models instead",
+ model);
+ request.setReasoningEffort(null);
+ }
+
+ if (request.getThinkingBudget() != null) {
+ log.debug(
+ "Kimi does not support thinking_budget, removing it from the request; use the"
+ + " 'thinking' body param (K2.x) or reasoning_effort (kimi-k3)"
+ + " instead");
+ request.setThinkingBudget(null);
+ }
+
+ // Kimi only documents max_tokens; map OpenAI-style max_completion_tokens onto it
+ if (request.getMaxCompletionTokens() != null) {
+ if (request.getMaxTokens() == null) {
+ log.debug("Kimi only supports max_tokens, mapping max_completion_tokens to it");
+ request.setMaxTokens(request.getMaxCompletionTokens());
+ }
+ request.setMaxCompletionTokens(null);
+ }
+ }
+
+ /**
+ * Apply Kimi-specific tool choice handling.
+ *
+ *
Per the official tool choice guide:
+ *
+ * - {@code auto} / {@code none} are supported on all models
+ * - {@code required} is only supported by {@code kimi-k3}; it is degraded to
+ * {@code auto} on the K2.x series, which rejects it
+ * - Forcing a specific function ({@code {"type": "function", ...}}) is incompatible with
+ * thinking enabled (HTTP 400); it is degraded to {@code auto} on always-thinking
+ * models and passed through otherwise (on {@code kimi-k2.6} / {@code kimi-k2.5},
+ * disable thinking via the {@code thinking} body param to use it)
+ *
+ *
+ * This method is static to allow sharing with {@link KimiMultiAgentFormatter}.
+ *
+ * @param request the request to apply tool choice to
+ * @param toolChoice the requested tool choice
+ */
+ protected static void applyKimiToolChoice(OpenAIRequest request, ToolChoice toolChoice) {
+ if (request.getTools() == null || request.getTools().isEmpty()) {
+ return;
+ }
+
+ String model = request.getModel();
+
+ if (toolChoice == null || toolChoice instanceof ToolChoice.Auto) {
+ request.setToolChoice("auto");
+ } else if (toolChoice instanceof ToolChoice.None) {
+ request.setToolChoice("none");
+ } else if (toolChoice instanceof ToolChoice.Required) {
+ if (supportsRequiredToolChoice(model)) {
+ request.setToolChoice("required");
+ } else {
+ log.info(
+ "Kimi model {} does not support tool_choice='required' (kimi-k3 only),"
+ + " degrading to 'auto'",
+ model);
+ request.setToolChoice("auto");
+ }
+ } else if (toolChoice instanceof ToolChoice.Specific specific) {
+ if (isAlwaysThinkingModel(model)) {
+ // Specified tool_choice is incompatible with thinking enabled (HTTP 400), and
+ // these models cannot disable thinking
+ log.warn(
+ "Kimi model {} always runs with thinking enabled, which is incompatible"
+ + " with a specific tool_choice; degrading to 'auto'",
+ model);
+ request.setToolChoice("auto");
+ } else {
+ Map namedToolChoice = new HashMap<>();
+ namedToolChoice.put("type", "function");
+ Map function = new HashMap<>();
+ function.put("name", specific.toolName());
+ namedToolChoice.put("function", function);
+ request.setToolChoice(namedToolChoice);
+ log.debug(
+ "Applied specific tool_choice '{}' for Kimi model {}; note it requires"
+ + " thinking to be disabled on thinking-capable models",
+ specific.toolName(),
+ model);
+ }
+ } else {
+ request.setToolChoice("auto");
+ }
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProvider.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProvider.java
new file mode 100644
index 0000000000..9b01e49ca6
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProvider.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.extensions.model.openai.compat.kimi;
+
+import io.agentscope.core.formatter.Formatter;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.Model;
+import io.agentscope.core.model.ModelCreationContext;
+import io.agentscope.core.model.spi.ModelProvider;
+import io.agentscope.core.model.transport.HttpTransport;
+import io.agentscope.core.model.transport.ProxyConfig;
+import io.agentscope.extensions.model.openai.OpenAIChatModel;
+import io.agentscope.extensions.model.openai.dto.OpenAIMessage;
+import io.agentscope.extensions.model.openai.dto.OpenAIRequest;
+import io.agentscope.extensions.model.openai.dto.OpenAIResponse;
+import java.util.regex.Pattern;
+
+/**
+ * Kimi (Moonshot AI) provider registered through {@link java.util.ServiceLoader}.
+ *
+ * Kimi exposes an OpenAI-compatible Chat Completions endpoint, so this provider creates
+ * {@link OpenAIChatModel} instances preconfigured for Kimi:
+ *
+ * - Base URL defaults to {@code https://api.moonshot.cn/v1}
+ * - Formatter defaults to {@link KimiFormatter} (a custom {@link Formatter} component in the
+ * {@link ModelCreationContext} takes precedence, e.g. {@link KimiMultiAgentFormatter})
+ * - Native structured output defaults to disabled, because the Kimi {@code response_format}
+ * only supports {@code json_object} (not {@code json_schema}); the agent falls back to the
+ * {@code generate_response} tool instead
+ * - Native structured output alongside tools defaults to disabled, because Kimi prioritises
+ * {@code response_format} over tool invocations when both are present
+ *
+ *
+ * The API key is taken from {@link ModelCreationContext#getApiKey()}, then from the
+ * {@code MOONSHOT_API_KEY} environment variable, then from {@code KIMI_API_KEY}.
+ *
+ *
Usage:
+ *
{@code
+ * ReActAgent agent = ReActAgent.builder()
+ * .name("assistant")
+ * .model("kimi:kimi-k3") // resolved by ModelRegistry through this provider
+ * .build();
+ * }
+ */
+public final class KimiModelProvider implements ModelProvider {
+
+ private static final String PREFIX = "kimi:";
+ private static final Pattern MODEL_ID = Pattern.compile("kimi:.+");
+ private static final String DEFAULT_BASE_URL = "https://api.moonshot.cn/v1";
+ private static final String OPTION_CONTEXT_WINDOW_SIZE = "contextWindowSize";
+ private static final String OPTION_NATIVE_STRUCTURED_OUTPUT = "nativeStructuredOutput";
+ private static final String OPTION_NATIVE_STRUCTURED_OUTPUT_WITH_TOOLS =
+ "nativeStructuredOutputWithTools";
+
+ @Override
+ public String providerId() {
+ return "kimi";
+ }
+
+ @Override
+ public boolean supports(String modelId) {
+ return modelId != null
+ && MODEL_ID.matcher(modelId).matches()
+ && trimToNull(modelId.substring(PREFIX.length())) != null;
+ }
+
+ @Override
+ public Model create(String modelId) {
+ return create(modelId, ModelCreationContext.empty());
+ }
+
+ @Override
+ public Model create(String modelId, ModelCreationContext context) {
+ if (!supports(modelId)) {
+ throw new IllegalArgumentException("Unsupported Kimi model id: " + modelId);
+ }
+ // supports() guarantees the suffix is non-blank
+ String modelName = trimToNull(modelId.substring(PREFIX.length()));
+ String apiKey =
+ firstNonBlank(
+ context.getApiKey(),
+ System.getenv("MOONSHOT_API_KEY"),
+ System.getenv("KIMI_API_KEY"));
+ if (apiKey == null) {
+ throw new IllegalStateException(
+ "An API key is required to auto-create model "
+ + modelId
+ + ": provide it via ModelCreationContext#apiKey or the"
+ + " MOONSHOT_API_KEY / KIMI_API_KEY environment variable");
+ }
+ String baseUrl = trimToNull(context.getBaseUrl());
+ OpenAIChatModel.Builder builder =
+ OpenAIChatModel.builder()
+ .apiKey(apiKey)
+ .modelName(modelName)
+ .baseUrl(baseUrl != null ? baseUrl : DEFAULT_BASE_URL)
+ .stream(context.getStream() != null ? context.getStream() : true);
+ String endpointPath = trimToNull(context.getEndpointPath());
+ if (endpointPath != null) {
+ builder.endpointPath(endpointPath);
+ }
+ applyAdvancedOptions(builder, context);
+ return builder.build();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void applyAdvancedOptions(
+ OpenAIChatModel.Builder builder, ModelCreationContext context) {
+ GenerateOptions generateOptions = context.component(GenerateOptions.class);
+ if (generateOptions != null) {
+ builder.generateOptions(generateOptions);
+ }
+ HttpTransport httpTransport = context.component(HttpTransport.class);
+ if (httpTransport != null) {
+ builder.httpTransport(httpTransport);
+ }
+ ProxyConfig proxyConfig = context.component(ProxyConfig.class);
+ if (proxyConfig != null) {
+ builder.proxy(proxyConfig);
+ }
+ Formatter formatter =
+ (Formatter)
+ findAssignableComponent(context, Formatter.class);
+ builder.formatter(formatter != null ? formatter : new KimiFormatter());
+ Integer contextWindowSize = intOption(context, OPTION_CONTEXT_WINDOW_SIZE);
+ if (contextWindowSize != null) {
+ builder.contextWindowSize(contextWindowSize);
+ }
+ // Kimi response_format only supports json_object, so native structured output is
+ // disabled by default; the option can explicitly re-enable it.
+ Boolean nativeStructuredOutput = booleanOption(context, OPTION_NATIVE_STRUCTURED_OUTPUT);
+ builder.nativeStructuredOutput(
+ nativeStructuredOutput != null ? nativeStructuredOutput : false);
+ // Kimi prioritises response_format over tool invocations when both are present, so
+ // structured output alongside tools is disabled by default as well.
+ Boolean nativeStructuredOutputWithTools =
+ booleanOption(context, OPTION_NATIVE_STRUCTURED_OUTPUT_WITH_TOOLS);
+ builder.nativeStructuredOutputWithTools(
+ nativeStructuredOutputWithTools != null ? nativeStructuredOutputWithTools : false);
+ }
+
+ private static String firstNonBlank(String... candidates) {
+ for (String candidate : candidates) {
+ String normalized = trimToNull(candidate);
+ if (normalized != null) {
+ return normalized;
+ }
+ }
+ return null;
+ }
+
+ private static String trimToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+
+ private static Object findAssignableComponent(
+ ModelCreationContext context, Class> componentType) {
+ for (Object value : context.getComponents().values()) {
+ if (componentType.isInstance(value)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ private static Integer intOption(ModelCreationContext context, String key) {
+ Object value = context.option(key);
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Number number) {
+ return number.intValue();
+ }
+ throw new IllegalArgumentException(
+ "ModelCreationContext option " + key + " must be a number");
+ }
+
+ private static Boolean booleanOption(ModelCreationContext context, String key) {
+ Object value = context.option(key);
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Boolean bool) {
+ return bool;
+ }
+ throw new IllegalArgumentException(
+ "ModelCreationContext option " + key + " must be a boolean");
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatter.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatter.java
new file mode 100644
index 0000000000..1ad50c3b05
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.extensions.model.openai.compat.kimi;
+
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.ToolChoice;
+import io.agentscope.extensions.model.openai.dto.OpenAIRequest;
+import io.agentscope.extensions.model.openai.formatter.OpenAIMultiAgentFormatter;
+
+/**
+ * Multi-agent formatter for Kimi (Moonshot AI) models.
+ *
+ * This formatter extends {@link OpenAIMultiAgentFormatter} with the same Kimi-specific
+ * handling as {@link KimiFormatter}:
+ *
+ * - Fixed sampling parameters ({@code temperature} / {@code top_p} / penalties) are
+ * stripped on {@code kimi-*} models
+ * - {@code reasoning_effort} is only kept for {@code kimi-k3};
+ * {@code thinking_budget} is always stripped
+ * - {@code max_completion_tokens} is mapped to {@code max_tokens}
+ * - {@code tool_choice} is degraded when the target model does not support the requested
+ * mode (see {@link KimiFormatter} for details)
+ * - The {@code strict} parameter in tool definitions is not sent
+ *
+ *
+ * Usage:
+ *
{@code
+ * OpenAIChatModel.builder()
+ * .formatter(new KimiMultiAgentFormatter())
+ * .modelName("kimi-k3")
+ * .baseUrl("https://api.moonshot.cn/v1")
+ * .apiKey(apiKey)
+ * .build();
+ * }
+ *
+ * @see KimiFormatter
+ */
+public class KimiMultiAgentFormatter extends OpenAIMultiAgentFormatter {
+
+ public KimiMultiAgentFormatter() {
+ super();
+ }
+
+ public KimiMultiAgentFormatter(String conversationHistoryPrompt) {
+ super(conversationHistoryPrompt);
+ }
+
+ @Override
+ protected boolean supportsStrict() {
+ return false;
+ }
+
+ @Override
+ public void applyOptions(
+ OpenAIRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
+ super.applyOptions(request, options, defaultOptions);
+ KimiFormatter.sanitizeKimiRequest(request);
+ }
+
+ @Override
+ public void applyToolChoice(OpenAIRequest request, ToolChoice toolChoice) {
+ KimiFormatter.applyKimiToolChoice(request, toolChoice);
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/resources/META-INF/services/io.agentscope.core.model.spi.ModelProvider b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/resources/META-INF/services/io.agentscope.core.model.spi.ModelProvider
index ab4ba676a5..ee4642883c 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/resources/META-INF/services/io.agentscope.core.model.spi.ModelProvider
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/resources/META-INF/services/io.agentscope.core.model.spi.ModelProvider
@@ -1,3 +1,4 @@
io.agentscope.extensions.model.openai.OpenAIModelProvider
+io.agentscope.extensions.model.openai.compat.kimi.KimiModelProvider
io.agentscope.extensions.model.openai.compat.glm.GLMModelProvider
io.agentscope.extensions.model.openai.compat.deepseek.DeepSeekModelProvider
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatterTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatterTest.java
new file mode 100644
index 0000000000..91364bed66
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiFormatterTest.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.extensions.model.openai.compat.kimi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.message.ThinkingBlock;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.ToolChoice;
+import io.agentscope.core.model.ToolSchema;
+import io.agentscope.extensions.model.openai.dto.OpenAIMessage;
+import io.agentscope.extensions.model.openai.dto.OpenAIRequest;
+import io.agentscope.extensions.model.openai.dto.OpenAITool;
+import io.agentscope.extensions.model.openai.dto.OpenAIToolFunction;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link KimiFormatter}.
+ *
+ * Tests verify Kimi (Moonshot AI)-specific requirements per the latest official API
+ * reference:
+ *
+ * - Fixed sampling params (temperature / top_p / penalties) are stripped on kimi-* models
+ * - reasoning_effort is kimi-k3 only
+ * - tool_choice: required is degraded on K2.x, specific is degraded on always-thinking
+ * models
+ * - Does NOT send the strict parameter in tool definitions
+ * - reasoning_content is preserved in assistant history (Preserved Thinking)
+ *
+ */
+@Tag("unit")
+@DisplayName("KimiFormatter (compat.kimi) Unit Tests")
+class KimiFormatterTest {
+
+ private KimiFormatter formatter;
+
+ @BeforeEach
+ void setUp() {
+ formatter = new KimiFormatter();
+ }
+
+ private static OpenAIRequest requestFor(String model) {
+ return OpenAIRequest.builder().model(model).messages(List.of()).build();
+ }
+
+ private static OpenAIRequest requestWithTools(String model) {
+ OpenAIToolFunction function = new OpenAIToolFunction();
+ function.setName("get_weather");
+ OpenAITool tool = new OpenAITool();
+ tool.setFunction(function);
+ tool.setType("function");
+
+ return OpenAIRequest.builder()
+ .model(model)
+ .messages(List.of())
+ .tools(List.of(tool))
+ .build();
+ }
+
+ @Nested
+ @DisplayName("supportsStrict Tests")
+ class SupportsStrictTests {
+
+ @Test
+ @DisplayName("supportsStrict should return false")
+ void testSupportsStrictReturnsFalse() {
+ assertFalse(formatter.supportsStrict());
+ }
+
+ @Test
+ @DisplayName("applyTools should not include strict parameter")
+ void testApplyToolsWithoutStrict() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ ToolSchema tool =
+ ToolSchema.builder()
+ .name("test_tool")
+ .description("Test tool")
+ .strict(true)
+ .build();
+
+ formatter.applyTools(request, List.of(tool));
+
+ assertNotNull(request.getTools());
+ assertEquals(1, request.getTools().size());
+ // Strict should not be set because Kimi doesn't document it
+ assertNull(request.getTools().get(0).getFunction().getStrict());
+ }
+ }
+
+ @Nested
+ @DisplayName("Model Classification Tests")
+ class ModelClassificationTests {
+
+ @Test
+ @DisplayName("kimi-* models have fixed sampling params")
+ void testFixedSamplingParams() {
+ assertTrue(KimiFormatter.hasFixedSamplingParams("kimi-k3"));
+ assertTrue(KimiFormatter.hasFixedSamplingParams("kimi-k2.7-code"));
+ assertTrue(KimiFormatter.hasFixedSamplingParams("kimi-k2.6"));
+ assertFalse(KimiFormatter.hasFixedSamplingParams("moonshot-v1-8k"));
+ assertFalse(KimiFormatter.hasFixedSamplingParams(null));
+ }
+
+ @Test
+ @DisplayName("kimi-k3 and kimi-k2.7-code are always-thinking models")
+ void testAlwaysThinkingModels() {
+ assertTrue(KimiFormatter.isAlwaysThinkingModel("kimi-k3"));
+ assertTrue(KimiFormatter.isAlwaysThinkingModel("kimi-k2.7-code"));
+ assertTrue(KimiFormatter.isAlwaysThinkingModel("kimi-k2.7-code-highspeed"));
+ // Only the documented kimi-k2.7-code series is always-thinking; a plain
+ // kimi-k2.7* model must not have its tool_choice degraded
+ assertFalse(KimiFormatter.isAlwaysThinkingModel("kimi-k2.7"));
+ assertFalse(KimiFormatter.isAlwaysThinkingModel("kimi-k2.6"));
+ assertFalse(KimiFormatter.isAlwaysThinkingModel("moonshot-v1-8k"));
+ assertFalse(KimiFormatter.isAlwaysThinkingModel(null));
+ }
+
+ @Test
+ @DisplayName("Only kimi-k3 supports reasoning_effort")
+ void testSupportsReasoningEffort() {
+ assertTrue(KimiFormatter.supportsReasoningEffort("kimi-k3"));
+ assertFalse(KimiFormatter.supportsReasoningEffort("kimi-k2.6"));
+ assertFalse(KimiFormatter.supportsReasoningEffort("kimi-k2.7-code"));
+ assertFalse(KimiFormatter.supportsReasoningEffort("moonshot-v1-8k"));
+ assertFalse(KimiFormatter.supportsReasoningEffort(null));
+ }
+
+ @Test
+ @DisplayName("K2.x series does not support tool_choice=required")
+ void testSupportsRequiredToolChoice() {
+ assertTrue(KimiFormatter.supportsRequiredToolChoice("kimi-k3"));
+ assertFalse(KimiFormatter.supportsRequiredToolChoice("kimi-k2.6"));
+ assertFalse(KimiFormatter.supportsRequiredToolChoice("kimi-k2.7-code"));
+ assertTrue(KimiFormatter.supportsRequiredToolChoice("moonshot-v1-8k"));
+ // Unknown model: keep the caller-requested value rather than degrade it
+ assertTrue(KimiFormatter.supportsRequiredToolChoice(null));
+ }
+ }
+
+ @Nested
+ @DisplayName("applyOptions Tests")
+ class ApplyOptionsTests {
+
+ @Test
+ @DisplayName("Should strip fixed sampling params on kimi-* models")
+ void testStripFixedSamplingParamsOnKimiModels() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ GenerateOptions options =
+ GenerateOptions.builder()
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.5)
+ .build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNull(request.getTemperature());
+ assertNull(request.getTopP());
+ assertNull(request.getFrequencyPenalty());
+ assertNull(request.getPresencePenalty());
+ }
+
+ @Test
+ @DisplayName("Should keep sampling params on moonshot-v1 models")
+ void testKeepSamplingParamsOnMoonshotModels() {
+ OpenAIRequest request = requestFor("moonshot-v1-8k");
+
+ GenerateOptions options =
+ GenerateOptions.builder()
+ .temperature(0.3)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.5)
+ .build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertEquals(0.3, request.getTemperature());
+ assertEquals(0.9, request.getTopP());
+ assertEquals(0.5, request.getFrequencyPenalty());
+ assertEquals(0.5, request.getPresencePenalty());
+ }
+
+ @Test
+ @DisplayName("Should keep reasoning_effort on kimi-k3")
+ void testKeepReasoningEffortOnK3() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ GenerateOptions options = GenerateOptions.builder().reasoningEffort("high").build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertEquals("high", request.getReasoningEffort());
+ }
+
+ @Test
+ @DisplayName("Should strip reasoning_effort on K2.x models")
+ void testStripReasoningEffortOnK2() {
+ OpenAIRequest request = requestFor("kimi-k2.6");
+
+ GenerateOptions options = GenerateOptions.builder().reasoningEffort("high").build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNull(request.getReasoningEffort());
+ }
+
+ @Test
+ @DisplayName("Should strip thinking_budget")
+ void testStripThinkingBudget() {
+ OpenAIRequest request = requestFor("kimi-k2.6");
+
+ GenerateOptions options = GenerateOptions.builder().thinkingBudget(2048).build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNull(request.getThinkingBudget());
+ }
+
+ @Test
+ @DisplayName("Should map max_completion_tokens to max_tokens")
+ void testMapMaxCompletionTokensToMaxTokens() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ GenerateOptions options = GenerateOptions.builder().maxCompletionTokens(32768).build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertEquals(32768, request.getMaxTokens());
+ assertNull(request.getMaxCompletionTokens());
+ }
+
+ @Test
+ @DisplayName("max_tokens should take precedence over max_completion_tokens")
+ void testMaxTokensTakesPrecedence() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ GenerateOptions options =
+ GenerateOptions.builder().maxTokens(16000).maxCompletionTokens(32768).build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertEquals(16000, request.getMaxTokens());
+ assertNull(request.getMaxCompletionTokens());
+ }
+
+ @Test
+ @DisplayName("Should pass through the thinking body param for K2.x models")
+ void testThinkingBodyParamPassThrough() {
+ OpenAIRequest request = requestFor("kimi-k2.6");
+
+ GenerateOptions options =
+ GenerateOptions.builder()
+ .additionalBodyParam("thinking", Map.of("type", "disabled"))
+ .build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNotNull(request.getExtraParams());
+ assertEquals(Map.of("type", "disabled"), request.getExtraParams().get("thinking"));
+ }
+ }
+
+ @Nested
+ @DisplayName("applyKimiToolChoice Tests")
+ class ApplyKimiToolChoiceTests {
+
+ @Test
+ @DisplayName("Should set tool_choice to auto for Auto")
+ void testToolChoiceAuto() {
+ OpenAIRequest request = requestWithTools("kimi-k3");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Auto());
+
+ assertEquals("auto", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should set tool_choice to none for None")
+ void testToolChoiceNone() {
+ OpenAIRequest request = requestWithTools("kimi-k2.6");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.None());
+
+ assertEquals("none", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should keep required on kimi-k3")
+ void testToolChoiceRequiredOnK3() {
+ OpenAIRequest request = requestWithTools("kimi-k3");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Required());
+
+ assertEquals("required", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should degrade required to auto on kimi-k2.6")
+ void testToolChoiceRequiredDegradesOnK26() {
+ OpenAIRequest request = requestWithTools("kimi-k2.6");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Required());
+
+ assertEquals("auto", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should degrade required to auto on kimi-k2.7-code")
+ void testToolChoiceRequiredDegradesOnK27Code() {
+ OpenAIRequest request = requestWithTools("kimi-k2.7-code");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Required());
+
+ assertEquals("auto", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should degrade specific to auto on always-thinking models")
+ void testToolChoiceSpecificDegradesOnAlwaysThinkingModels() {
+ OpenAIRequest request = requestWithTools("kimi-k3");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Specific("get_weather"));
+
+ assertEquals("auto", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should keep specific tool_choice on kimi-k2.6")
+ @SuppressWarnings("unchecked")
+ void testToolChoiceSpecificKeptOnK26() {
+ OpenAIRequest request = requestWithTools("kimi-k2.6");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Specific("get_weather"));
+
+ assertTrue(request.getToolChoice() instanceof Map);
+ Map choice = (Map) request.getToolChoice();
+ assertEquals("function", choice.get("type"));
+ assertEquals("get_weather", ((Map) choice.get("function")).get("name"));
+ }
+
+ @Test
+ @DisplayName("Should not set tool_choice if no tools")
+ void testNoToolChoiceWithoutTools() {
+ OpenAIRequest request = requestFor("kimi-k3");
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Auto());
+
+ assertNull(request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should not set tool_choice if tools list is empty")
+ void testNoToolChoiceWithEmptyTools() {
+ OpenAIRequest request =
+ OpenAIRequest.builder()
+ .model("kimi-k3")
+ .messages(List.of())
+ .tools(List.of())
+ .build();
+
+ KimiFormatter.applyKimiToolChoice(request, new ToolChoice.Auto());
+
+ assertNull(request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("Should handle null toolChoice")
+ void testNullToolChoice() {
+ OpenAIRequest request = requestWithTools("kimi-k3");
+
+ KimiFormatter.applyKimiToolChoice(request, null);
+
+ assertEquals("auto", request.getToolChoice());
+ }
+ }
+
+ @Nested
+ @DisplayName("doFormat / Preserved Thinking Tests")
+ class DoFormatTests {
+
+ @Test
+ @DisplayName("Should format basic conversation")
+ void testFormatBasicConversation() {
+ List messages =
+ List.of(
+ Msg.builder()
+ .role(MsgRole.SYSTEM)
+ .content(
+ List.of(
+ TextBlock.builder()
+ .text("You are Kimi")
+ .build()))
+ .build(),
+ Msg.builder()
+ .role(MsgRole.USER)
+ .content(List.of(TextBlock.builder().text("Hello").build()))
+ .build());
+
+ List result = formatter.format(messages);
+
+ assertEquals(2, result.size());
+ assertEquals("system", result.get(0).getRole());
+ assertEquals("user", result.get(1).getRole());
+ }
+
+ @Test
+ @DisplayName("Should preserve reasoning_content in assistant history")
+ void testPreservesReasoningContent() {
+ List messages =
+ List.of(
+ Msg.builder()
+ .role(MsgRole.USER)
+ .content(
+ List.of(
+ TextBlock.builder()
+ .text("First question")
+ .build()))
+ .build(),
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .content(
+ List.of(
+ ThinkingBlock.builder()
+ .thinking("Let me think...")
+ .build(),
+ TextBlock.builder().text("The answer").build()))
+ .build());
+
+ List result = formatter.format(messages);
+
+ assertEquals(2, result.size());
+ OpenAIMessage assistantMsg = result.get(1);
+ assertEquals("assistant", assistantMsg.getRole());
+ // Preserved Thinking: kimi-k3 / kimi-k2.7-code require reasoning_content to be
+ // passed back in the message history
+ assertEquals("Let me think...", assistantMsg.getReasoningContent());
+ }
+ }
+
+ @Nested
+ @DisplayName("applyToolChoice Integration Tests")
+ class ApplyToolChoiceIntegrationTests {
+
+ @Test
+ @DisplayName("Should apply tool choice through formatter")
+ void testApplyToolChoiceThroughFormatter() {
+ OpenAIRequest request = requestWithTools("kimi-k2.6");
+
+ formatter.applyToolChoice(request, new ToolChoice.Required());
+
+ // kimi-k2.6 does not support required, degraded to auto
+ assertEquals("auto", request.getToolChoice());
+ }
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProviderTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProviderTest.java
new file mode 100644
index 0000000000..716b2dcb6e
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiModelProviderTest.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.extensions.model.openai.compat.kimi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.Model;
+import io.agentscope.core.model.ModelCreationContext;
+import io.agentscope.core.model.ModelRegistry;
+import io.agentscope.core.model.transport.HttpRequest;
+import io.agentscope.core.model.transport.HttpResponse;
+import io.agentscope.core.model.transport.HttpTransport;
+import io.agentscope.core.model.transport.ProxyConfig;
+import io.agentscope.extensions.model.openai.OpenAIChatModel;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+class KimiModelProviderTest {
+
+ @AfterEach
+ void tearDown() {
+ ModelRegistry.reset();
+ }
+
+ @Test
+ void providerIdIsKimi() {
+ assertEquals("kimi", new KimiModelProvider().providerId());
+ }
+
+ @Test
+ void supportsKimiModelIds() {
+ KimiModelProvider provider = new KimiModelProvider();
+ assertTrue(provider.supports("kimi:kimi-k3"));
+ assertTrue(provider.supports("kimi:kimi-k2.6"));
+ assertTrue(provider.supports("kimi:moonshot-v1-8k"));
+ assertFalse(provider.supports("kimi:"));
+ // Whitespace-only model names must not be treated as supported
+ assertFalse(provider.supports("kimi: "));
+ assertFalse(provider.supports("kimi: "));
+ assertFalse(provider.supports("openai:gpt-4o-mini"));
+ assertFalse(provider.supports(null));
+ }
+
+ @Test
+ void createTrimsModelName() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder().apiKey("test-kimi-key").build();
+
+ Model model = provider.create("kimi: kimi-k3", context);
+
+ assertEquals("kimi-k3", model.getModelName());
+ }
+
+ @Test
+ void createRejectsUnsupportedModelIdsBeforeReadingEnvironment() {
+ KimiModelProvider provider = new KimiModelProvider();
+
+ assertThrows(IllegalArgumentException.class, () -> provider.create("kimi:"));
+ assertThrows(IllegalArgumentException.class, () -> provider.create("kimi: "));
+ assertThrows(IllegalArgumentException.class, () -> provider.create("kimi-k3"));
+ assertThrows(IllegalArgumentException.class, () -> provider.create(null));
+ }
+
+ @Test
+ void createUsesModelCreationContext() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder().apiKey("test-kimi-key").stream(false)
+ .component(GenerateOptions.class, GenerateOptions.builder().build())
+ .component(ProxyConfig.class, ProxyConfig.http("localhost", 8080))
+ .option("contextWindowSize", 262144)
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model instanceof OpenAIChatModel);
+ assertEquals("kimi-k3", model.getModelName());
+ assertEquals(262144, model.getContextWindowSize());
+ }
+
+ @Test
+ void nativeStructuredOutputDisabledByDefault() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder().apiKey("test-kimi-key").build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ // Kimi response_format only supports json_object, so native structured
+ // output falls back to the generate_response tool by default
+ assertFalse(model.supportsNativeStructuredOutput());
+ }
+
+ @Test
+ void nativeStructuredOutputCanBeEnabledByOption() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .option("nativeStructuredOutput", true)
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model.supportsNativeStructuredOutput());
+ }
+
+ @Test
+ void nativeStructuredOutputWithToolsDisabledByDefault() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder().apiKey("test-kimi-key").build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ // Kimi prioritises response_format over tool invocations when both are present
+ assertFalse(model.supportsNativeStructuredOutputWithTools());
+ }
+
+ @Test
+ void nativeStructuredOutputWithToolsCanBeEnabledByOption() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .option("nativeStructuredOutput", true)
+ .option("nativeStructuredOutputWithTools", true)
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model.supportsNativeStructuredOutputWithTools());
+ }
+
+ @Test
+ void contextWindowSizeOptionMustBeANumber() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .option("contextWindowSize", "not-a-number")
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class, () -> provider.create("kimi:kimi-k3", context));
+ }
+
+ @Test
+ void nativeStructuredOutputOptionsMustBeBooleans() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext structuredOutputContext =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .option("nativeStructuredOutput", "yes")
+ .build();
+ ModelCreationContext structuredOutputWithToolsContext =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .option("nativeStructuredOutputWithTools", "yes")
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> provider.create("kimi:kimi-k3", structuredOutputContext));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> provider.create("kimi:kimi-k3", structuredOutputWithToolsContext));
+ }
+
+ @Test
+ void createWithoutApiKeyThrowsIllegalStateException() {
+ // Only meaningful when the environment provides no Kimi API key either
+ assumeTrue(isBlank(System.getenv("MOONSHOT_API_KEY")));
+ assumeTrue(isBlank(System.getenv("KIMI_API_KEY")));
+
+ KimiModelProvider provider = new KimiModelProvider();
+
+ IllegalStateException fromEmptyContext =
+ assertThrows(IllegalStateException.class, () -> provider.create("kimi:kimi-k3"));
+ assertTrue(fromEmptyContext.getMessage().contains("MOONSHOT_API_KEY"));
+ assertTrue(fromEmptyContext.getMessage().contains("KIMI_API_KEY"));
+
+ // A blank context API key must fall through to the environment lookup
+ ModelCreationContext blankKeyContext = ModelCreationContext.builder().apiKey(" ").build();
+ assertThrows(
+ IllegalStateException.class,
+ () -> provider.create("kimi:kimi-k3", blankKeyContext));
+ }
+
+ @Test
+ void endpointPathIsApplied() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .endpointPath("/custom/chat/completions")
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model instanceof OpenAIChatModel);
+ }
+
+ @Test
+ void httpTransportComponentIsApplied() {
+ KimiModelProvider provider = new KimiModelProvider();
+ HttpTransport transport =
+ new HttpTransport() {
+ @Override
+ public HttpResponse execute(HttpRequest request) {
+ throw new UnsupportedOperationException("not used in this test");
+ }
+
+ @Override
+ public Flux stream(HttpRequest request) {
+ return Flux.empty();
+ }
+
+ @Override
+ public void close() {}
+ };
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .component(HttpTransport.class, transport)
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model instanceof OpenAIChatModel);
+ }
+
+ @Test
+ void customFormatterComponentTakesPrecedence() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .component(KimiMultiAgentFormatter.class, new KimiMultiAgentFormatter())
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model instanceof OpenAIChatModel);
+ }
+
+ @Test
+ void customBaseUrlOverridesDefault() {
+ KimiModelProvider provider = new KimiModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .apiKey("test-kimi-key")
+ .baseUrl("https://kimi.example.com/v1")
+ .build();
+
+ Model model = provider.create("kimi:kimi-k3", context);
+
+ assertTrue(model instanceof OpenAIChatModel);
+ }
+
+ @Test
+ void modelRegistryFindsKimiProviderFromServiceLoader() {
+ assertTrue(ModelRegistry.canResolve("kimi:kimi-k3"));
+ }
+
+ private static boolean isBlank(String value) {
+ return value == null || value.trim().isEmpty();
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatterTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatterTest.java
new file mode 100644
index 0000000000..0073f10b46
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/compat/kimi/KimiMultiAgentFormatterTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.extensions.model.openai.compat.kimi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.ToolChoice;
+import io.agentscope.core.model.ToolSchema;
+import io.agentscope.extensions.model.openai.dto.OpenAIMessage;
+import io.agentscope.extensions.model.openai.dto.OpenAIRequest;
+import io.agentscope.extensions.model.openai.dto.OpenAITool;
+import io.agentscope.extensions.model.openai.dto.OpenAIToolFunction;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link KimiMultiAgentFormatter}.
+ *
+ * Tests verify Kimi multi-agent specific requirements:
+ *
+ * - Inherits multi-agent conversation merging from OpenAIMultiAgentFormatter
+ * - Fixed sampling params are stripped on kimi-* models
+ * - tool_choice is degraded when unsupported by the target model
+ * - Does NOT send the strict parameter in tool definitions
+ *
+ */
+@Tag("unit")
+@DisplayName("KimiMultiAgentFormatter (compat.kimi) Unit Tests")
+class KimiMultiAgentFormatterTest {
+
+ private KimiMultiAgentFormatter formatter;
+
+ @BeforeEach
+ void setUp() {
+ formatter = new KimiMultiAgentFormatter();
+ }
+
+ private static Msg textMsg(MsgRole role, String name, String text) {
+ return Msg.builder()
+ .role(role)
+ .name(name)
+ .content(List.of(TextBlock.builder().text(text).build()))
+ .build();
+ }
+
+ private static OpenAIRequest requestWithTools(String model) {
+ OpenAIToolFunction function = new OpenAIToolFunction();
+ function.setName("test_tool");
+ OpenAITool tool = new OpenAITool();
+ tool.setFunction(function);
+ tool.setType("function");
+
+ return OpenAIRequest.builder()
+ .model(model)
+ .messages(List.of())
+ .tools(List.of(tool))
+ .build();
+ }
+
+ @Nested
+ @DisplayName("Constructor Tests")
+ class ConstructorTests {
+
+ @Test
+ @DisplayName("Custom conversation history prompt should be honored")
+ void testCustomPromptConstructor() {
+ KimiMultiAgentFormatter customFormatter =
+ new KimiMultiAgentFormatter("# Custom History\n");
+
+ List messages =
+ List.of(
+ textMsg(MsgRole.USER, "Alice", "Hello"),
+ textMsg(MsgRole.ASSISTANT, "Bob", "Hi Alice"));
+
+ List result = customFormatter.format(messages);
+
+ assertFalse(result.isEmpty());
+ String content = result.get(0).getContentAsString();
+ assertTrue(content.contains("# Custom History"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Multi-agent Formatting Tests")
+ class MultiAgentFormattingTests {
+
+ @Test
+ @DisplayName("Should merge agent conversation into a user message")
+ void testMergesConversationIntoUserMessage() {
+ List messages =
+ List.of(
+ textMsg(MsgRole.USER, "Alice", "Hello"),
+ textMsg(MsgRole.ASSISTANT, "Bob", "Hi Alice"));
+
+ List result = formatter.format(messages);
+
+ assertFalse(result.isEmpty());
+ assertTrue(result.stream().anyMatch(m -> "user".equals(m.getRole())));
+ }
+ }
+
+ @Nested
+ @DisplayName("Kimi-specific Behavior Tests")
+ class KimiSpecificBehaviorTests {
+
+ @Test
+ @DisplayName("supportsStrict should return false")
+ void testSupportsStrictReturnsFalse() {
+ assertFalse(formatter.supportsStrict());
+ }
+
+ @Test
+ @DisplayName("applyTools should not include strict parameter")
+ void testApplyToolsWithoutStrict() {
+ OpenAIRequest request =
+ OpenAIRequest.builder().model("kimi-k3").messages(List.of()).build();
+
+ ToolSchema tool =
+ ToolSchema.builder()
+ .name("test_tool")
+ .description("Test tool")
+ .strict(true)
+ .build();
+
+ formatter.applyTools(request, List.of(tool));
+
+ assertNotNull(request.getTools());
+ assertNull(request.getTools().get(0).getFunction().getStrict());
+ }
+
+ @Test
+ @DisplayName("applyToolChoice should degrade required to auto on K2.x")
+ void testToolChoiceRequiredDegradesOnK2() {
+ OpenAIRequest request = requestWithTools("kimi-k2.6");
+
+ formatter.applyToolChoice(request, new ToolChoice.Required());
+
+ assertEquals("auto", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("applyToolChoice should keep required on kimi-k3")
+ void testToolChoiceRequiredKeptOnK3() {
+ OpenAIRequest request = requestWithTools("kimi-k3");
+
+ formatter.applyToolChoice(request, new ToolChoice.Required());
+
+ assertEquals("required", request.getToolChoice());
+ }
+
+ @Test
+ @DisplayName("applyOptions should strip fixed sampling params on kimi-* models")
+ void testStripFixedSamplingParams() {
+ OpenAIRequest request =
+ OpenAIRequest.builder().model("kimi-k2.6").messages(List.of()).build();
+
+ GenerateOptions options =
+ GenerateOptions.builder()
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.5)
+ .build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNull(request.getTemperature());
+ assertNull(request.getTopP());
+ assertNull(request.getFrequencyPenalty());
+ assertNull(request.getPresencePenalty());
+ }
+
+ @Test
+ @DisplayName("applyOptions should map max_completion_tokens and strip reasoning_effort")
+ void testSharedSanitizeBehavior() {
+ OpenAIRequest request =
+ OpenAIRequest.builder().model("kimi-k2.6").messages(List.of()).build();
+
+ GenerateOptions options =
+ GenerateOptions.builder()
+ .reasoningEffort("high")
+ .maxCompletionTokens(32768)
+ .build();
+
+ formatter.applyOptions(request, options, null);
+
+ assertNull(request.getReasoningEffort());
+ assertEquals(32768, request.getMaxTokens());
+ assertNull(request.getMaxCompletionTokens());
+ }
+ }
+}
diff --git a/docs/v2/en/docs/building-blocks/model.md b/docs/v2/en/docs/building-blocks/model.md
index c34fb54882..d3ff81789f 100644
--- a/docs/v2/en/docs/building-blocks/model.md
+++ b/docs/v2/en/docs/building-blocks/model.md
@@ -73,6 +73,8 @@ ReActAgent agent =
.build();
```
+The extension module is discovered through Java SPI. The model provider reads its standard environment variables such as `DASHSCOPE_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `MOONSHOT_API_KEY` (for `kimi:` ids served by the OpenAI-compatible Kimi endpoint). Ollama reads `OLLAMA_BASE_URL` when present and otherwise defaults to the local Ollama endpoint.
+The extension module is discovered through Java SPI. The model provider reads its standard environment variables such as `DASHSCOPE_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY`. Ollama reads `OLLAMA_BASE_URL` when present and otherwise defaults to the local Ollama endpoint.
The extension module is discovered through Java SPI. The model provider reads its standard environment variables such as `DASHSCOPE_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `GLM_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY`. Ollama reads `OLLAMA_BASE_URL` when present and otherwise defaults to the local Ollama endpoint.
### Explicit model builder
diff --git a/docs/v2/en/integration/model/openai.md b/docs/v2/en/integration/model/openai.md
index 3aaa7b2d28..9807b98262 100644
--- a/docs/v2/en/integration/model/openai.md
+++ b/docs/v2/en/integration/model/openai.md
@@ -1,6 +1,6 @@
# OpenAI Model
-`agentscope-extensions-model-openai` integrates OpenAI Chat Completions-style models. It is also the module to use for OpenAI-compatible endpoints such as DeepSeek, GLM, and similar services when their wire format follows the OpenAI API.
+`agentscope-extensions-model-openai` integrates OpenAI Chat Completions-style models. It is also the module to use for OpenAI-compatible endpoints such as DeepSeek, GLM, Kimi, and similar services when their wire format follows the OpenAI API.
## Add the dependency
@@ -39,6 +39,11 @@ OpenAIChatModel model = OpenAIChatModel.builder()
For generic compatible endpoints, set `baseUrl(...)` and the model name expected by that service. For DeepSeek, prefer the dedicated `deepseek:` registry id so AgentScope applies the DeepSeek base URL, `DEEPSEEK_API_KEY`, and DeepSeek formatter defaults. See [DeepSeek](deepseek.md).
+## Kimi (Moonshot AI)
+
+Kimi has a dedicated adapter in the `io.agentscope.extensions.model.openai.compat.kimi` package. It reuses `OpenAIChatModel` under the hood but is preconfigured for the Kimi open platform endpoint (`https://api.moonshot.cn/v1`).
+
+Set `MOONSHOT_API_KEY` (or `KIMI_API_KEY`), then use the `kimi:` id:
## GLM (Zhipu AI)
GLM has a dedicated adapter in the `io.agentscope.extensions.model.openai.compat.glm` package. It reuses `OpenAIChatModel` under the hood but is preconfigured for the Zhipu open platform endpoint (`https://open.bigmodel.cn/api/paas/v4`).
@@ -48,6 +53,21 @@ Set `GLM_API_KEY` (or `ZHIPUAI_API_KEY`), then use the `glm:` id:
```java
ReActAgent agent = ReActAgent.builder()
.name("assistant")
+ .model("kimi:kimi-k3") // Resolved by KimiModelProvider through ModelRegistry
+ .build();
+```
+
+Or build the model explicitly with a Kimi formatter:
+
+```java
+import io.agentscope.extensions.model.openai.OpenAIChatModel;
+import io.agentscope.extensions.model.openai.compat.kimi.KimiFormatter;
+
+OpenAIChatModel model = OpenAIChatModel.builder()
+ .apiKey(System.getenv("MOONSHOT_API_KEY"))
+ .baseUrl("https://api.moonshot.cn/v1")
+ .modelName("kimi-k3")
+ .formatter(new KimiFormatter()) // or KimiMultiAgentFormatter for multi-agent prompts
.model("glm:glm-5.2") // Resolved by GLMModelProvider through ModelRegistry
.build();
```
@@ -67,6 +87,22 @@ OpenAIChatModel model = OpenAIChatModel.builder()
.build();
```
+The Kimi formatters adapt requests to the latest official API:
+
+- On `kimi-*` models (kimi-k3, kimi-k2.7-code, kimi-k2.6, etc.), `temperature` / `top_p` / `frequency_penalty` / `presence_penalty` are fixed by the platform and the API rejects other values, so these parameters are stripped from requests; the `moonshot-v1` series still accepts them and they are passed through.
+- `reasoning_effort` (`low` / `high` / `max`, default `max`) is only supported by `kimi-k3` and is stripped on other models; use the `thinking` body parameter on K2.x models instead.
+- `thinking_budget` is not a Kimi parameter and is always stripped.
+- Kimi only supports `max_tokens`: when only `max_completion_tokens` (the newer OpenAI style) is set, it is mapped to `max_tokens`. Note that reasoning tokens (`reasoning_content`) count towards `max_tokens`; the official guide recommends `max_tokens >= 16000` for thinking models.
+- `tool_choice`: `auto` / `none` are supported by all models; `required` is only supported by `kimi-k3` and is degraded to `auto` on the K2.x series; forcing a specific function is incompatible with thinking enabled (HTTP 400), so it is degraded to `auto` on always-thinking models (`kimi-k3`, `kimi-k2.7-code`) and passed through otherwise (disable thinking via the `thinking` parameter first on `kimi-k2.6`).
+- The `strict` parameter in tool definitions is not sent (not documented by Kimi).
+- `reasoning_content` on assistant history messages is passed back as-is, satisfying Preserved Thinking on `kimi-k3` / `kimi-k2.7-code` and `thinking.keep = "all"` on `kimi-k2.6`.
+- `KimiModelProvider` disables native structured output by default because the Kimi `response_format` only supports `json_object` (JSON Mode); agents fall back to the `generate_response` tool. It also disables structured output alongside tools by default, because Kimi prioritises `response_format` over tool invocations. Both can be re-enabled with the `nativeStructuredOutput` / `nativeStructuredOutputWithTools` options in `ModelCreationContext`.
+
+Thinking-related parameters:
+
+- `kimi-k2.6` / `kimi-k2.5` control thinking through the `thinking` body parameter, e.g. `GenerateOptions.additionalBodyParam("thinking", Map.of("type", "disabled"))`; `kimi-k2.6` also supports `Map.of("type", "enabled", "keep", "all")` to enable Preserved Thinking.
+- `kimi-k2.7-code` always thinks and Preserved Thinking cannot be disabled; do not pass a `thinking` parameter.
+- `kimi-k3` always thinks and uses the top-level `reasoning_effort` to configure thinking effort; use `GenerateOptions.reasoningEffort("high")`, which is passed through directly.
The GLM formatters adapt requests to the latest Zhipu API:
- Ensure at least one user message exists (the API returns error 1214 otherwise).
diff --git a/docs/v2/zh/docs/building-blocks/model.md b/docs/v2/zh/docs/building-blocks/model.md
index af207671d8..3de56c29fe 100644
--- a/docs/v2/zh/docs/building-blocks/model.md
+++ b/docs/v2/zh/docs/building-blocks/model.md
@@ -73,6 +73,8 @@ ReActAgent agent =
.build();
```
+扩展模块会通过 Java SPI 被自动发现。模型提供商会读取自己的标准环境变量,例如 `DASHSCOPE_API_KEY`、`OPENAI_API_KEY`、`ANTHROPIC_API_KEY`、`GEMINI_API_KEY`、`MOONSHOT_API_KEY`(`kimi:` id 走 Kimi 的 OpenAI 兼容端点)。Ollama 会在存在时读取 `OLLAMA_BASE_URL`,否则默认使用本地 Ollama endpoint。
+扩展模块会通过 Java SPI 被自动发现。模型提供商会读取自己的标准环境变量,例如 `DASHSCOPE_API_KEY`、`OPENAI_API_KEY`、`DEEPSEEK_API_KEY`、`ANTHROPIC_API_KEY`、`GEMINI_API_KEY`。Ollama 会在存在时读取 `OLLAMA_BASE_URL`,否则默认使用本地 Ollama endpoint。
扩展模块会通过 Java SPI 被自动发现。模型提供商会读取自己的标准环境变量,例如 `DASHSCOPE_API_KEY`、`OPENAI_API_KEY`、`GLM_API_KEY`、`DEEPSEEK_API_KEY`、`ANTHROPIC_API_KEY`、`GEMINI_API_KEY`。Ollama 会在存在时读取 `OLLAMA_BASE_URL`,否则默认使用本地 Ollama endpoint。
### 显式 Model builder
diff --git a/docs/v2/zh/integration/model/openai.md b/docs/v2/zh/integration/model/openai.md
index 6327fc4f6e..ff78c073c6 100644
--- a/docs/v2/zh/integration/model/openai.md
+++ b/docs/v2/zh/integration/model/openai.md
@@ -1,6 +1,6 @@
# OpenAI 模型
-`agentscope-extensions-model-openai` 接入 OpenAI Chat Completions 风格的模型。OpenAI 兼容端点也使用这个适配模块,例如 DeepSeek、GLM 等遵循 OpenAI API 载荷格式的服务。
+`agentscope-extensions-model-openai` 接入 OpenAI Chat Completions 风格的模型。OpenAI 兼容端点也使用这个适配模块,例如 DeepSeek、GLM、Kimi 等遵循 OpenAI API 载荷格式的服务。
## 添加依赖
@@ -39,6 +39,11 @@ OpenAIChatModel model = OpenAIChatModel.builder()
接入通用兼容端点时,设置 `baseUrl(...)` 和该服务期望的模型名。DeepSeek 推荐使用专门的 `deepseek:` registry id,这样 AgentScope 会自动应用 DeepSeek base URL、`DEEPSEEK_API_KEY` 和 DeepSeek formatter 默认值。详见 [DeepSeek](deepseek.md)。
+## Kimi(月之暗面 / Moonshot AI)
+
+Kimi 在 `io.agentscope.extensions.model.openai.compat.kimi` 包中提供了专用适配。它底层复用 `OpenAIChatModel`,但默认配置为 Kimi 开放平台端点(`https://api.moonshot.cn/v1`)。
+
+设置 `MOONSHOT_API_KEY`(或 `KIMI_API_KEY`)后,使用 `kimi:` 字符串 id:
## GLM(智谱 AI)
GLM 在 `io.agentscope.extensions.model.openai.compat.glm` 包中提供了专用适配。它底层复用 `OpenAIChatModel`,但默认配置为智谱开放平台端点(`https://open.bigmodel.cn/api/paas/v4`)。
@@ -48,6 +53,21 @@ GLM 在 `io.agentscope.extensions.model.openai.compat.glm` 包中提供了专用
```java
ReActAgent agent = ReActAgent.builder()
.name("assistant")
+ .model("kimi:kimi-k3") // 底层由 KimiModelProvider 通过 ModelRegistry 解析
+ .build();
+```
+
+也可以显式构建并指定 Kimi formatter:
+
+```java
+import io.agentscope.extensions.model.openai.OpenAIChatModel;
+import io.agentscope.extensions.model.openai.compat.kimi.KimiFormatter;
+
+OpenAIChatModel model = OpenAIChatModel.builder()
+ .apiKey(System.getenv("MOONSHOT_API_KEY"))
+ .baseUrl("https://api.moonshot.cn/v1")
+ .modelName("kimi-k3")
+ .formatter(new KimiFormatter()) // 多智能体提示词场景使用 KimiMultiAgentFormatter
.model("glm:glm-5.2") // 底层由 GLMModelProvider 通过 ModelRegistry 解析
.build();
```
@@ -67,6 +87,22 @@ OpenAIChatModel model = OpenAIChatModel.builder()
.build();
```
+Kimi formatter 按最新官方 API 对请求做了以下适配:
+
+- `kimi-*` 系列模型(kimi-k3、kimi-k2.7-code、kimi-k2.6 等)的 `temperature` / `top_p` / `frequency_penalty` / `presence_penalty` 由平台固定、传入其他值会报错,因此这些参数会从请求中移除;`moonshot-v1` 系列仍可修改,会正常透传。
+- `reasoning_effort`(`low` / `high` / `max`,默认 `max`)仅 `kimi-k3` 支持,其他模型上会移除;K2.x 模型请改用 `thinking` 请求体参数。
+- `thinking_budget` 不是 Kimi 参数,始终移除。
+- Kimi 仅支持 `max_tokens`:如果只设置了 `max_completion_tokens`(OpenAI 新风格),会自动映射为 `max_tokens`。注意思考内容(`reasoning_content`)也计入 `max_tokens`,官方建议思考模型设置 `max_tokens >= 16000`。
+- `tool_choice`:`auto` / `none` 全系支持;`required` 仅 `kimi-k3` 支持,K2.x 系列会降级为 `auto`;指定具体函数与思考开启不兼容(HTTP 400),在始终思考的模型(`kimi-k3`、`kimi-k2.7-code`)上会降级为 `auto`,其余模型正常透传(`kimi-k2.6` 上使用时请先通过 `thinking` 参数关闭思考)。
+- 工具定义中不发送 `strict` 参数(Kimi 文档未支持)。
+- assistant 历史消息中的 `reasoning_content` 会原样回传,满足 `kimi-k3` / `kimi-k2.7-code` 的保留式思考(Preserved Thinking)以及 `kimi-k2.6` 的 `thinking.keep = "all"` 要求。
+- `KimiModelProvider` 默认关闭原生结构化输出,因为 Kimi 的 `response_format` 仅支持 `json_object`(JSON Mode),agent 会自动改走 `generate_response` 工具;同时默认关闭"结构化输出与工具共存"(Kimi 会优先遵循 `response_format` 而跳过工具调用)。两者都可以在 `ModelCreationContext` 中通过 `nativeStructuredOutput` / `nativeStructuredOutputWithTools` 选项显式开启。
+
+思考相关参数:
+
+- `kimi-k2.6` / `kimi-k2.5` 通过 `thinking` 请求体参数控制思考,例如 `GenerateOptions.additionalBodyParam("thinking", Map.of("type", "disabled"))`;`kimi-k2.6` 还支持 `Map.of("type", "enabled", "keep", "all")` 开启保留式思考。
+- `kimi-k2.7-code` 始终开启思考且保留式思考不可关闭,无需(也不应)传入 `thinking` 参数。
+- `kimi-k3` 始终思考,使用顶层 `reasoning_effort` 配置思考力度,直接用 `GenerateOptions.reasoningEffort("high")` 即可透传。
GLM formatter 按最新智谱 API 对请求做了以下适配:
- 保证至少存在一条 user 消息(否则 API 返回错误码 1214)。