Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## [Unreleased]

### Added

- 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.
- Synchronous/asynchronous `request(ApiRequest, RequestOptions)` for additional JSON endpoints, reusing authentication, retries, cancellation and response metadata with relative path segments.
- Synchronous/asynchronous `withOptions(...)` to derive independently closeable clients while retaining resolved authentication and unspecified settings. Caller-supplied credentials and transports remain caller-owned.

### Fixed

- 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.

## [0.1.0] - 2026-10-21

First stable release of the Java SDK for the Forward API, using `com.qoder.sdk:qoder-cloud-agents-sdk:0.1.0`.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ Microphone capture and audio playback belong to the application. Connections do

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

Expand Down
2 changes: 2 additions & 0 deletions docs/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ DeletedChannelPairing removed = client.channelPairings().delete(pairing.id());

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.

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.

`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.

## Lists and examples
Expand Down
70 changes: 69 additions & 1 deletion docs/client-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,75 @@ com.qoder.cloudagents.core.ApiResponse<com.qoder.cloudagents.forward.Template> r
- 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.
- 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`.

## Template configuration
## Client extensions (unreleased)

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.

### Derived clients

```java
try (ForwardClient slow = client.withOptions(options -> options
.timeout(java.time.Duration.ofSeconds(60))
.maxRetries(1)
.defaultHeader("X-Trace-Source", "background-job"))) {
slow.models().list();
}
```

`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.

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)`.

### Additional JSON endpoints

```java
import com.qoder.cloudagents.core.ApiRequest;
import com.qoder.cloudagents.core.ApiResponse;
import com.qoder.cloudagents.core.JsonValue;

ApiResponse<JsonValue> response = client.request(
ApiRequest.builder("GET", "models").build(),
RequestOptions.builder().totalTimeout(java.time.Duration.ofSeconds(30)).build());
System.out.println(response.requestId());
```

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.

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.

### HTTP interceptors and custom transport

```java
try (ForwardClient traced = client.withOptions(options -> options.interceptor((request, chain) -> {
long started = System.nanoTime();
return chain.proceed(request.withHeader("X-Trace-Source", "example"))
.whenComplete((response, error) -> {
long elapsedMs = (System.nanoTime() - started) / 1_000_000;
System.out.println("http_elapsed_ms=" + elapsedMs);
});
}))) {
traced.models().list();
}
```

`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.

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`.

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`.

| Operation | Custom transport | API interceptors |
| --- | --- | --- |
| JSON, multipart, authenticated downloads | Yes | Yes |
| Session/Thread SSE, including resumption | Yes | Yes, per connection |
| File signed-URL storage download | Yes | No; no API headers/credentials copied |
| Realtime create/History HTTP | Yes | Yes |
| Realtime WebSocket | No | No |
| `ServiceAccountCredential` platform exchange | No | No |

The default HTTP implementation remains OkHttp. Injected transports and interceptors do not change the SDK's retry safety rules or grant additional API permissions.

## Template configuration example

```java
import com.qoder.cloudagents.forward.TemplateCreateParams;
Expand Down
2 changes: 2 additions & 0 deletions docs/forward-api-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ HTTP BaseURL 为 CN `https://api.qoder.com.cn/api/v1/forward` 或 Global `https:

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

开发分支已按 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` 中。

## 使用指南

- [客户端认证、超时、重试与取消](client-configuration.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/memory-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ MemoryStoreCreateMount mount = MemoryStoreCreateMount.builder()
// Supply .mount(mount) on MemoryStoreCreateParams.builder().
```

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(...)`.
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(...)`.

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.

Expand Down
2 changes: 2 additions & 0 deletions docs/schedules-batches.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Schedule schedule = client.schedules().create(

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.

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.

- `schedules().list/create/retrieve/update/archive/archiveMany/pause/unpause/run` cover the nine Schedule operations.
- `scheduleRuns().list/retrieve` cover Run queries. Listing requires `ScheduleRunListParams.identityId`; retrieval accepts optional `ScheduleRunRetrieveParams.identityId`.
- 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ System.out.println(accepted.requestId());

`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`.

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.

`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.

A successful send acknowledges accepted input; it does not mean the model finished. Reading history returns persisted events, not a live subscription:
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ For SDK development or an unpublished version, install the local SDK first:

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

`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.

To run against your environment, configure credentials in your shell and choose one mode:

| Mode | Environment |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.qoder.cloudagents.examples;

import com.qoder.cloudagents.ForwardClient;
import com.qoder.cloudagents.core.ApiRequest;
import com.qoder.cloudagents.core.ApiResponse;
import com.qoder.cloudagents.core.JsonValue;
import com.qoder.cloudagents.core.RequestOptions;
import com.qoder.cloudagents.core.http.HttpTransport;
import java.io.PrintStream;
import java.time.Duration;

/** Read-only model listing through a copied client, HTTP interceptor and generic JSON request. */
public final class ClientExtensionsExample {
private ClientExtensionsExample() {}

public static void main(String[] args) throws Exception {
try (ExampleClient setup = ExampleClient.fromEnvironment()) {
run(setup.client(), null, System.out);
}
}

/** Supply a caller-owned transport to use the application's HTTP implementation. */
public static void run(ForwardClient client, HttpTransport transport, PrintStream output) {
try (ForwardClient derived =
client.withOptions(
options -> {
options
.timeout(Duration.ofSeconds(30))
.interceptor(
(request, chain) -> {
long started = System.nanoTime();
return chain
.proceed(request.withHeader("X-Trace-Source", "sdk-example"))
.whenComplete(
(response, error) ->
output.println(
"http_elapsed_ms="
+ (System.nanoTime() - started) / 1_000_000));
});
if (transport != null) options.transport(transport);
})) {
ApiResponse<JsonValue> response =
derived.request(
ApiRequest.builder("GET", "models").build(),
RequestOptions.builder().totalTimeout(Duration.ofSeconds(30)).build());
output.println(
"models="
+ response.data().get("data").asArray().size()
+ " request_id="
+ response.requestId());
}
}
}
Loading
Loading