Pre Merge pull request !76 from orcasjh/feat-orcarouter

pull/76/MERGE
orcasjh 4 weeks ago committed by Gitee
commit fef96a4f32
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F

@ -1280,7 +1280,7 @@ public void demoJobHandler() throws Exception {
- d、commandJobHandler通用命令行任务Handler业务方只需要提供命令行即可命令及参数之间通过空格隔开如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据;
#### 原生内置Bean模式任务AI执行器
为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式 AI任务Handler与spring-ai、ollama、dify等集成打通支持快速开发AI类任务如下
为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式 AI任务Handler与spring-ai、ollama、OrcaRouter、dify等集成打通支持快速开发AI类任务如下
**AI执行器说明**
- AppNamexxl-job-executor-sample-ai
@ -1317,6 +1317,15 @@ public void demoJobHandler() throws Exception {
}
```
- d、orcaRouterJobHandler [OrcaRouter](https://www.orcarouter.ai)任务OpenAI兼容的LLM路由网关支持自定义prompt、input、model等输入信息。示例任务入参如下
```
{
"input": "{输入信息,必填信息}",
"prompt": "{模型prompt可选信息}",
"model": "{模型如orcarouter/auto可选信息}"
}
```
- 依赖1参考 [Ollama本地化部署大模型](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8Ollama%E6%9C%AC%E5%9C%B0%E5%8C%96%E9%83%A8%E7%BD%B2DeepSeek.md) 执行器示例部署“qwen2.5:1.5b”模型,也可自定选择其他模型版本。
- 依赖2参考 [使用DeepSeek与Dify搭建AI助手](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8DeepSeek%E4%B8%8EDify%E6%90%AD%E5%BB%BAAI%E5%8A%A9%E6%89%8B.md)执行器示例新建Dify DifyWork应用并在开始节点添加“input”参数可结合实际情况调整。
- 依赖3启动示例 “AI执行器” 相关配置文件说明如下:
@ -1333,6 +1342,11 @@ spring.ai.ollama.chat.options.temperature=0.8
dify.base-url=http://localhost/v1
// dify api-key选择相关 workflow 应用并进入 “访问API” 页面,右上角 “API 密钥” 入口获取 api-key。
dify.api-key={自行获取并修改}
// orcarouter 配置OrcaRouter 是 OpenAI 兼容的 LLM 路由网关,官网 https://www.orcarouter.ai
spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1
spring.ai.orcarouter.api-key={自行获取并修改}
spring.ai.orcarouter.model=orcarouter/auto
```
### 3.3 GLUE模式(Java)

@ -0,0 +1,41 @@
package com.xxl.job.executor.config;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* OrcaRouter ChatModel
*
* OrcaRouter OpenAI LLM ChatModel Spring AI OpenAiChatModel
* base-url OrcaRouter https://api.orcarouter.ai即可接入。
*
* @author orcasjh
*/
public class OrcaRouterChatModel implements ChatModel {
private final OpenAiChatModel delegate;
public OrcaRouterChatModel(OpenAiChatModel delegate) {
this.delegate = delegate;
}
@Override
public ChatResponse call(Prompt prompt) {
return delegate.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return delegate.stream(prompt);
}
@Override
public ChatOptions getOptions() {
return delegate.getOptions();
}
}

@ -0,0 +1,62 @@
package com.xxl.job.executor.config;
import com.openai.client.OpenAIClient;
import com.openai.client.OpenAIClientAsync;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.setup.OpenAiSetup;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.List;
/**
* OrcaRouter
*
* OrcaRouterhttps://www.orcarouter.ai是 OpenAI 兼容的 LLM 路由网关,
* Spring AI OpenAiChatModel base-url OrcaRouter
*
* application.properties
* <pre>
* spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1
* spring.ai.orcarouter.api-key={}
* spring.ai.orcarouter.model=orcarouter/auto
* </pre>
*
* @author orcasjh
*/
@Configuration
public class OrcaRouterConfig {
@Bean
public OrcaRouterChatModel orcaRouterChatModel(
@Value("${spring.ai.orcarouter.base-url:https://api.orcarouter.ai/v1}") String baseUrl,
@Value("${spring.ai.orcarouter.api-key:}") String apiKey,
@Value("${spring.ai.orcarouter.model:orcarouter/auto}") String model) {
// 构建 OrcaRouter OpenAI 兼容客户端(同步 + 异步)
ObservationRegistry observationRegistry = ObservationRegistry.create();
MeterRegistry meterRegistry = new SimpleMeterRegistry();
OpenAIClient client = OpenAiSetup.setupSyncClient(
baseUrl, apiKey, null, null, null, null,
false, false, model, Duration.ofSeconds(30), 2,
null, null, observationRegistry, meterRegistry, List.of());
OpenAIClientAsync clientAsync = OpenAiSetup.setupAsyncClient(
baseUrl, apiKey, null, null, null, null,
false, false, model, Duration.ofSeconds(30), 2,
null, null, observationRegistry, meterRegistry, List.of());
OpenAiChatModel inner = OpenAiChatModel.builder()
.openAiClient(client)
.openAiClientAsync(clientAsync)
.build();
return new OrcaRouterChatModel(inner);
}
}

@ -2,6 +2,7 @@ package com.xxl.job.executor.jobhandler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.executor.config.OrcaRouterChatModel;
import com.xxl.tool.json.GsonTool;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.DifyWorkflowClient;
@ -17,6 +18,7 @@ import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@ -36,6 +38,8 @@ public class AIXxlJob {
private OllamaChatModel ollamaChatModel;
@Resource
private OpenAiChatModel openAiChatModel;
@Resource
private OrcaRouterChatModel orcaRouterChatModel;
/**
* 1ollama Chat
@ -345,4 +349,108 @@ public class AIXxlJob {
}
// --------------------------------- orcarouter ---------------------------------
/**
* 4OrcaRouter
*
* OrcaRouter OpenAI LLM https://www.orcarouter.ai
* spring-ai OpenAI base-url https://api.orcarouter.ai。
*
* OrcaRouterParam
* <pre>
* {
* "input": "{输入信息,必填信息}",
* "prompt": "{模型prompt可选信息}",
* "model": "{模型,可选,默认 orcarouter/auto}"
* }
* </pre>
*/
@XxlJob("orcaRouterJobHandler")
public void orcaRouterJobHandler() {
// param
String param = XxlJobHelper.getJobParam();
if (param == null || param.trim().isEmpty()) {
XxlJobHelper.log("param is empty.");
XxlJobHelper.handleFail();
return;
}
// orcarouter param
OrcaRouterParam orcaRouterParam = null;
try {
orcaRouterParam = GsonTool.fromJson(param, OrcaRouterParam.class);
if (orcaRouterParam.getPrompt()==null || orcaRouterParam.getPrompt().isBlank()) {
orcaRouterParam.setPrompt("你是一个研发工程师,擅长解决技术类问题。");
}
if (orcaRouterParam.getInput() == null || orcaRouterParam.getInput().isBlank()) {
XxlJobHelper.log("input is empty.");
XxlJobHelper.handleFail();
return;
}
if (orcaRouterParam.getModel()==null || orcaRouterParam.getModel().isBlank()) {
orcaRouterParam.setModel("orcarouter/auto");
}
} catch (Exception e) {
XxlJobHelper.log(new RuntimeException("OrcaRouterParam parse error", e));
XxlJobHelper.handleFail();
return;
}
// input
XxlJobHelper.log("<br><br><b>【Input】: " + orcaRouterParam.getInput()+ "</b><br><br>");
// build chat-client
ChatClient orcaRouterChatClient = ChatClient
.builder(orcaRouterChatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build())
.defaultAdvisors(SimpleLoggerAdvisor.builder().build())
.defaultOptions(OpenAiChatOptions.builder().model(orcaRouterParam.getModel()))
.build();
// call orcarouter
String response = orcaRouterChatClient
.prompt(orcaRouterParam.getPrompt())
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "conversationId-default"))
.user(orcaRouterParam.getInput())
.call()
.content();
XxlJobHelper.log("<br><br><b>【Output】: " + response + "</b><br><br>");
}
private static class OrcaRouterParam {
private String input;
private String prompt;
private String model;
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public String getPrompt() {
return prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}
}

