GoodMemGoodMem
ReferenceClient SDKs

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 returns CompletableFuture<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" (no v1 suffix)
  • 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 #3

Proxy

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

MethodDescription
backgroundJobsPurgePurge completed background jobs
drainRequest the server to enter drain mode
licenseReloadReload the active license from disk
retrieveMemoryLogPoliciesCreateCreate a RetrieveMemory log policy
retrieveMemoryLogPoliciesDeleteDelete a RetrieveMemory log policy
retrieveMemoryLogPoliciesGetGet a RetrieveMemory log policy
retrieveMemoryLogPoliciesListList RetrieveMemory log policies

apikeys

API key lifecycle — create, list, update, soft-delete.

Javadoc: ai.pairsys.goodmem.client.api.ApikeysAPI

MethodDescription
createCreate a new API key
deleteDelete an API key Permanently deletes an API key
listList API keys
updateUpdate an API key

embedders

Embedder management — provider configuration + lifecycle.

Javadoc: ai.pairsys.goodmem.client.api.EmbeddersAPI

MethodDescription
createCreate a new embedder
deleteDelete an embedder
getGet an embedder by ID
listList embedders
updateUpdate an embedder

llms

LLM management — generation-time model registration.

Javadoc: ai.pairsys.goodmem.client.api.LlmsAPI

MethodDescription
createCreate a new LLM
deleteDelete an LLM
getGet an LLM by ID
listList LLMs
updateUpdate an LLM

memories

Memory CRUD + retrieval (streaming) + file upload + batch ops.

Javadoc: ai.pairsys.goodmem.client.api.MemoriesAPI

MethodDescription
batchCreateCreate multiple memories in a single batch
batchDeleteDelete memories in batch
batchGetGet multiple memories by ID
contentDownload memory content
createCreate a memory from text content or base64-encoded binary content
deleteDelete a memory
getGet a memory by ID
listLists memories within a given space
pagesList memory page images
pagesImageDownload memory page image content
retrievePerforms 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

MethodDescription
documentRun OCR on a document or image

ping

Endpoint health probes — single-shot and streaming.

Javadoc: ai.pairsys.goodmem.client.api.PingAPI

MethodDescription
onceRun a single ping probe
streamStream ping probe results

rerankers

Reranker management — re-scoring of retrieval hits.

Javadoc: ai.pairsys.goodmem.client.api.RerankersAPI

MethodDescription
createCreate a new reranker
deleteDelete a reranker
getGet a reranker by ID
listList rerankers
updateUpdate a reranker

spaces

Memory space management — the top-level container for memories.

Javadoc: ai.pairsys.goodmem.client.api.SpacesAPI

MethodDescription
createCreate a new Space
deleteDelete a space
getGet a space by ID
listList spaces accessible to the caller, with optional filtering by owner, labels, and name
updateUpdate a space

system

Server info + system initialization.

Javadoc: ai.pairsys.goodmem.client.api.SystemAPI

MethodDescription
infoRetrieve server build metadata
initInitialize the system

users

User lookup by id, email, or me.

Javadoc: ai.pairsys.goodmem.client.api.UsersAPI

MethodDescription
getRetrieves a user by ID or email address
meGet current user profile

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 typeMethod
SpaceListOptionsspaces.list(options)
EmbedderListOptionsembedders.list(options)
LLMListOptionsllms.list(options)
RerankerListOptionsrerankers.list(options)
MemoryListOptionsmemories.list(spaceId, options)
MemoryGetOptionsmemories.get(id, options)
MemoryPageListOptionsmemories.pages(id, options)
MemoryPageImageOptionsmemories.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:

WhereString/Long baseTyped overloadWhat it does
endpointUrl / apiPath / monitoringEndpointStringjava.net.URI.toString()
*Ms timeout fieldsLongjava.time.Duration.toMillis()
*Sec timeout fieldsLongjava.time.Duration.toSeconds()
*At timestamp fields (e.g. expiresAt)Longjava.time.Instant.toEpochMilli()
JsonMemoryCreationRequest.originalContentStringbyte[]base64-encodes into originalContentB64
RetrieveMemoryRequest.postProcessorPostProcessorChatPostProcessorConfig.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.processingStatusMemoryProcessingStatus
  • ApiKeyResponse.status / UpdateApiKeyRequest.statusApiKeyStatus
  • BackgroundJobSummary.statusBackgroundJobStatus
  • AdminDrainResponse.stateAdminDrainState

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:

FieldBound
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 page
  • nextToken()String — continuation token, or null if exhausted
  • hasMore()boolean — true iff nextToken != null
  • next()Page<T> — fetch next page; throws NoSuchElementException when exhausted
  • iterator()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_END boundary strategy, measured by characters. Auto-injected by spaces.create when defaultChunkingConfig is 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.

EndpointAuthentication

Structured credential payload describing how GoodMem should authenticate with an upstream provider.

  • kind (CredentialKind) — Selected credential strategy
  • apiKey (ApiKeyAuth, optional) — Configuration when kind is CREDENTIAL_KIND_API_KEY
  • gcpAdc (GcpAdcAuth, optional) — Configuration when kind is CREDENTIAL_KIND_GCP_ADC
  • labels (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 message
  • message (String) — Human-readable status message
  • details (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 chunks
  • separators (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 patterns
  • lengthMeasurement (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 chunk
  • minChunkSize (Long) — Minimum size before creating a new chunk
  • enableLanguageDetection (Boolean, optional) — Whether to detect language for better segmentation
  • lengthMeasurement (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.

ExceptionHTTPDescription
GoodmemExceptionBase exception for all SDK errors (also wraps I/O failures)
ApiExceptionany 4xx/5xxGeneric HTTP error (has getStatusCode(), getBody())
BadRequestException400Malformed or invalid request
AuthenticationException401Invalid or missing API key
PermissionDeniedException403Insufficient permissions
NotFoundException404Resource not found
ConflictException409Conflict (e.g., duplicate id)
UnprocessableEntityException422Invalid request parameters
RateLimitException429Too many requests
InternalServerException5xxServer-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.html

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