From c20083338e3689034eb17cfc959d410338a3b5a0 Mon Sep 17 00:00:00 2001 From: orcasjh Date: Sat, 15 Aug 2026 04:25:34 +0000 Subject: [PATCH] feat: add example-orcarouter for OrcaRouter LLM routing gateway Add a new standalone Spring Boot example under forest-examples that calls the OrcaRouter OpenAI-compatible API through the Forest declarative HTTP client, mirroring the existing example-deepseek integration. Co-Authored-By: Claude --- forest-examples/example-orcarouter/README.md | 31 +++++++ forest-examples/example-orcarouter/pom.xml | 88 +++++++++++++++++++ .../example/OrcaRouterExampleApplication.java | 50 +++++++++++ .../forest/example/client/OrcaRouter.java | 18 ++++ .../interceptor/OrcaRouterInterceptor.java | 19 ++++ .../example/model/OrcaRouterResult.java | 35 ++++++++ .../example/model/OrcaRouterResultChoice.java | 11 +++ .../example/model/OrcaRouterResultDelta.java | 15 ++++ .../src/main/resources/application.yml | 8 ++ 9 files changed, 275 insertions(+) create mode 100644 forest-examples/example-orcarouter/README.md create mode 100644 forest-examples/example-orcarouter/pom.xml create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/OrcaRouterExampleApplication.java create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/client/OrcaRouter.java create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/interceptor/OrcaRouterInterceptor.java create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResult.java create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultChoice.java create mode 100644 forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultDelta.java create mode 100644 forest-examples/example-orcarouter/src/main/resources/application.yml diff --git a/forest-examples/example-orcarouter/README.md b/forest-examples/example-orcarouter/README.md new file mode 100644 index 00000000..4be78919 --- /dev/null +++ b/forest-examples/example-orcarouter/README.md @@ -0,0 +1,31 @@ + +Spring Boot 环境下接入 [OrcaRouter](https://www.orcarouter.ai) 的示例 +==== + +本案例使用`forest-spring-boot-starter`包发送HTTP请求来调用 [OrcaRouter](https://www.orcarouter.ai) 的接口 + +[OrcaRouter](https://www.orcarouter.ai) 是一个 OpenAI 兼容的 LLM 路由网关,通过统一的 `https://api.orcarouter.ai/v1` 接入点路由到多个上游模型。 + +使用 Fastjson2 进行JSON序列化/反序列化 + +#### 项目结构介绍 + +本案例包含以下几个类: + +- OrcaRouterExampleApplication: 项目启动类 +- OrcaRouter: 接入 OrcaRouter 的 Forest 声明式接口 +- OrcaRouterResult: 用于接受 OrcaRouter 响应结果的数据类 +- OrcaRouterResultChoice: 用于接受 OrcaRouter 响应结果中文本内容的数据类 +- OrcaRouterResultDelta: 用于接受 OrcaRouter 增量更新内容的类 + +#### 配置 + +```yaml +forest: + connect-timeout: 10000 # HTTP请求连接超时时间 + read-timeout: 3600000 # HTTP请求读取超时时间 + variables: # 自定义变量: + baseUrl: https://api.orcarouter.ai/v1 + apiKey: YOUR_API_KEY # 你的 OrcaRouter 的 API KEY (sk-orca- 开头) + model: orcarouter/auto # 由 OrcaRouter 自动路由到最佳模型 +``` diff --git a/forest-examples/example-orcarouter/pom.xml b/forest-examples/example-orcarouter/pom.xml new file mode 100644 index 00000000..55fcc1ce --- /dev/null +++ b/forest-examples/example-orcarouter/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + com.dtflys.forest + example-orcarouter + 1.0.0 + + + 1.8 + true + 1.7.2 + 2.0.53 + 1.18.30 + + + + org.springframework.boot + spring-boot-starter-parent + 2.1.8.RELEASE + + + + + + + org.springframework.boot + spring-boot-starter + + + + com.dtflys.forest + forest-spring-boot-starter + ${forest.version} + + + + com.dtflys.forest + forest-jaxb + ${forest.version} + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/OrcaRouterExampleApplication.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/OrcaRouterExampleApplication.java new file mode 100644 index 00000000..6162df89 --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/OrcaRouterExampleApplication.java @@ -0,0 +1,50 @@ +package com.dtflys.forest.example; + +import com.dtflys.forest.example.client.OrcaRouter; +import com.dtflys.forest.example.model.OrcaRouterResult; +import com.dtflys.forest.sse.SSELinesMode; +import com.dtflys.forest.utils.StringUtils; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import javax.annotation.Resource; + +@SpringBootApplication +public class OrcaRouterExampleApplication implements CommandLineRunner { + + @Resource + private OrcaRouter orcaRouter; + + /** + * 通过 OrcaRouter 路由网关发送聊天请求 + * + * @param message 发送给 OrcaRouter 的消息 + */ + private void completions(String message) { + orcaRouter.completions(message) + .setOnMessage(event -> { + try { + OrcaRouterResult result = event.value(OrcaRouterResult.class); + String content = result.content(); + System.out.print(StringUtils.isNotBlank(content) ? content : ""); + } catch (Exception e) { + } + }) + .listen(SSELinesMode.SINGLE_LINE); + } + + @Override + public void run(String... args) { + completions("1+1等于几?"); + } + + public static void main(String[] args) { + try { + SpringApplication.run(OrcaRouterExampleApplication.class, args); + } catch (Throwable th) { + th.printStackTrace(); + } + } + +} diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/client/OrcaRouter.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/client/OrcaRouter.java new file mode 100644 index 00000000..f5291383 --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/client/OrcaRouter.java @@ -0,0 +1,18 @@ +package com.dtflys.forest.example.client; + +import com.dtflys.forest.annotation.BaseRequest; +import com.dtflys.forest.annotation.Post; +import com.dtflys.forest.annotation.Var; +import com.dtflys.forest.example.interceptor.OrcaRouterInterceptor; +import com.dtflys.forest.http.ForestSSE; + +@BaseRequest(baseURL = "{baseUrl}", interceptor = OrcaRouterInterceptor.class) +public interface OrcaRouter { + + @Post( + url = "/chat/completions", + contentType = "application/json", + headers = "Authorization: Bearer {apiKey}", + data = "{\"messages\":[{\"content\":\"{content}\",\"role\":\"user\"}],\"model\":\"{model}\",\"stream\":true}") + ForestSSE completions(@Var("content") String content); +} diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/interceptor/OrcaRouterInterceptor.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/interceptor/OrcaRouterInterceptor.java new file mode 100644 index 00000000..41d371a7 --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/interceptor/OrcaRouterInterceptor.java @@ -0,0 +1,19 @@ +package com.dtflys.forest.example.interceptor; + +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.http.ForestResponse; +import com.dtflys.forest.interceptor.ResponseResult; +import com.dtflys.forest.interceptor.SSEInterceptor; + +public class OrcaRouterInterceptor implements SSEInterceptor { + + @Override + public ResponseResult onResponse(ForestRequest request, ForestResponse response) { + if (response.isError()) { + System.out.println("服务端繁忙,请稍后再试"); + return success(); + } + return proceed(); + } + +} diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResult.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResult.java new file mode 100644 index 00000000..be104e67 --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResult.java @@ -0,0 +1,35 @@ +package com.dtflys.forest.example.model; + +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +import java.util.List; + +@Data +public class OrcaRouterResult { + + private String id; + + private String object; + + private Integer created; + + private String model; + + @JSONField(name = "system_fingerprint") + private String systemFingerprint; + + private List choices; + + public String content() { + List choices = getChoices(); + if (choices != null && !choices.isEmpty()) { + JSONObject chooseJson = choices.get(0); + OrcaRouterResultChoice choice = chooseJson.toJavaObject(OrcaRouterResultChoice.class); + return choice.getDelta().getContent(); + } + return ""; + } + +} diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultChoice.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultChoice.java new file mode 100644 index 00000000..d393cb3b --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultChoice.java @@ -0,0 +1,11 @@ +package com.dtflys.forest.example.model; + +import lombok.Data; + +@Data +public class OrcaRouterResultChoice { + + private Integer index; + + private OrcaRouterResultDelta delta; +} diff --git a/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultDelta.java b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultDelta.java new file mode 100644 index 00000000..aa321c5a --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/java/com/dtflys/forest/example/model/OrcaRouterResultDelta.java @@ -0,0 +1,15 @@ +package com.dtflys.forest.example.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class OrcaRouterResultDelta { + + private String content; + + @JSONField(name = "reasoning_content") + private String reasoningContent; +} diff --git a/forest-examples/example-orcarouter/src/main/resources/application.yml b/forest-examples/example-orcarouter/src/main/resources/application.yml new file mode 100644 index 00000000..a204c00b --- /dev/null +++ b/forest-examples/example-orcarouter/src/main/resources/application.yml @@ -0,0 +1,8 @@ + +forest: + connect-timeout: 10000 + read-timeout: 3600000 + variables: + baseUrl: https://api.orcarouter.ai/v1 + apiKey: YOUR_API_KEY + model: orcarouter/auto -- Gitee