Skip to content

Commit d93dbf9

Browse files
authored
Merge pull request #5 from QoderAI/codex/java-client-extensions
feat(java): complete Forward fields and add client extensions
2 parents 1c19623 + 5bbdf48 commit d93dbf9

28 files changed

Lines changed: 1844 additions & 29 deletions

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- Public Java 8 HTTP transport and per-attempt interceptor contracts for custom networking and tracing, without exposing OkHttp types. Cancellation reaches downstream transport futures; SSE bodies remain streaming. Storage downloads use the transport without API credentials or interceptors. WebSockets and SA Key exchange retain their existing transports.
8+
- Synchronous/asynchronous `request(ApiRequest, RequestOptions)` for additional JSON endpoints, reusing authentication, retries, cancellation and response metadata with relative path segments.
9+
- Synchronous/asynchronous `withOptions(...)` to derive independently closeable clients while retaining resolved authentication and unspecified settings. Caller-supplied credentials and transports remain caller-owned.
10+
11+
### Fixed
12+
13+
- Add typed image `file_id`, Memory Store creation-mount `access`, Schedule/Run `initial_events_version` and Pairing `created_at` / `updated_at` fields. Missing versions remain unknown rather than becoming zero.
14+
515
## [0.1.0] - 2026-10-21
616

717
First stable release of the Java SDK for the Forward API, using `com.qoder.sdk:qoder-cloud-agents-sdk:0.1.0`.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ Microphone capture and audio playback belong to the application. Connections do
227227

