feat(react-agent-openai): add OpenAI entry and OpenAISpec

OpenAI.of(modelName) returns an OpenAISpec exposing reasoningEffort,
frequencyPenalty, and presencePenalty in addition to the common
ModelSpec setters; resolve() reads liteflow.agent.openai credential.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-04-29 18:11:54 +08:00
parent b40ec52632
commit 5cc43acff2
3 changed files with 139 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.yomahub.liteflow.agent.openai;
/**
* OpenAI 官方 API 入口。credential 来源:{@code liteflow.agent.openai}。
*/
public final class OpenAI {
private OpenAI() {}
public static OpenAISpec of(String modelName) {
return new OpenAISpec(modelName);
}
}

View File

@@ -0,0 +1,81 @@
package com.yomahub.liteflow.agent.openai;
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.GenerateOptions;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.OpenAIChatModel;
/**
* OpenAI 系(含 OpenAI 兼容族)通用 spec。
* 暴露 OpenAI 平台特有的 reasoningEffort / frequencyPenalty / presencePenalty 等参数。
*/
public class OpenAISpec extends ModelSpec<OpenAISpec> {
private final String modelName;
private String reasoningEffort;
private Double frequencyPenalty;
private Double presencePenalty;
public OpenAISpec(String modelName) {
this.modelName = modelName;
}
public OpenAISpec reasoningEffort(String level) { this.reasoningEffort = level; return this; }
public OpenAISpec frequencyPenalty(double v) { this.frequencyPenalty = v; return this; }
public OpenAISpec presencePenalty(double v) { this.presencePenalty = v; return this; }
public String getModelName() { return modelName; }
public String getReasoningEffort() { return reasoningEffort; }
public Double getFrequencyPenalty() { return frequencyPenalty; }
public Double getPresencePenalty() { return presencePenalty; }
@Override
public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getOpenai(), "liteflow.agent.openai");
return buildModel(cred.getApiKey(), cred.getBaseUrl());
}
/** 子类OpenAICompatibleSpec可覆盖以提供不同 baseUrl / apiKey 来源。 */
protected Model buildModel(String apiKey, String baseUrl) {
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
.apiKey(apiKey)
.modelName(modelName);
if (baseUrl != null && !baseUrl.isBlank()) {
builder.baseUrl(baseUrl);
}
GenerateOptions options = buildGenerateOptions();
if (options != null) {
builder.generateOptions(options);
}
if (getStream() != null) {
builder.stream(getStream());
}
return builder.build();
}
/** 把共性 + 个性参数装配成 GenerateOptions全部为 null 时返回 null。 */
protected GenerateOptions buildGenerateOptions() {
if (getTemperature() == null && getTopP() == null && getTopK() == null
&& getMaxTokens() == null && getSeed() == null
&& getCacheControl() == null
&& reasoningEffort == null
&& frequencyPenalty == null && presencePenalty == 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 (getCacheControl() != null) b.cacheControl(getCacheControl());
if (reasoningEffort != null) b.reasoningEffort(reasoningEffort);
if (frequencyPenalty != null) b.frequencyPenalty(frequencyPenalty);
if (presencePenalty != null) b.presencePenalty(presencePenalty);
return b.build();
}
}

View File

@@ -0,0 +1,45 @@
package com.yomahub.liteflow.agent.openai;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.OpenAIChatModel;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OpenAIEntryTest {
@Test
void buildsOpenAIChatModelWithGivenModelName() {
AgentConfig cfg = new AgentConfig();
cfg.getOpenai().setApiKey("sk-test");
OpenAISpec spec = OpenAI.of("gpt-4o").temperature(0.7);
Model model = spec.resolve(cfg);
assertTrue(model instanceof OpenAIChatModel);
assertEquals("gpt-4o", ((OpenAIChatModel) model).getModelName());
}
@Test
void throwsWhenApiKeyMissing() {
AgentConfig cfg = new AgentConfig(); // openai credential not set
OpenAISpec spec = OpenAI.of("gpt-4o");
AgentConfigException ex = assertThrows(AgentConfigException.class,
() -> spec.resolve(cfg));
assertTrue(ex.getMessage().contains("liteflow.agent.openai.api-key"));
}
@Test
void specSettersReturnSubclassType() {
// 编译期断言fluent 链返回 OpenAISpec能链式调用 OpenAI 特有方法
OpenAISpec spec = OpenAI.of("gpt-4o")
.temperature(0.7)
.topP(0.9)
.reasoningEffort("high")
.frequencyPenalty(0.1)
.presencePenalty(0.2);
assertNotNull(spec);
}
}