feat(react-agent-anthropic): add Anthropic entry and AnthropicSpec

Anthropic.of(modelName) and AnthropicCompatible.custom(configKey,
modelName) return AnthropicSpec, with a thinking sub-builder using
Anthropic's native budget/enabled terms.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-04-29 18:22:27 +08:00
parent 679dc33ca2
commit 994cbd2a25
5 changed files with 179 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<AnthropicSpec> {
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<AnthropicThinking> 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();
}
}

View File

@@ -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; }
}

View File

@@ -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());
}
}