228228
- [Runnable examples](examples/README.md): shared PAT/SA setup, resource lifecycles, Session SSE and Realtime.
229229
- [API coverage](docs/forward-api-coverage.md): supported operations and guides for each resource.
230+
- [Client extensions (unreleased)](docs/client-configuration.md#client-extensions-unreleased): custom HTTP transport, interceptors, generic JSON requests and configuration copies.
230231
- [Files and downloads](docs/environments-files.md) and [Drive, Usage and SAT management](docs/usage-sat-drive.md).
231232
- [Live testing](docs/live-testing.md): opt-in CN/Global verification and coverage boundaries.
232233

‎docs/channels.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ DeletedChannelPairing removed = client.channelPairings().delete(pairing.id());
102102

103103
Use the returned Pairing ID for revocation and Schedule sinks. Current API supports completion by pending Pairing ID as well as code; code-only older SDK types are not copied. An ID-based completion can be retried against the same binding without depending on code expiry. Used/expired codes cannot be treated as fresh pairing requests. Permission, ownership and concurrent-change errors remain server decisions.
104104

105+
The development SDK also exposes nullable `createdAt()` and `updatedAt()` strings on `ChannelPairing`, preserving timestamps returned by the service. These getters are not included in the published `0.1.0` JAR.
106+
105107
`delete` removes the Channel connection; it is distinct from `channelPairings().delete`, which revokes one binding. Already absent/unpaired bindings can return `deleted=true`. SDK keeps the declared deletion response and accepts an empty 204 without inventing a JSON body; use `withResponse()` for metadata in that case.
106108

107109
## Lists and examples

‎docs/client-configuration.md‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,75 @@ com.qoder.cloudagents.core.ApiResponse<com.qoder.cloudagents.forward.Template> r
8787
- Models provide `requestId()`, `field(name)` for unknown fields and `rawJson()`. Unknown fields and numeric precision are preserved; invalid typed field access raises `ResponseValidationException`. Request metadata is separate from the JSON body.
8888
- Template list parameters support `limit`, `status`, `afterId` or `beforeId`. Use `page.data()` for one page, `getNextPage()` / `getNextPageAsync()` for manual pagination, or `for (Template template : page)` for lazy synchronous iteration. Server order is retained; filters and request options follow subsequent pages. Check `hasNextPage()` before fetching; an exhausted page or repeated cursor raises `PaginationException`.
8989

90-
## Template configuration
90+
## Client extensions (unreleased)
91+
92+
The following client extensions are on the development branch and are not included in the published `0.1.0` JAR. Build and install the local SDK to try them.
93+
94+
### Derived clients
95+
96+
```java
97+
try (ForwardClient slow = client.withOptions(options -> options
98+
.timeout(java.time.Duration.ofSeconds(60))
99+
.maxRetries(1)
100+
.defaultHeader("X-Trace-Source", "background-job"))) {
101+
slow.models().list();
102+
}
103+
```
104+
105+
`withOptions` retains the resolved BaseURL, authentication, headers, interceptors and custom transport unless changed. It does not reread environment credentials. Header setters replace matching names case-insensitively; interceptors append in registration order. Use `clearDefaultHeaders()` or `clearInterceptors()` to replace those collections. Per-request options still take precedence. `client.async().withOptions(...)` creates an independently owned async client.
106+
107+
Copies have separate default connection pools, retry timers and in-flight request lifecycles. Closing either client does not close the other. Custom transports and credentials are shared by reference and remain caller-owned; keep them open until all clients and streams finish. Changing `.pat(...)`, `.accessToken(...)` or `.credential(...)` on a copy replaces its inherited credential source. For a static SAT use `.authMode(AuthMode.SA).accessToken(token)`.
108+
109+
### Additional JSON endpoints
110+
111+
```java
112+
import com.qoder.cloudagents.core.ApiRequest;
113+
import com.qoder.cloudagents.core.ApiResponse;
114+
import com.qoder.cloudagents.core.JsonValue;
115+
116+
ApiResponse<JsonValue> response = client.request(
117+
ApiRequest.builder("GET", "models").build(),
118+
RequestOptions.builder().totalTimeout(java.time.Duration.ofSeconds(30)).build());
119+
System.out.println(response.requestId());
120+
```
121+
122+
Pass unencoded path **segments**, for example `ApiRequest.builder("POST", "sessions", sessionId, "new-action")`; the SDK encodes each segment beneath its configured BaseURL. Absolute URLs, slash-separated paths, traversal and embedded queries/fragments are rejected. Use `RequestOptions.query(...)` and `header(...)` for query parameters and headers, and `.body(JsonValue)` for a JSON body. GET/HEAD cannot have a body. HEAD and 204/205 responses return JSON null.
123+
124+
The return type includes JSON, status and response headers; `client.async().request(...)` returns a cancellable `CompletableFuture<ApiResponse<JsonValue>>`. HTTP errors retain the existing exception mapping. This entry point uses business PAT/SAT credentials and the shared retry rules; it does not add resource-specific validation. Prefer typed resource methods where available, particularly SA Key token management and credential rotation. Use the existing resource APIs for multipart upload, downloads and SSE.
125+
126+
### HTTP interceptors and custom transport
127+
128+
```java
129+
try (ForwardClient traced = client.withOptions(options -> options.interceptor((request, chain) -> {
130+
long started = System.nanoTime();
131+
return chain.proceed(request.withHeader("X-Trace-Source", "example"))
132+
.whenComplete((response, error) -> {
133+
long elapsedMs = (System.nanoTime() - started) / 1_000_000;
134+
System.out.println("http_elapsed_ms=" + elapsedMs);
135+
});
136+
}))) {
137+
traced.models().list();
138+
}
139+
```
140+
141+
`HttpInterceptor` runs after authentication on **every attempt**, including SSE connection/reconnection and explicit SA Key token-management requests. It may inspect metadata, add ordinary headers or return a response, but may call `chain.proceed(...)` only once. It cannot change routing, method, body, timeout, Authorization or idempotency. Do not log headers, bodies or full URLs, or read a streaming body just to log it. Interceptor callbacks must return promptly. Their completion future measures response-header arrival, not SSE/download completion.
142+
143+
Implement `com.qoder.cloudagents.core.http.HttpTransport` and pass it to `.transport(...)` when an application needs its own HTTP library, proxy, TLS settings or connection pool. The SDK passes an immutable `HttpRequest` with URI, headers, connect/read/write timeouts and an optional streaming `Body`. Use `Body.contentType()`, `contentLength()` and `writeTo(OutputStream)` when sending uploads; do not buffer all uploads or close the destination from `writeTo`.
144+
145+
The transport must return a `CompletableFuture<HttpResponse>` promptly, perform one attempt without automatic redirects/retries/authentication, honor timeouts, and abort its I/O when its future is cancelled. Return an unconsumed `InputStream` in `HttpResponse`; the SDK closes it on completion, cancellation or failure. Transport implementations must release responses that lose a race with cancellation. Read timeout is zero for an accepted SSE stream; SDK header and optional total deadlines remain active. Cancelling an SDK request also cancels downstream transport futures even when an interceptor has wrapped them in `thenApply`/`whenComplete`.
146+
147+
| Operation | Custom transport | API interceptors |
148+
| --- | --- | --- |
149+
| JSON, multipart, authenticated downloads | Yes | Yes |
150+
| Session/Thread SSE, including resumption | Yes | Yes, per connection |
151+
| File signed-URL storage download | Yes | No; no API headers/credentials copied |
152+
| Realtime create/History HTTP | Yes | Yes |
153+
| Realtime WebSocket | No | No |
154+
| `ServiceAccountCredential` platform exchange | No | No |
155+
156+
The default HTTP implementation remains OkHttp. Injected transports and interceptors do not change the SDK's retry safety rules or grant additional API permissions.
157+
158+
## Template configuration example
91159

92160
```java
93161
import com.qoder.cloudagents.forward.TemplateCreateParams;

‎docs/forward-api-coverage.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ HTTP BaseURL 为 CN `https://api.qoder.com.cn/api/v1/forward` 或 Global `https:
1010

1111
这里只盘点协议入口;参数、响应、分页、认证和重试行为由对应资源指南与契约测试说明。
1212

13+
开发分支已按 Go `a996a88`、Python `2f8c625`、TypeScript `f4e3a4f` 的 main 补齐 ImageSource `file_id`、创建 Memory Store 时的 mount `access`、Schedule/Run `initial_events_version` 和 ChannelPairing 时间戳。接口操作集合仍为 123 项;客户端 HTTP 扩展、通用 JSON 请求和配置复制见[客户端配置](client-configuration.md),这些新增能力尚不在已发布的 `0.1.0` 中。
14+
1315
## 使用指南
1416

1517
- [客户端认证、超时、重试与取消](client-configuration.md)

‎docs/memory-stores.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ MemoryStoreCreateMount mount = MemoryStoreCreateMount.builder()
4242
// Supply .mount(mount) on MemoryStoreCreateParams.builder().
4343
```
4444

45-
Both IDs are required when supplying `mount`. This is a read-only explicit mount, matching the separate Identity mount API. Store `bindingInfo().identityTemplateCount()` reports active mounts; unknown binding fields remain available through `field(...)`.
45+
Both IDs are required when supplying `mount`. The development SDK also supports `.access("read_only")` or `.access("read_write")`; omitting it leaves the default to the service. This typed setter is not included in the published `0.1.0` JAR. Store `bindingInfo().identityTemplateCount()` reports active mounts; unknown binding fields remain available through `field(...)`.
4646

4747
Store metadata is a string map with at most 15 user keys. `created_by` is reserved and added by the service. Memory metadata allows 16 keys and does not reserve `created_by`. Key/value size rules and other business constraints are enforced by the service. The SDK does not insert reserved metadata.
4848

‎docs/schedules-batches.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Schedule schedule = client.schedules().create(
2424

2525
Schedule initial events use string `content` as documented by this endpoint; ordinary Session events have their own content-block model. `ScheduleTriggerPolicyParams` exposes request fields (`type`, `expression`, `timezone`), while `ScheduleTriggerPolicy` includes computed response fields such as `upcomingRunsAt` and `lastRunAt`. Cron/timezone, once-time and interval rules are validated by the server.
2626

27+
The development SDK exposes `initialEventsVersion()` on both `Schedule` and `ScheduleRun`. It returns a nullable `Long`: an absent version remains unknown, distinct from an explicit zero. This getter is not included in the published `0.1.0` JAR.
28+
2729
- `schedules().list/create/retrieve/update/archive/archiveMany/pause/unpause/run` cover the nine Schedule operations.
2830
- `scheduleRuns().list/retrieve` cover Run queries. Listing requires `ScheduleRunListParams.identityId`; retrieval accepts optional `ScheduleRunRetrieveParams.identityId`.
2931
- An omitted trigger policy or explicit `null` creates a manual Schedule. Updating `triggerPolicy(null)` switches to manual. Updating `sinks(null)` or an empty list clears delivery targets; omitting a field retains it. Execution fields merge. Metadata values of JSON `null` remove keys.

‎docs/sessions.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ System.out.println(accepted.requestId());
6666

6767
`EventContent.text(...)` creates the content-block array required for a user message. Use `EventContent.blocks(...)` with typed `ContentBlock` and `ImageSource` for text/image inputs. `EventContent.ofString(...)` represents string content for event types such as tool results; do not use it for a `user.message`.
6868

69+
The development SDK adds `ImageSource.builder().type("file").fileId(fileId)` and `fileId()` for images referenced by file ID. These typed methods are not included in the published `0.1.0` JAR; the service validates the image source and file permissions.
70+
6971
`SessionEventParam` also exposes tool confirmation/result fields, custom tool result IDs, interrupt reason and outcome definition. `OutcomeRubric` is an object with a type and text/file ID, matching the documented outcome contract. Event-specific validation and permissions remain server-side.
7072

7173
A successful send acknowledges accepted input; it does not mean the model finished. Reading history returns persisted events, not a live subscription:

‎examples/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ For SDK development or an unpublished version, install the local SDK first:
1717

1818
Verification uses a local fake service and asserts the SDK was loaded from a JAR. It does not use production credentials.
1919

20+
`ClientExtensionsExample` demonstrates a read-only generic JSON request, a per-attempt tracing interceptor and an independently closeable client copy. It also accepts an application-owned `HttpTransport`. These extension APIs are unreleased; install the development SDK first, then run with `-Dexec.mainClass=com.qoder.cloudagents.examples.ClientExtensionsExample`. The packaged-consumer test verifies an adapter built with Java 8 `HttpURLConnection`, without importing SDK internals.
21+
2022
To run against your environment, configure credentials in your shell and choose one mode:
2123

2224
| Mode | Environment |
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.qoder.cloudagents.examples;
2+
3+
import com.qoder.cloudagents.ForwardClient;
4+
import com.qoder.cloudagents.core.ApiRequest;
5+
import com.qoder.cloudagents.core.ApiResponse;
6+
import com.qoder.cloudagents.core.JsonValue;
7+
import com.qoder.cloudagents.core.RequestOptions;
8+
import com.qoder.cloudagents.core.http.HttpTransport;
9+
import java.io.PrintStream;
10+
import java.time.Duration;
11+
12+
/** Read-only model listing through a copied client, HTTP interceptor and generic JSON request. */
13+
public final class ClientExtensionsExample {
14+
private ClientExtensionsExample() {}
15+
16+
public static void main(String[] args) throws Exception {
17+
try (ExampleClient setup = ExampleClient.fromEnvironment()) {
18+
run(setup.client(), null, System.out);
19+
}
20+
}
21+
22+
/** Supply a caller-owned transport to use the application's HTTP implementation. */
23+
public static void run(ForwardClient client, HttpTransport transport, PrintStream output) {
24+
try (ForwardClient derived =
25+
client.withOptions(
26+
options -> {
27+
options
28+
.timeout(Duration.ofSeconds(30))
29+
.interceptor(
30+
(request, chain) -> {
31+
long started = System.nanoTime();
32+
return chain
33+
.proceed(request.withHeader("X-Trace-Source", "sdk-example"))
34+
.whenComplete(
35+
(response, error) ->
36+
output.println(
37+
"http_elapsed_ms="
38+
+ (System.nanoTime() - started) / 1_000_000));
39+
});
40+
if (transport != null) options.transport(transport);
41+
})) {
42+
ApiResponse<JsonValue> response =
43+
derived.request(
44+
ApiRequest.builder("GET", "models").build(),
45+
RequestOptions.builder().totalTimeout(Duration.ofSeconds(30)).build());
46+
output.println(
47+
"models="
48+
+ response.data().get("data").asArray().size()
49+
+ " request_id="
50+
+ response.requestId());
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)