@ -46,4 +46,8 @@ spring.ai.ollama.base-url=http://localhost:11434
### spring-ai: openai (for openclaw)
spring.ai.openai.base-url=http://127.0.0.1:18789
spring.ai.openai.api-key=xxxxxx
### spring-ai: orcarouter (OpenAI 兼容 LLM 路由网关,官网 https://www.orcarouter.ai)
spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1
spring.ai.orcarouter.api-key={自行获取并修改}
spring.ai.orcarouter.model=orcarouter/auto

@ -0,0 +1,48 @@
package com.xxl.job.executor.test.orcarouter;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.executor.config.OrcaRouterChatModel;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@SpringBootTest
public class OrcaRouterLiveTest {
private static final Logger logger = LoggerFactory.getLogger(OrcaRouterLiveTest.class);
// ignore
@MockitoBean
private XxlJobSpringExecutor xxlJobSpringExecutor;
@Resource
private OrcaRouterChatModel orcaRouterChatModel;
@Test
public void chatTest() {
String prompt = "你是一个研发工程师,擅长解决技术类问题。";
String input = "请简单介绍一下你自己";
// build chat-client
ChatClient orcaRouterChatClient = ChatClient
.builder(orcaRouterChatModel)
.defaultOptions(OpenAiChatOptions.builder().model("orcarouter/auto"))
.build();
// call orcarouter
String response = orcaRouterChatClient
.prompt(prompt)
.user(input)
.call()
.content();
logger.info("input: {}", input);
logger.info("response: {}", response);
}
}
Loading…
Cancel
Save