Java
The GoodMem Java SDK offers an OpenAI-style API where any operation is accessed through client.<namespace>.<method>(...) on a Goodmem instance.
Installation
Available on Maven Central as ai.pairsys:goodmem-java.
// build.gradle.kts
repositories { mavenCentral() }
dependencies {
implementation("ai.pairsys:goodmem-java:0.1.2")
}<!-- pom.xml -->
<dependency>
<groupId>ai.pairsys</groupId>
<artifactId>goodmem-java</artifactId>
<version>0.1.2</version>
</dependency>To build from source instead (e.g., to depend on an unreleased commit):
cd goodmem/clients/v2/java
./gradlew publishToMavenLocal # installs to ~/.m2/repository/Then add mavenLocal() to your repositories block alongside mavenCentral().
Requirements: JDK 21 LTS. Brings in OkHttp 4 (HTTP) and Jackson 2.18 (JSON) transitively.
Clients
The Java SDK ships two clients with identical construction surfaces and parallel methods:
Goodmem— synchronous. Every method returns the response directly or throws.AsyncGoodmem— asynchronous. Every method returnsCompletableFuture<T>.
Most of the usage details in this page apply equally to both; code samples default to the sync client. For the async surface see Async client below.
Construction
Goodmem is constructed via its Builder in two mutually exclusive patterns.
Pattern 1 — Simple mode (baseUrl + apiKey):
import ai.pairsys.goodmem.client.Goodmem;
Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.timeout(java.time.Duration.ofSeconds(60)) // optional; defaults to 30s
.build();baseUrl— Goodmem server URL, e.g.,"http://localhost:8080"(nov1suffix)apiKey— Goodmem API key, e.g.,"gm_..."timeout— Call/connect/read/write timeout. Defaults to 30 seconds. Increase for long operations like RAG retrieval with LLM generation.
Pattern 2 — http_client mode (custom OkHttpClient):
import okhttp3.OkHttpClient;
Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.httpClient(myOkHttpClient) // fully configured by you
.build();Use this when you need interceptors (logging, tracing, retry), a proxy, a custom TLS trust store, or connection pool tuning. You own the client's lifecycle — goodmem.close() is a no-op under this mode.
try-with-resources
Goodmem implements AutoCloseable. Use it as a resource so the connection pool is released deterministically:
try (Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// use client
}Quickstart
import ai.pairsys.goodmem.client.Goodmem;
import ai.pairsys.goodmem.client.models.*;
try (Goodmem client = Goodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// Create an embedder. Registry auto-fills provider, endpoint,
// dimensionality, max_sequence_length, distribution_type from the
// model_identifier; apiKey is converted to structured credentials.
EmbedderResponse embedder = client.embedders.create(
EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build(),
"sk-..."
);
// Create a space. DEFAULT_CHUNKING_CONFIG is auto-injected when unset.
Space space = client.spaces.create(
SpaceCreationRequest.builder()
.name("My Space")
.label("env", "dev")
.spaceEmbedders(java.util.List.of(new SpaceEmbedderConfig(embedder.embedderId(), null)))
.publicRead(false)
.build());
// Pagination is automatic — iterate across all pages.
for (Memory m : client.memories.list(space.spaceId())) {
System.out.println(m.memoryId());
}
}TLS configuration
Four situations cover the typical needs:
1. Default — trusts system CAs
No configuration needed. Works for any server with a cert from a public CA.
2. Skip verification (localhost / dev)
Build an OkHttpClient that trusts all certificates, then pass it in http_client mode. Never use in production — an attacker in the middle can impersonate the server:
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
TrustManager[] trustAll = new TrustManager[] {
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustAll, new java.security.SecureRandom());
OkHttpClient http = new OkHttpClient.Builder()
.sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) trustAll[0])
.hostnameVerifier((host, session) -> true)
.build();
Goodmem client = Goodmem.builder()
.baseUrl("https://localhost:8081")
.apiKey("gm_...")
.httpClient(http)
.build();3. Custom CA — trusts only that CA
Load your CA into a KeyStore and build a TrustManagerFactory:
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import javax.net.ssl.*;
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
ks.setCertificateEntry("ca", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
OkHttpClient http = new OkHttpClient.Builder()
.sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0])
.build();4. Custom CA + system CAs
If you need to trust both your CA and the default system trust store, load both into a KeyStore:
KeyStore defaultKs = KeyStore.getInstance(KeyStore.getDefaultType());
defaultKs.load(null);
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
defaultTmf.init((KeyStore) null); // loads system CAs
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
// add default CAs
for (TrustManager tm : defaultTmf.getTrustManagers()) {
if (tm instanceof X509TrustManager xtm) {
int i = 0;
for (X509Certificate c : xtm.getAcceptedIssuers()) ks.setCertificateEntry("sys-" + i++, c);
}
}
// add your CA
try (var in = new java.io.FileInputStream("/path/to/rootCA.pem")) {
ks.setCertificateEntry("mine", CertificateFactory.getInstance("X.509").generateCertificate(in));
}
// then build TrustManagerFactory + OkHttpClient as in #3Proxy
OkHttpClient http = new OkHttpClient.Builder()
.proxy(new java.net.Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("proxy.example", 8080)))
.build();Request logging
import okhttp3.logging.HttpLoggingInterceptor;
HttpLoggingInterceptor log = new HttpLoggingInterceptor();
log.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient http = new OkHttpClient.Builder().addInterceptor(log).build();Requires the extra dep: com.squareup.okhttp3:logging-interceptor.
Methods
Each namespace below links to its full class Javadoc on javadoc.io — that's where the per-method @param contract, @throws block, Example code, and REST equivalent all live. The MDX page below this section is the narrative reference; the Javadoc is the per-method reference.
admin
Server lifecycle ops — drain, license reload, background-job purge.
Javadoc: ai.pairsys.goodmem.client.api.AdminAPI
| Method | Description |
|---|---|
backgroundJobsPurge | Purge completed background jobs |
drain | Request the server to enter drain mode |
licenseReload | Reload the active license from disk |
retrieveMemoryLogPoliciesCreate | Create a RetrieveMemory log policy |
retrieveMemoryLogPoliciesDelete | Delete a RetrieveMemory log policy |
retrieveMemoryLogPoliciesGet | Get a RetrieveMemory log policy |
retrieveMemoryLogPoliciesList | List RetrieveMemory log policies |
apikeys
API key lifecycle — create, list, update, soft-delete.
Javadoc: ai.pairsys.goodmem.client.api.ApikeysAPI
| Method | Description |
|---|---|
create | Create a new API key |
delete | Delete an API key Permanently deletes an API key |
list | List API keys |
update | Update an API key |
embedders
Embedder management — provider configuration + lifecycle.
Javadoc: ai.pairsys.goodmem.client.api.EmbeddersAPI
| Method | Description |
|---|---|
create | Create a new embedder |
delete | Delete an embedder |
get | Get an embedder by ID |
list | List embedders |
update | Update an embedder |
llms
LLM management — generation-time model registration.
Javadoc: ai.pairsys.goodmem.client.api.LlmsAPI
| Method | Description |
|---|---|
create | Create a new LLM |
delete | Delete an LLM |
get | Get an LLM by ID |
list | List LLMs |
update | Update an LLM |
memories
Memory CRUD + retrieval (streaming) + file upload + batch ops.
Javadoc: ai.pairsys.goodmem.client.api.MemoriesAPI
| Method | Description |
|---|---|
batchCreate | Create multiple memories in a single batch |
batchDelete | Delete memories in batch |
batchGet | Get multiple memories by ID |
content | Download memory content |
create | Create a memory from text content or base64-encoded binary content |
delete | Delete a memory |
get | Get a memory by ID |
list | Lists memories within a given space |
pages | List memory page images |
pagesImage | Download memory page image content |
retrieve | Performs a streaming semantic search across one or more memory spaces and returns matching chunks ranked by relevance as well as… |
ocr
Document OCR — text extraction from PDFs / images.
Javadoc: ai.pairsys.goodmem.client.api.OcrAPI
| Method | Description |
|---|---|
document | Run OCR on a document or image |
ping
Endpoint health probes — single-shot and streaming.
Javadoc: ai.pairsys.goodmem.client.api.PingAPI
rerankers
Reranker management — re-scoring of retrieval hits.
Javadoc: ai.pairsys.goodmem.client.api.RerankersAPI
| Method | Description |
|---|---|
create | Create a new reranker |
delete | Delete a reranker |
get | Get a reranker by ID |
list | List rerankers |
update | Update a reranker |
spaces
Memory space management — the top-level container for memories.
Javadoc: ai.pairsys.goodmem.client.api.SpacesAPI
| Method | Description |
|---|---|
create | Create a new Space |
delete | Delete a space |
get | Get a space by ID |
list | List spaces accessible to the caller, with optional filtering by owner, labels, and name |
update | Update a space |
system
Server info + system initialization.
Javadoc: ai.pairsys.goodmem.client.api.SystemAPI
users
User lookup by id, email, or me.
Javadoc: ai.pairsys.goodmem.client.api.UsersAPI
Request builders
Every request record (EmbedderCreationRequest, SpaceCreationRequest, JsonMemoryCreationRequest, RetrieveMemoryRequest, RerankerCreationRequest, LLMCreationRequest, etc.) exposes a nested Builder so you can set only the fields you care about and skip the long positional null, null, null, … constructor:
EmbedderCreationRequest req = EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build();Map<String, String> fields named labels, hints, or metadata get a singular-form convenience setter that appends to the map:
SpaceCreationRequest req = SpaceCreationRequest.builder()
.name("My Space")
.label("env", "dev") // appends to labels
.label("team", "search")
.spaceEmbedders(List.of(new SpaceEmbedderConfig(embedderId, null)))
.build();Under the hood the Builder just calls the record's positional constructor. Null unset fields are omitted from the wire payload thanks to @JsonInclude(NON_NULL). You can still use the positional constructor directly if you prefer — the Builder is purely additive.
Typed list/get options
List and get methods expose a typed XxxListOptions / XxxGetOptions record so you don't have to remember wire-format keys (maxResults, sortOrder, …) and can let the IDE autocomplete them:
Page<Space> page = client.spaces.list(
SpaceListOptions.builder()
.maxResults(50)
.nameFilter("research*")
.sortOrder(SortOrder.ASCENDING)
.label("env", "prod") // flattened to label.env=prod on the wire
.build());Null-valued fields are dropped from the query string. Map<String, String> fields named label are flattened into label.<key>=<value> entries to match the server's label-filter syntax. Passing null for the options argument is equivalent to an empty filter.
Eight options classes are generated:
| Options type | Method |
|---|---|
SpaceListOptions | spaces.list(options) |
EmbedderListOptions | embedders.list(options) |
LLMListOptions | llms.list(options) |
RerankerListOptions | rerankers.list(options) |
MemoryListOptions | memories.list(spaceId, options) |
MemoryGetOptions | memories.get(id, options) |
MemoryPageListOptions | memories.pages(id, options) |
MemoryPageImageOptions | memories.pagesImage(id, pageIndex, options) |
The original Map<String, Object> overload is still available as an escape hatch — it's renamed to listRaw(...) / getRaw(...) / pagesRaw(...) / pagesImageRaw(...) so the primary method name belongs to the typed path:
// Typed (preferred):
client.spaces.list(SpaceListOptions.builder().maxResults(50).build());
// Raw escape hatch:
client.spaces.listRaw(Map.of("maxResults", 50));UUID-typed path parameters
Every get / delete / update method that takes a resource id offers a java.util.UUID overload alongside the String form. The generator converts via .toString() internally:
UUID spaceUuid = UUID.fromString("...");
Space typed = client.spaces.get(spaceUuid); // UUID-typed
Space raw = client.spaces.get("sp_..."); // String-typed (escape hatch)memories.list(spaceId, options) and memories.pagesImage(id, pageIndex, ...) also get UUID + long pageIndex overloads, plus a combined (UUID, long, ...) variant so the strongly-typed signatures compose without forcing you to mix.
Factory methods for oneOf models
Records with an "exactly one of" contract (ChunkingConfiguration, ContextItem, PingEvent, RetrievedItem) expose static factory methods per variant, so you can't accidentally hand-null sibling fields:
ChunkingConfiguration cfg = ChunkingConfiguration.recursive(
RecursiveChunkingConfiguration.builder()
.chunkSize(512)
.chunkOverlap(64)
.build());
RetrievedItem item = RetrievedItem.memory(memory);Encoded-primitive convenience setters
The Builder for each request record overloads a handful of high-churn fields with stronger Java types:
| Where | String/Long base | Typed overload | What it does |
|---|---|---|---|
endpointUrl / apiPath / monitoringEndpoint | String | java.net.URI | .toString() |
*Ms timeout fields | Long | java.time.Duration | .toMillis() |
*Sec timeout fields | Long | java.time.Duration | .toSeconds() |
*At timestamp fields (e.g. expiresAt) | Long | java.time.Instant | .toEpochMilli() |
JsonMemoryCreationRequest.originalContent | String | byte[] | base64-encodes into originalContentB64 |
RetrieveMemoryRequest.postProcessor | PostProcessor | ChatPostProcessorConfig | .toPostProcessor() |
CreateApiKeyRequest req = CreateApiKeyRequest.builder()
.label("scope", "ci")
.expiresAt(Instant.now().plus(Duration.ofDays(30)))
.build();Use ai.pairsys.goodmem.client.MediaTypes for common contentType values: TEXT_PLAIN, APPLICATION_PDF, IMAGE_PNG, etc. — avoids string-literal typos.
Typed configs for the built-in RAG chat processor
ai.pairsys.goodmem.client.ChatPostProcessorConfig is a fluent, validated builder for the ChatPostProcessor factory — the common RAG pipeline. Numeric fields enforce documented bounds where applicable (llmTemp [0, 2], positive token budgets, positive maxResults):
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("What do you know about AI?")
.spaceKeys(List.of(new SpaceKey(spaceId, null, null)))
.postProcessor(ChatPostProcessorConfig.builder()
.llmId(llmId)
.genTokenBudget(2048L)
.llmTemp(0.2)
.build())
.build();For non-built-in processors, keep using new PostProcessor(factoryName, configMap) directly.
Typed status enums
Four IR-declared status fields now deserialize as typed Java enums instead of raw strings — so pattern matching and switch on known states works:
Memory.processingStatus→MemoryProcessingStatusApiKeyResponse.status/UpdateApiKeyRequest.status→ApiKeyStatusBackgroundJobSummary.status→BackgroundJobStatusAdminDrainResponse.state→AdminDrainState
Unknown server values fail Jackson deserialization loudly; if the server adds a new state, regenerate from an updated IR.
Range validation on Builder setters
Setters on documented numeric bounds throw IllegalArgumentException at call time rather than waiting for the server to reject. Null still clears the field. Covered today:
| Field | Bound |
|---|---|
dimensionality, maxSequenceLength, chunkSize, maxResults, limit, count, efSearch, maxScan, maxInFlight | ≥ 1 |
chunkOverlap | ≥ 0 |
temperature | [0.0, 2.0] |
topP | [0.0, 1.0] |
frequencyPenalty, presencePenalty | [-2.0, 2.0] |
Common Data Models
Types shared across multiple API namespaces. Models are Java records with Jackson annotations; JSON wire names are camelCase (matching the record component names).
Page<T>
ai.pairsys.goodmem.client.Page<T> — a page of results from a list endpoint. Returned by every method that auto-paginates (spaces.list, memories.list). Iterates across all pages lazily; subsequent pages are fetched on demand. Page<T> does not implement AutoCloseable — the underlying HTTP connection is released per call, so there's nothing to close.
Page<Space> page = client.spaces.list(SpaceListOptions.builder().maxResults(50).build());
// First-page items only
List<Space> first = page.items();
// Auto-paginate through everything (lazy)
for (Space s : page) { process(s); }
// Manual page advance
while (page.hasMore()) {
page = page.next();
for (Space s : page.items()) { process(s); }
}items()→List<T>— items in this pagenextToken()→String— continuation token, ornullif exhaustedhasMore()→boolean— true iffnextToken != nullnext()→Page<T>— fetch next page; throwsNoSuchElementExceptionwhen exhaustediterator()→Iterator<T>— lazy cross-page iteration; can only be called once
RetrieveMemoryStream
ai.pairsys.goodmem.client.RetrieveMemoryStream — iterable, closeable NDJSON stream of RetrieveMemoryEvent returned by memories.retrieve. Each newline-delimited JSON document is parsed into one event. Always use try-with-resources to release the underlying HTTP connection:
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("question")
.spaceId(spaceId)
.build();
try (RetrieveMemoryStream events = client.memories.retrieve(req)) {
for (RetrieveMemoryEvent evt : events) {
process(evt);
}
}The iterator can only be consumed once — NDJSON cannot be rewound. To restart, call memories.retrieve again.
Defaults
ai.pairsys.goodmem.client.Defaults — hand-written SDK default constants. Currently exposes:
-
DEFAULT_CHUNKING_CONFIG(ChunkingConfiguration) — recursive chunking at 512 characters with 64-character overlap,KEEP_ENDboundary strategy, measured by characters. Auto-injected byspaces.createwhendefaultChunkingConfigis null. Reference it directly if you want to start from the defaults and tweak:import ai.pairsys.goodmem.client.Defaults; ChunkingConfiguration base = Defaults.DEFAULT_CHUNKING_CONFIG;
ApiKeyAuth
Configuration for classic API-key authentication.
inlineSecret(String, optional) — Secret stored directly in GoodMem (mutually exclusive with secretRef)secretRef(SecretReference, optional) — Reference to an external secret manager entry (mutually exclusive with inlineSecret)headerName(String, optional) — Desired HTTP header to carry the credential (defaults to Authorization)prefix(String, optional) — Optional prefix prepended to the secret (e.g., "Bearer ")
ChunkingConfiguration
Configuration for text chunking strategy used when processing content. Exactly one of none, recursive, or sentence must be provided.
none(NoChunkingConfiguration, optional) — No chunking strategy - preserve original content as single unitrecursive(RecursiveChunkingConfiguration, optional) — Recursive hierarchical chunking strategy with configurable separatorssentence(SentenceChunkingConfiguration, optional) — Sentence-based chunking strategy with language detection
EndpointAuthentication
Structured credential payload describing how GoodMem should authenticate with an upstream provider.
kind(CredentialKind) — Selected credential strategyapiKey(ApiKeyAuth, optional) — Configuration when kind is CREDENTIAL_KIND_API_KEYgcpAdc(GcpAdcAuth, optional) — Configuration when kind is CREDENTIAL_KIND_GCP_ADClabels(Map``<``String,String``>, optional) — Optional annotations to aid operators (e.g., "owner=vertex")
GcpAdcAuth
Configuration for Google Application Default Credentials (ADC).
scopes(List``<``String``>, optional) — Additional OAuth scopes. Empty list falls back to the default cloud-platform scope.quotaProjectId(String, optional) — Optional quota project used for billing
GoodMemStatus
Warning or non-fatal status with granular codes (operation continues)
code(String) — Status code for the warning or informational messagemessage(String) — Human-readable status messagedetails(Map``<``String,String``>, optional) — Additional contextual details
NoChunkingConfiguration
No chunking strategy - preserves original content as a single unit
(empty record)
RecursiveChunkingConfiguration
Recursive hierarchical chunking strategy with configurable separators and overlap
chunkSize(Long) — Maximum size of a chunk (should be ≤ context window)chunkOverlap(Long) — Sliding overlap between chunksseparators(List``<``String``>, optional) — Hierarchical separator list (order = preference)keepStrategy(SeparatorKeepStrategy) — How to handle separators after splitting. KEEP_NONE is deprecated and behaves as KEEP_END.separatorIsRegex(Boolean, optional) — Whether separators are regex patternslengthMeasurement(LengthMeasurement) — How to measure chunk length
SecretReference
uri(String) — URI identifying where the secret can be resolved (e.g., vault://, env://)hints(Map``<``String,String``>, optional) — Optional metadata to help resolvers decode the secret (e.g., {"encoding":"base64"})
SentenceChunkingConfiguration
Sentence-based chunking strategy with language detection support
maxChunkSize(Long) — Maximum size of a chunkminChunkSize(Long) — Minimum size before creating a new chunkenableLanguageDetection(Boolean, optional) — Whether to detect language for better segmentationlengthMeasurement(LengthMeasurement) — How to measure chunk length
CredentialKind
Credential kinds supported for upstream endpoint authentication.
Java enum — CREDENTIAL_KIND_UNSPECIFIED, CREDENTIAL_KIND_API_KEY, CREDENTIAL_KIND_GCP_ADC.
DistributionType
Type of embedding distribution produced by the embedder
Java enum — DENSE, SPARSE.
HnswIterativeScan
Iterative scan modes for HNSW index search.
Java enum — ITERATIVE_SCAN_UNSPECIFIED, ITERATIVE_SCAN_OFF, ITERATIVE_SCAN_RELAXED_ORDER, ITERATIVE_SCAN_STRICT_ORDER.
LLMProviderType
LLM provider types
Java enum — OPENAI, LITELLM_PROXY, OPEN_ROUTER, VLLM, OLLAMA, LLAMA_CPP, CUSTOM_OPENAI_COMPATIBLE.
LengthMeasurement
Strategy for measuring chunk length during text splitting
Java enum — CHARACTER_COUNT, TOKEN_COUNT, CUSTOM.
Modality
Content modality types supported by embedders
Java enum — TEXT, IMAGE, AUDIO, VIDEO.
OcrCategory
Normalized OCR layout category.
Java enum — UNSPECIFIED, CAPTION, FOOTNOTE, FORMULA, LIST_ITEM, PAGE_FOOTER, PAGE_HEADER, PICTURE, SECTION_HEADER, TABLE, TEXT, TITLE, OTHER, UNKNOWN.
OcrInputFormat
OCR input format hint.
Java enum — AUTO, PDF, TIFF, PNG, JPEG, BMP.
PingPayloadType
Payload types supported by ping operations
Java enum — PAYLOAD_TYPE_UNSPECIFIED, TEXT, JSON, BINARY.
PingTargetType
Target types for ping operations
Java enum — TARGET_TYPE_UNSPECIFIED, EMBEDDER, RERANKER, LLM.
ProviderType
Embedding provider types
Java enum — OPENAI, VLLM, TEI, LLAMA_CPP, VOYAGE, COHERE, JINA.
PurgeableBackgroundJobStatus
Terminal background job statuses eligible for purge requests.
Java enum — BACKGROUND_JOB_SUCCEEDED, BACKGROUND_JOB_FAILED, BACKGROUND_JOB_CANCELED.
SeparatorKeepStrategy
Strategy for handling separators after text splitting. KEEP_NONE is deprecated and treated as KEEP_END.
Java enum — KEEP_NONE, KEEP_START, KEEP_END.
SortOrder
Java enum — ASCENDING, DESCENDING, SORT_ORDER_UNSPECIFIED.
Errors
All SDK methods throw typed unchecked exceptions on HTTP errors. The hierarchy is rooted at GoodmemException, with ApiException for any HTTP 4xx/5xx and per-status subclasses for the common ones. Error class names align with the Python SDK's naming.
| Exception | HTTP | Description |
|---|---|---|
GoodmemException | — | Base exception for all SDK errors (also wraps I/O failures) |
ApiException | any 4xx/5xx | Generic HTTP error (has getStatusCode(), getBody()) |
BadRequestException | 400 | Malformed or invalid request |
AuthenticationException | 401 | Invalid or missing API key |
PermissionDeniedException | 403 | Insufficient permissions |
NotFoundException | 404 | Resource not found |
ConflictException | 409 | Conflict (e.g., duplicate id) |
UnprocessableEntityException | 422 | Invalid request parameters |
RateLimitException | 429 | Too many requests |
InternalServerException | 5xx | Server-side error |
import ai.pairsys.goodmem.client.errors.*;
try {
Memory memory = client.memories.get("nonexistent-id");
} catch (NotFoundException e) {
System.out.println("Memory not found");
} catch (ApiException e) {
System.out.println("API error " + e.getStatusCode() + ": " + e.getBody());
}All exceptions are RuntimeExceptions — no checked-exception surface.
File upload convenience
memories.create(String spaceId, Path filePath) reads a file from disk, base64-encodes it, infers a contentType via Files.probeContentType, and posts a JSON memory:
import java.nio.file.Path;
Memory memory = client.memories.create(
space.spaceId(),
Path.of("/tmp/report.pdf"));For large files consider streaming the bytes yourself or using an async upload in your own code — the built-in overload reads the file fully into memory before base64-encoding.
Javadoc
Every class, method, field, and parameter is documented in full Javadoc. Locally:
cd goodmem/clients/v2/java
./gradlew javadoc
open build/docs/javadoc/index.htmljavadoc.io hosts the per-class API reference automatically once releases land on Maven Central:
https://javadoc.io/doc/ai.pairsys/goodmem-java/latest/.
Async client
AsyncGoodmem is a parallel client where every method returns
CompletableFuture<T> instead of blocking for the response. Construction
mirrors Goodmem exactly — same Builder, same two modes (simple /
custom OkHttpClient), same timeout / close semantics.
import ai.pairsys.goodmem.client.AsyncGoodmem;
import ai.pairsys.goodmem.client.models.SystemInfoResponse;
try (AsyncGoodmem client = AsyncGoodmem.builder()
.baseUrl("http://localhost:8080")
.apiKey("gm_...")
.build()) {
// Single call — `.get()` or `.join()` if you want to block here
SystemInfoResponse info = client.system.info().get();
// Composition — ideal for pipelines
EmbedderCreationRequest req = EmbedderCreationRequest.builder()
.displayName("My OpenAI")
.modelIdentifier("text-embedding-3-large")
.build();
client.embedders
.create(req, "sk-...")
.thenCompose(emb -> client.spaces.create(
SpaceCreationRequest.builder()
.name("My Space")
.spaceEmbedders(java.util.List.of(
new SpaceEmbedderConfig(emb.embedderId(), null)))
.build()))
.thenAccept(space -> System.out.println("Space ready: " + space.spaceId()))
.exceptionally(ex -> { ex.printStackTrace(); return null; })
.join();
}Under the hood, the async client uses OkHttp's native async callbacks —
no thread pool is required beyond OkHttp's own dispatcher, and the same
connection pool is shared across concurrent requests. If you need more
concurrency control (custom dispatcher executor, rate limiting, etc.),
build an OkHttpClient yourself and pass it via .httpClient(...).
Async pagination
List endpoints return CompletableFuture<AsyncPage<T>> instead of Page<T>.
AsyncPage<T> exposes the current page's items synchronously (they're
already loaded); .next() returns CompletableFuture<AsyncPage<T>> to
fetch the next page.
static CompletableFuture<List<Space>> collectAll(AsyncPage<Space> page, List<Space> acc) {
acc.addAll(page.items());
return page.hasMore()
? page.next().thenCompose(p -> collectAll(p, acc))
: CompletableFuture.completedFuture(acc);
}
client.spaces.list()
.thenCompose(first -> collectAll(first, new ArrayList<>()))
.thenAccept(all -> System.out.println("Got " + all.size() + " spaces"));AsyncPage<T> intentionally does not implement Iterable<T> — mixing
blocking iteration with async I/O is a footgun. Chain via .next() and
.thenCompose() instead.
Async streaming
memories.retrieve returns CompletableFuture<RetrieveMemoryStream> on
the async client. The future completes once the stream headers arrive;
the returned stream is consumed the same way as in the sync client (lazy
Iterable<RetrieveMemoryEvent> inside a try-with-resources block).
RetrieveMemoryRequest req = RetrieveMemoryRequest.builder()
.message("question")
.spaceId(spaceId)
.build();
client.memories.retrieve(req).thenAccept(events -> {
try (events) {
for (RetrieveMemoryEvent e : events) process(e);
}
}).join();Error handling
HTTP errors (4xx/5xx) and I/O failures complete the future exceptionally
with the same exception types as the sync client, wrapped by
CompletionException at the call site:
client.memories.get("missing")
.thenAccept(System.out::println)
.exceptionally(ex -> {
Throwable cause = ex.getCause(); // unwrap CompletionException
if (cause instanceof NotFoundException) {
System.out.println("Memory not found");
}
return null;
});Request validation errors — e.g., credential_check throwing when a SaaS
endpoint is targeted without credentials — are thrown synchronously,
before the HTTP call is scheduled. The rationale: that's a programmer
error, not an I/O event, and should fail fast.