diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/Anthropic.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/Anthropic.java new file mode 100644 index 000000000..c4783e4e1 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/Anthropic.java @@ -0,0 +1,8 @@ +package com.yomahub.liteflow.agent.anthropic; + +public final class Anthropic { + private Anthropic() {} + public static AnthropicSpec of(String modelName) { + return new AnthropicSpec(modelName); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicCompatible.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicCompatible.java new file mode 100644 index 000000000..30bc352a7 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicCompatible.java @@ -0,0 +1,8 @@ +package com.yomahub.liteflow.agent.anthropic; + +public final class AnthropicCompatible { + private AnthropicCompatible() {} + public static AnthropicSpec custom(String configKey, String modelName) { + return new AnthropicSpec(modelName, configKey); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicSpec.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicSpec.java new file mode 100644 index 000000000..c14b0efd9 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicSpec.java @@ -0,0 +1,86 @@ +package com.yomahub.liteflow.agent.anthropic; + +import com.yomahub.liteflow.agent.model.CredentialResolver; +import com.yomahub.liteflow.agent.model.ModelSpec; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.PlatformCredential; +import io.agentscope.core.model.AnthropicChatModel; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.Model; + +import java.util.function.Consumer; + +public class AnthropicSpec extends ModelSpec { + + private final String modelName; + private Integer thinkingBudget; + private Boolean thinkingEnabled; + + /** null 表示走头等平台 (cfg.getAnthropic());非 null 表示走 anthropic-compatible map。 */ + private final String compatibleConfigKey; + + public AnthropicSpec(String modelName) { + this(modelName, null); + } + + public AnthropicSpec(String modelName, String compatibleConfigKey) { + this.modelName = modelName; + this.compatibleConfigKey = compatibleConfigKey; + } + + public AnthropicSpec thinking(Consumer c) { + AnthropicThinking t = new AnthropicThinking(); + c.accept(t); + this.thinkingBudget = t.getBudget(); + this.thinkingEnabled = t.getEnabled(); + return this; + } + + public String getModelName() { return modelName; } + public Integer getThinkingBudget() { return thinkingBudget; } + public Boolean getThinkingEnabled() { return thinkingEnabled; } + + @Override + public Model resolve(AgentConfig cfg) { + PlatformCredential cred; + if (compatibleConfigKey == null) { + cred = CredentialResolver.requireFirstClass( + cfg.getAnthropic(), "liteflow.agent.anthropic"); + } else { + cred = CredentialResolver.requireCompatible( + cfg.getAnthropicCompatible(), compatibleConfigKey, + "liteflow.agent.anthropic-compatible"); + } + + AnthropicChatModel.Builder builder = AnthropicChatModel.builder() + .apiKey(cred.getApiKey()) + .modelName(modelName); + if (cred.getBaseUrl() != null && !cred.getBaseUrl().isBlank()) { + builder.baseUrl(cred.getBaseUrl()); + } + GenerateOptions options = buildGenerateOptions(); + if (options != null) { + builder.defaultOptions(options); + } + if (getStream() != null) { + builder.stream(getStream()); + } + return builder.build(); + } + + private GenerateOptions buildGenerateOptions() { + if (getTemperature() == null && getTopP() == null && getTopK() == null + && getMaxTokens() == null && getSeed() == null + && thinkingBudget == null) { + return null; + } + GenerateOptions.Builder b = GenerateOptions.builder(); + if (getTemperature() != null) b.temperature(getTemperature()); + if (getTopP() != null) b.topP(getTopP()); + if (getTopK() != null) b.topK(getTopK()); + if (getMaxTokens() != null) b.maxTokens(getMaxTokens()); + if (getSeed() != null) b.seed(getSeed()); + if (thinkingBudget != null) b.thinkingBudget(thinkingBudget); + return b.build(); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicThinking.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicThinking.java new file mode 100644 index 000000000..db9a43891 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicThinking.java @@ -0,0 +1,13 @@ +package com.yomahub.liteflow.agent.anthropic; + +/** Anthropic 平台 thinking 子构建器。沿用 Anthropic 原生术语。 */ +public final class AnthropicThinking { + private Integer budget; + private Boolean enabled; + + public AnthropicThinking budget(int tokens) { this.budget = tokens; return this; } + public AnthropicThinking enabled(boolean v) { this.enabled = v; return this; } + + public Integer getBudget() { return budget; } + public Boolean getEnabled() { return enabled; } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicEntryTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicEntryTest.java new file mode 100644 index 000000000..60746922c --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicEntryTest.java @@ -0,0 +1,64 @@ +package com.yomahub.liteflow.agent.anthropic; + +import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.PlatformCredential; +import io.agentscope.core.model.AnthropicChatModel; +import io.agentscope.core.model.Model; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AnthropicEntryTest { + + @Test + void buildsAnthropicChatModel() { + AgentConfig cfg = new AgentConfig(); + cfg.getAnthropic().setApiKey("ak-test"); + + Model model = Anthropic.of("claude-sonnet-4-6") + .temperature(0.5) + .thinking(t -> t.budget(2000).enabled(true)) + .resolve(cfg); + + assertTrue(model instanceof AnthropicChatModel); + assertEquals("claude-sonnet-4-6", ((AnthropicChatModel) model).getModelName()); + } + + @Test + void throwsWhenApiKeyMissing() { + AgentConfig cfg = new AgentConfig(); + AgentConfigException ex = assertThrows(AgentConfigException.class, + () -> Anthropic.of("claude-sonnet-4-6").resolve(cfg)); + assertTrue(ex.getMessage().contains("liteflow.agent.anthropic.api-key")); + } + + @Test + void compatibleResolvesFromAnthropicCompatibleMap() { + AgentConfig cfg = new AgentConfig(); + PlatformCredential c = new PlatformCredential(); + c.setApiKey("anc-key"); + c.setBaseUrl("https://my.anthropic-mirror/v1"); + cfg.getAnthropicCompatible().put("mirror", c); + + Model model = AnthropicCompatible.custom("mirror", "claude-haiku") + .resolve(cfg); + assertEquals("claude-haiku", ((AnthropicChatModel) model).getModelName()); + } + + @Test + void compatibleThrowsWhenKeyMissing() { + AgentConfig cfg = new AgentConfig(); + AgentConfigException ex = assertThrows(AgentConfigException.class, + () -> AnthropicCompatible.custom("mirror", "x").resolve(cfg)); + assertTrue(ex.getMessage().contains("anthropic-compatible.mirror")); + } + + @Test + void thinkingBuilderStoresBudgetAndEnabled() { + AnthropicSpec spec = Anthropic.of("claude-sonnet-4-6") + .thinking(t -> t.budget(1500).enabled(true)); + assertEquals(1500, spec.getThinkingBudget()); + assertEquals(Boolean.TRUE, spec.getThinkingEnabled()); + } +}