From 15823d0157fd72066deb546259486467d2e90a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=97=E9=80=94=E4=B9=90=E6=97=85?= <3296249574@qq.com> Date: Mon, 19 Oct 2026 15:43:42 +0800 Subject: [PATCH] fix: classify detected abusive traffic 403 and lock public API signatures --- README.md | 9 ++++++ build.gradle.kts | 2 +- .../OpenCodeZenUpstreamDiagnostics.java | 3 +- .../OpenCodeZenPublicApiContractTest.java | 31 +++++++++++++++++++ .../OpenCodeZenResponseParityTest.java | 22 +++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenPublicApiContractTest.java diff --git a/README.md b/README.md index 240ab6f..fd28973 100644 --- a/README.md +++ b/README.md @@ -217,3 +217,12 @@ OpenCodeZenSseEventReader.read(streamResponse.body(), event -> { ``` 注意:上述两种 BodyHandler 为演示分类和流式解析的独立示例。实际运行时先检查 HTTP 状态码,403/429 不要进入 SSE 解析。`200 /models` 也不代表推理授权成功。 + + +## 1.3.3: 403 诊断兼容性修复 + +- `OpenCodeZenUpstreamDiagnostics` 现在也识别实际观测到的 `detected abusive traffic` / `abusive traffic` 文案。与旧版 `Detect Abuse Traffic` 一样归类为 `ABUSE_REJECTED`;它不是可自动重试的错误。 +- 添加按真实服务端措辞构造的分类回归测试,以及公开 API 编译契约测试;没有改变请求 Header、工具声明、代理出口或推理逻辑。 +- 1.3.2 源码中 `OpenCodeZenRequestFactory.newEphemeralInferenceRequest(URI, ZenModelProtocol, boolean, String, Map)` 和 `OpenCodeZenProtocolPolicy.applyRequiredEnvelope(Map, ZenModelProtocol, boolean)` 都已存在。若只在恢复本地改动后报 `cannot find symbol`,请检查本地测试文件的导入、接收对象类型、参数类型,以及测试使用的依赖/模块 classpath。不能仅由该错误断定上游方法被删除。 +- 如需同步此修复到已有本地两文件的相同未提交改动,请先备份、提交或 stash 当前工作区;普通 `git pull` 可能因工作区冲突而中止。不要使用 `git reset --hard` 丢弃用户改动。 + diff --git a/build.gradle.kts b/build.gradle.kts index 8fbb7d7..8a2b5ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } group = "com.sharedlibs" -version = "1.3.2" +version = "1.3.3" subprojects { group = rootProject.group diff --git a/opencode-zen-spring-boot-autoconfigure/src/main/java/com/sharedlibs/opencodezen/OpenCodeZenUpstreamDiagnostics.java b/opencode-zen-spring-boot-autoconfigure/src/main/java/com/sharedlibs/opencodezen/OpenCodeZenUpstreamDiagnostics.java index 87fd10f..5ebc05f 100644 --- a/opencode-zen-spring-boot-autoconfigure/src/main/java/com/sharedlibs/opencodezen/OpenCodeZenUpstreamDiagnostics.java +++ b/opencode-zen-spring-boot-autoconfigure/src/main/java/com/sharedlibs/opencodezen/OpenCodeZenUpstreamDiagnostics.java @@ -34,7 +34,8 @@ public static Outcome classify(int statusCode, byte[] responseBody) { String message = new String(bytes, 0, length, StandardCharsets.UTF_8) .toLowerCase(Locale.ROOT); if (message.contains("detect abuse traffic") - || message.contains("abuse traffic")) { + || message.contains("abuse traffic") + || message.contains("abusive traffic")) { return Outcome.ABUSE_REJECTED; } if (message.contains("freetiererror") || message.contains("free tier")) { diff --git a/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenPublicApiContractTest.java b/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenPublicApiContractTest.java new file mode 100644 index 0000000..c9b6468 --- /dev/null +++ b/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenPublicApiContractTest.java @@ -0,0 +1,31 @@ +package com.sharedlibs.opencodezen; + +import com.sharedlibs.opencodezen.autoconfigure.OpenCodeZenRequestFactory; +import org.junit.jupiter.api.Test; +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.LinkedHashMap; +import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This test intentionally compiles against public API signatures as a guard + * against consumer API regressions. It performs no network requests. + */ +class OpenCodeZenPublicApiContractTest { + @Test + void methodsReportedMissingAreActuallyExposedByTheCurrentSource() { + OpenCodeZenRequestFactory factory = + new OpenCodeZenRequestFactory(OpenCodeZenProtocolPolicy.USER_AGENT); + HttpRequest request = factory.newEphemeralInferenceRequest( + URI.create(OpenCodeZenProtocolPolicy.DEFAULT_BASE_URL + "/chat/completions"), + ZenModelProtocol.CHAT, true, null, + Map.of("model", "big-pickle")); + assertThat(request.method()).isEqualTo("POST"); + + Map payload = new LinkedHashMap<>(); + payload.put("model", "big-pickle"); + OpenCodeZenProtocolPolicy.applyRequiredEnvelope(payload, ZenModelProtocol.CHAT, true); + assertThat(payload).containsEntry("stream", true).containsKey("tools"); + } +} diff --git a/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenResponseParityTest.java b/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenResponseParityTest.java index 65b4f3f..6d45e9e 100644 --- a/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenResponseParityTest.java +++ b/opencode-zen-spring-boot-autoconfigure/src/test/java/com/sharedlibs/opencodezen/OpenCodeZenResponseParityTest.java @@ -19,6 +19,28 @@ void detectsExplicitTrafficRejectionAndNeverRetries() { assertThat(OpenCodeZenUpstreamDiagnostics.shouldRetryAutomatically(result)).isFalse(); } + @Test + void detectsAbusiveTrafficVariantBeforeGenericFreeTierError() { + // This wording was observed on a rejected network egress, not a JSON schema error. + String[] upstreamBodies = { + "{\"error\":{\"type\":\"FreeTierError\",\"message\":\"detected abusive traffic\"}}", + "{\"error\":{\"type\":\"FreeTierError\",\"message\":\"Forbidden: Detected Abusive Traffic\"}}", + "FreeTierError: current network has abusive traffic", + "Forbidden, OpenCode Free Model Gateway Detect Abuse Traffic" + }; + for (String upstreamBody : upstreamBodies) { + var outcome = OpenCodeZenUpstreamDiagnostics.classify( + 403, upstreamBody.getBytes(StandardCharsets.UTF_8)); + assertThat(outcome) + .as("403 error classification for %s", upstreamBody) + .isEqualTo(OpenCodeZenUpstreamDiagnostics.Outcome.ABUSE_REJECTED); + assertThat(OpenCodeZenUpstreamDiagnostics.shouldRetryAutomatically(outcome)).isFalse(); + } + assertThat(OpenCodeZenUpstreamDiagnostics.classify(403, + "FreeTierError".getBytes(StandardCharsets.UTF_8))) + .isEqualTo(OpenCodeZenUpstreamDiagnostics.Outcome.FREE_TIER_REJECTED); + } + @Test void distinguishesFreeTierAndOtherHttpErrorsWithoutStoringBody() { assertThat(OpenCodeZenUpstreamDiagnostics.classify(403,