diff --git a/.gitignore b/.gitignore index cb37348..344ef15 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,8 @@ build/ ### Mac OS ### .DS_Store +### JBang ### +.jbang/ + ### Environment ### .env \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb7..5291372 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/README.md b/README.md index 8e8123a..0f3ff13 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,197 @@ -# langfuse-java +# Langfuse Java SDK -This repository contains an auto-generated Langfuse API client for Java based on our [API specification](https://github.com/langfuse/langfuse/tree/main/fern/apis/server). -See the [Langfuse API reference](https://api.reference.langfuse.com) for more details on the available endpoints. +Java SDK for [Langfuse](https://langfuse.com) -- the open-source LLM engineering platform for tracing, evaluation, prompt management, and metrics. -**Note:** We recommend to solve tracing via the [OpenTelemetry Instrumentation](https://langfuse.com/docs/opentelemetry/get-started) instead of using the Ingestion API directly. You can use the [OpenTelemetry Java SDK](https://github.com/open-telemetry/opentelemetry-java) and export spans to the [Langfuse OTel endpoint](https://langfuse.com/integrations/native/opentelemetry). -This allows for a more detailed and standardized tracing experience without the need to handle batching and updates internally. -Check out our [Spring AI Example](https://langfuse.com/docs/integrations/spring-ai) for more details. +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## Installation +## Modules -The recommended way to install the langfuse-java API client is via Maven Central: +| Module | Artifact | Description | +|--------|----------|-------------| +| [langfuse-java-api](langfuse-java-api/) | `com.langfuse:langfuse-java-api` | API interfaces, generated model types, and SPI | +| [langfuse-java-client](langfuse-java-client/) | `com.langfuse:langfuse-java-client` | Reference HTTP client (Jackson 2 & 3) | +| [langfuse-java-testcontainers](langfuse-java-testcontainers/) | `com.langfuse:langfuse-java-testcontainers` | Testcontainers support for integration testing | +| [langfuse-java-legacy](langfuse-java-legacy/) | `com.langfuse:langfuse-java` | Legacy fern-generated SDK (maintained for backward compatibility) | + +- **[langfuse-java-api](langfuse-java-api/)** defines the public API contract -- interfaces, generated model types from the [OpenAPI spec](https://cloud.langfuse.com/generated/api/openapi.yml), request objects with builders, and the `ServiceLoader`-based SPI for pluggable client implementations. +- **[langfuse-java-client](langfuse-java-client/)** is the reference HTTP client built on `java.net.http.HttpClient`. It supports both Jackson 2 and Jackson 3, request/response logging with sensitive header masking, and automatic HTTP/1.1 fallback for plain HTTP connections. +- **[langfuse-java-testcontainers](langfuse-java-testcontainers/)** provides `LangfuseContainer` for spinning up a complete Langfuse environment (PostgreSQL, ClickHouse, Redis, MinIO, web server, and worker) in integration tests via [Testcontainers](https://testcontainers.com). +- **[langfuse-java-legacy](langfuse-java-legacy/)** is the original fern-generated SDK, preserved for backward compatibility. New projects should use `langfuse-java-client` instead. See its [README](langfuse-java-legacy/README.md) for usage. + +## Design + +The Langfuse upstream project uses [Fern](https://buildwithfern.com/) to generate its SDKs. Fern produces an [OpenAPI specification](https://cloud.langfuse.com/generated/api/openapi.yml) but its generated Java code is not customizable -- it lacks builder patterns, dual Jackson support, request parameter objects, and JPMS modules. + +This SDK takes a different approach: it uses the Fern-generated OpenAPI spec as input to [openapi-generator](https://openapi-generator.tech/) with [custom Mustache templates](https://openapi-generator.tech/docs/templating) that produce the API interfaces, model types, and client implementations at build time. Almost all Java source in the `langfuse-java-api` and `langfuse-java-client` modules are generated during `mvn compile` -- only a handful of hand-coded classes provide the SPI wiring, Jackson version abstraction, and builder infrastructure. + +Because everything is generated at build time, the generated API interfaces, model types, and client implementations are never checked into version control -- only the OpenAPI spec, custom Mustache templates, and hand-coded infrastructure classes are stored in the repository. + +This means: +- Updating to a new Langfuse API version is a matter of dropping in the updated `openapi.yml` and rebuilding. +- The generated code includes builders, bean validation annotations, request parameter objects, dual Jackson 2/3 annotations, and `@JsonInclude(NON_EMPTY)` -- none of which the Fern-generated SDK provides. +- Framework integrations (Spring, Quarkus) can provide their own `LangfuseApiBuilderFactory` via `ServiceLoader` without depending on the reference client. +- The `langfuse-java-client` module includes a comprehensive integration test suite (sync and async) that uses the `langfuse-java-testcontainers` module to verify every API operation against a real Langfuse environment. + +## Requirements + +- Java 17+ +- Maven 3.8.1+ + +## Quick Start + +Add the client dependency to your project: ```xml com.langfuse - langfuse-java - 0.2.0 + langfuse-java-client + 0.2.1-SNAPSHOT ``` -## Usage +You also need a Jackson implementation on the classpath. The SDK supports both Jackson 2 and Jackson 3: + +```xml + + + com.fasterxml.jackson.core + jackson-databind + ${jackson2.version} + + + + + tools.jackson.core + jackson-databind + ${jackson3.version} + +``` -Instantiate the Langfuse Client with the respective endpoint and your API Keys. +### Create a client ```java -import com.langfuse.client.LangfuseClient; - -LangfuseClient client = LangfuseClient.builder() - .url("https://cloud.langfuse.com") // πŸ‡ͺπŸ‡Ί EU data region - // .url("https://us.cloud.langfuse.com") // πŸ‡ΊπŸ‡Έ US data region - // .url("http://localhost:3000") // 🏠 Local deployment - .credentials("pk-lf-...", "sk-lf-...") +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") // Langfuse public key + .password("sk-lf-...") // Langfuse secret key + .url("https://cloud.langfuse.com") .build(); ``` -An async client is also available via `AsyncLangfuseClient.builder()` with the same configuration options. +The builder uses `ServiceLoader` to discover the client implementation on the classpath. +When both Jackson 2 and Jackson 3 are present, Jackson 3 is preferred. -Make requests using the clients: +### Ingest a trace ```java -import com.langfuse.client.core.LangfuseClientApiException; -import com.langfuse.client.resources.prompts.types.PromptMetaListResponse; - -try { - PromptMetaListResponse prompts = client.prompts().list(); -} catch (LangfuseClientApiException error) { - System.out.println(error.body()); - System.out.println(error.statusCode()); -} +var response = langfuse.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("my-trace") + .userId("user-123") + .build()) + .build()))) + .build()) + .build()); ``` -## Testing +### Query traces -### Unit tests +```java +var traces = langfuse.trace().traceList( + TraceApi.APITraceListRequest.newBuilder() + .name("my-trace") + .limit(10) + .build()); + +traces.getData().forEach(trace -> + System.out.println(trace.getId() + ": " + trace.getName())); +``` -Unit tests (deserialization, query string mapping) run without any credentials: +### Check health -```bash -mvn test +```java +var health = langfuse.health().healthHealth(); +System.out.println("Status: " + health.getStatus()); // OK +System.out.println("Version: " + health.getVersion()); // 3.x.x ``` -### Integration tests +### Async API -Integration tests connect to a real Langfuse project. They require credentials and are excluded from `mvn test`. +Every synchronous API has an async counterpart that returns `CompletionStage`: -1. Copy `.env.example` to `.env` and fill in your API keys: - ```bash - cp .env.example .env - ``` +```java +langfuse.asyncHealth().healthHealth() + .thenAccept(health -> System.out.println("Status: " + health.getStatus())); +``` + +### Request/Response logging + +```java +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .logRequests() + .logResponses() + .prettyPrint() + .build(); +``` + +Sensitive headers (e.g. `Authorization`) are automatically masked in log output. + +## Integration Testing with Testcontainers + +Add the testcontainers module to your test dependencies: + +```xml + + com.langfuse + langfuse-java-testcontainers + 0.2.1-SNAPSHOT + test + +``` -2. Ensure your Langfuse project contains the following prompts: - - `test-chat-prompt` β€” chat type, at least one message with `role` and `content` - - `test-text-prompt` β€” text type, non-empty prompt text +Start a full Langfuse environment (PostgreSQL, ClickHouse, Redis, MinIO, Langfuse web + worker): -3. Run all tests (unit + integration): - ```bash - mvn verify - ``` +```java +var langfuse = new LangfuseContainer(); +langfuse.start(); - Or run only integration tests: - ```bash - mvn failsafe:integration-test - ``` +var client = LangfuseApi.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .build(); +``` -Integration tests skip gracefully when credentials are absent. +For sharing a single container across test classes, use the +[Testcontainers singleton pattern](https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers): -## Drafting a Release +```java +abstract class AbstractIntegrationTest { + static LangfuseContainer langfuse = new LangfuseContainer(); -Run `./mvnw release:prepare -DreleaseVersion=` with the version you want to create. -Push the changes including the tag. + static { + langfuse.start(); + } +} +``` -## Publishing to Maven Central +See the [testcontainers module README](langfuse-java-testcontainers/) for configuration options. -This project is configured to publish to Maven Central. -To publish to Maven Central, you need to configure the following secrets in your GitHub repository: +## Building -- `OSSRH_USERNAME`: Your Sonatype OSSRH username -- `OSSRH_PASSWORD`: Your Sonatype OSSRH password -- `GPG_PRIVATE_KEY`: Your GPG private key for signing artifacts -- `GPG_PASSPHRASE`: The passphrase for your GPG private key +```bash +./mvnw clean verify +``` -## Updating +## License -1. Ensure that langfuse-java is placed in the same directory as the main [langfuse](https://github.com/langfuse/langfuse) repository. -2. Setup a new Java fern generator using - ```yaml - - name: fernapi/fern-java-sdk - version: 3.38.1 - output: - location: local-file-system - path: ../../../../langfuse-java/src/main/java/com/langfuse/client/ - config: - client-class-name: LangfuseClient - ``` -3. Generate the new client code using `npx fern-api generate --api server`. -4. Manually set the `package` across all files to `com.langfuse.client`. -5. Verify that `LangfuseClientBuilder.setAuthentication()` uses `Basic` auth (not `Bearer`). -6. Adjust Javadoc strings with HTML properties as the apidocs package does not support them. -7. Commit the changes in langfuse-java and push them to the repository. +[MIT](LICENSE) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94ff06d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,179 @@ +# Make sure to update the credential placeholders with your own secrets. +# We mark them with # CHANGEME in the file below. +# In addition, we recommend to restrict inbound traffic on the host to langfuse-web (port 3000) and minio (port 9090) only. +# All other components are bound to localhost (127.0.0.1) to only accept connections from the local machine. +# External connections from other machines will not be able to reach these services directly. +name: langfuse-java +services: + langfuse-worker: + image: docker.io/langfuse/langfuse-worker:3 + restart: always + depends_on: &langfuse-depends-on + postgres: + condition: service_healthy + minio: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + ports: + - 127.0.0.1:3030:3030 + environment: &langfuse-worker-env + NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} + DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME + SALT: ${SALT:-mysalt} # CHANGEME + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -hex 32` + TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true} + LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} + CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000} + CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123} + CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME + CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false} + LANGFUSE_USE_AZURE_BLOB: ${LANGFUSE_USE_AZURE_BLOB:-false} + LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE: ${LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE:-false} + LANGFUSE_OCI_AUTH_TYPE: ${LANGFUSE_OCI_AUTH_TYPE:-workload_identity} + LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse} + LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto} + LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio} + LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME + LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000} + LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true} + LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/} + LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse} + LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto} + LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio} + LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME + LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090} + LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true} + LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/} + LANGFUSE_S3_BATCH_EXPORT_ENABLED: ${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false} + LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse} + LANGFUSE_S3_BATCH_EXPORT_PREFIX: ${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/} + LANGFUSE_S3_BATCH_EXPORT_REGION: ${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto} + LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT:-http://minio:9000} + LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: ${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT:-http://localhost:9090} + LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID:-minio} + LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME + LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: ${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true} + LANGFUSE_INGESTION_QUEUE_DELAY_MS: ${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-} + LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: ${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_AUTH: ${REDIS_AUTH:-myredissecret} # CHANGEME + REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false} + REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt} + REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt} + REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key} + EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} + SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL:-} + + langfuse-web: + image: docker.io/langfuse/langfuse:3 + restart: always + depends_on: *langfuse-depends-on + ports: + - 3000:3000 + environment: + <<: *langfuse-worker-env + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret} # CHANGEME + LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-langfuse-dev-org} + LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-Langfuse Dev Org} + LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-langfuse-dev-project} + LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-langfuse-dev} + LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-pk-lf-dev} + LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-sk-lf-dev} + LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-dev@langfuse.com} + LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-Dev User} + LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-password} + + clickhouse: + image: docker.io/clickhouse/clickhouse-server + restart: always + user: "101:101" + environment: + CLICKHOUSE_DB: default + CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME + volumes: + - langfuse_clickhouse_data:/var/lib/clickhouse + - langfuse_clickhouse_logs:/var/log/clickhouse-server + ports: + - 127.0.0.1:8123:8123 + - 127.0.0.1:9000:9000 + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 1s + + minio: + image: cgr.dev/chainguard/minio + restart: always + entrypoint: sh + # create the 'langfuse' bucket before starting the service + command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-miniosecret} # CHANGEME + ports: + - 9090:9000 + - 127.0.0.1:9091:9001 + volumes: + - langfuse_minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1s + timeout: 5s + retries: 5 + start_period: 1s + + redis: + image: docker.io/redis:7 + restart: always + # CHANGEME: row below to secure redis password + command: > + --requirepass ${REDIS_AUTH:-myredissecret} + --maxmemory-policy noeviction + ports: + - 127.0.0.1:6379:6379 + volumes: + - langfuse_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 3s + timeout: 10s + retries: 10 + + postgres: + image: docker.io/postgres:${POSTGRES_VERSION:-17} + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 10 + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} # CHANGEME + POSTGRES_DB: ${POSTGRES_DB:-postgres} + TZ: UTC + PGTZ: UTC + ports: + - 127.0.0.1:5432:5432 + volumes: + - langfuse_postgres_data:/var/lib/postgresql/data + +volumes: + langfuse_postgres_data: + driver: local + langfuse_clickhouse_data: + driver: local + langfuse_clickhouse_logs: + driver: local + langfuse_minio_data: + driver: local + langfuse_redis_data: + driver: local \ No newline at end of file diff --git a/langfuse-java-api/.mvn/wrapper/maven-wrapper.properties b/langfuse-java-api/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/langfuse-java-api/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/langfuse-java-api/README.md b/langfuse-java-api/README.md new file mode 100644 index 0000000..55be0d7 --- /dev/null +++ b/langfuse-java-api/README.md @@ -0,0 +1,95 @@ +# langfuse-java-api + +API interfaces, generated model types, and SPI for the Langfuse Java SDK. + +This module defines the public contract that client implementations must fulfill. It contains no HTTP or serialization logic -- only interfaces, model classes, and the `ServiceLoader`-based discovery mechanism. + +## Maven Coordinates + +```xml + + com.langfuse + langfuse-java-api + 0.2.1-SNAPSHOT + +``` + +## What's Inside + +### LangfuseApi + +The top-level interface providing access to all Langfuse API operations: + +```java +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .build(); + +// Synchronous +var health = langfuse.health().healthHealth(); + +// Asynchronous +langfuse.asyncHealth().healthHealth() + .thenAccept(h -> System.out.println(h.getStatus())); +``` + +Each API area has a sync and async accessor: + +| Accessor | Async Accessor | Description | +|----------|---------------|-------------| +| `health()` | `asyncHealth()` | Health checks | +| `ingestion()` | `asyncIngestion()` | Batch event ingestion | +| `trace()` | `asyncTrace()` | Trace CRUD | +| `prompts()` | `asyncPrompts()` | Prompt management | +| `scores()` | `asyncScores()` | Score queries | +| `datasets()` | `asyncDatasets()` | Dataset management | +| `observations()` | `asyncObservations()` | Observation queries | +| `sessions()` | `asyncSessions()` | Session management | +| ... | ... | 28 API areas total | + +### Builder Configuration + +The `LangfuseApiBuilder` interface defines the configuration options available to all implementations: + +| Method | Description | +|--------|-------------| +| `username(String)` | Langfuse public key (used for Basic auth) | +| `password(String)` | Langfuse secret key (used for Basic auth) | +| `url(String)` | Langfuse server base URL | +| `readTimeout(Duration)` | HTTP read timeout | +| `addHeader(String, String)` | Custom HTTP header sent with every request | +| `logRequests()` / `logRequests(boolean)` | Enable/disable HTTP request logging | +| `logResponses()` / `logResponses(boolean)` | Enable/disable HTTP response logging | +| `prettyPrint()` / `prettyPrint(boolean)` | Pretty-print JSON in logged requests and responses | + +### Request Objects + +API methods with parameters use request objects with builders, avoiding long parameter lists: + +```java +var traces = langfuse.trace().traceList( + TraceApi.APITraceListRequest.newBuilder() + .name("my-trace") + .userId("user-123") + .limit(10) + .build()); +``` + +No-argument methods (e.g. `healthHealth()`) keep their direct signature. + +### Generated Models + +Model types are generated from the [Langfuse OpenAPI spec](https://cloud.langfuse.com/generated/api/openapi.yml) with: + +- **Builders** on all model classes (`TraceBody.builder().name("...").build()`) +- **Protected constructors** -- all creation goes through builders +- **Bean Validation annotations** (`@NotNull`, `@Size`, etc.) from the OpenAPI schema +- **Dual Jackson 2 + 3 annotations** for serialization compatibility +- **Empty container defaults** -- Lists, Maps, and Sets initialize to empty, never null +- **`@JsonInclude(NON_EMPTY)`** -- empty optional containers are omitted from serialized JSON + +### SPI + +The `LangfuseApiBuilder` interface and `LangfuseApiBuilderFactory` SPI allow framework-specific implementations (e.g. Spring, Quarkus) to provide their own client without depending on `langfuse-java-client`. diff --git a/langfuse-java-api/mvnw b/langfuse-java-api/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/langfuse-java-api/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/langfuse-java-api/mvnw.cmd b/langfuse-java-api/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/langfuse-java-api/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/langfuse-java-api/pom.xml b/langfuse-java-api/pom.xml new file mode 100644 index 0000000..6130f7b --- /dev/null +++ b/langfuse-java-api/pom.xml @@ -0,0 +1,256 @@ + + 4.0.0 + + + com.langfuse + langfuse-java-parent + 0.2.1-SNAPSHOT + + + langfuse-java-api + jar + + langfuse-java-api + API interfaces, model types, and SPI for the Langfuse Java SDK + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + provided + + + + + tools.jackson.core + jackson-databind + provided + + + + + jakarta.validation + jakarta.validation-api + 3.1.1 + provided + + + + + org.jspecify + jspecify + 1.0.0 + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + + + generate-models + + generate + + + ${project.basedir}/../openapi.yml + java + com.langfuse.api.model + ${project.basedir}/src/main/templates + ${project.build.directory}/generated-sources/openapi-models + true + false + false + false + false + false + true + AbstractOpenApiSchema.java + + true + + + native + java8 + jackson + false + true + true + true + false + false + true + + + + + generate-api-interfaces + + generate + + + ${project.basedir}/../openapi.yml + java + ${project.build.directory}/generated-sources/openapi-apis + true + ${project.basedir}/src/main/templates + false + false + false + false + false + false + + true + + + native + java8 + false + true + true + true + + com.langfuse.api + com.langfuse.api.model + + + + generate-async-api-interfaces + + generate + + + ${project.basedir}/../openapi.yml + java + ${project.build.directory}/generated-sources/openapi-async-apis + true + ${project.basedir}/src/main/templates-async + false + false + false + false + false + false + + true + + + native + java8 + false + true + true + true + true + + com.langfuse.api + com.langfuse.api.model + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + fix-generic-type-references + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + + + dev.jbang + jbang-maven-plugin + + + relocate-to-subpackages + generate-sources + + run + + + + + ${project.build.directory}/generated-sources/openapi-apis + ${project.build.directory}/generated-sources/openapi-async-apis + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApi.java b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApi.java new file mode 100644 index 0000000..112164e --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApi.java @@ -0,0 +1,385 @@ +package com.langfuse.api; + +import java.time.Duration; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import com.langfuse.api.annotationQueues.AnnotationQueuesApi; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi; +import com.langfuse.api.comments.CommentsApi; +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.health.HealthApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyMetricsV1.LegacyMetricsV1Api; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api; +import com.langfuse.api.llmConnections.LlmConnectionsApi; +import com.langfuse.api.media.MediaApi; +import com.langfuse.api.metrics.MetricsApi; +import com.langfuse.api.models.ModelsApi; +import com.langfuse.api.observations.ObservationsApi; +import com.langfuse.api.opentelemetry.OpentelemetryApi; +import com.langfuse.api.organizations.OrganizationsApi; +import com.langfuse.api.projects.ProjectsApi; +import com.langfuse.api.promptVersion.PromptVersionApi; +import com.langfuse.api.prompts.PromptsApi; +import com.langfuse.api.scim.ScimApi; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; +import com.langfuse.api.scores.ScoresApi; +import com.langfuse.api.sessions.SessionsApi; +import com.langfuse.api.spi.LangfuseApiBuilderFactory; +import com.langfuse.api.spi.ServiceLoaderHelper; +import com.langfuse.api.trace.TraceApi; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi; + +/** + * Langfuse API interface. Entry point for all Langfuse operations. + * + *

Obtain an instance via the {@link #builder()} method, which uses {@link ServiceLoader} + * to discover the implementation on the classpath: + * + *

{@code
+ * LangfuseApi api = LangfuseApi.builder()
+ *     .username("pk-lf-...")
+ *     .password("sk-lf-...")
+ *     .url("https://cloud.langfuse.com")
+ *     .build();
+ *
+ * HealthResponse health = api.health().healthHealth();
+ * }
+ * + * @author Eric Deandrea + * @see LangfuseApiBuilder + * @see LangfuseApiBuilderFactory + */ +public interface LangfuseApi { + + /** + * Returns the Health API for checking the status of the Langfuse server. + * + * @return the {@link HealthApi} instance + */ + HealthApi health(); + + /** @return the async Health API */ + com.langfuse.api.health.async.HealthApi asyncHealth(); + + /** @return the Annotation Queues API */ + AnnotationQueuesApi annotationQueues(); + + /** @return the async Annotation Queues API */ + com.langfuse.api.annotationQueues.async.AnnotationQueuesApi asyncAnnotationQueues(); + + /** @return the Blob Storage Integrations API */ + BlobStorageIntegrationsApi blobStorageIntegrations(); + + /** @return the async Blob Storage Integrations API */ + com.langfuse.api.blobStorageIntegrations.async.BlobStorageIntegrationsApi asyncBlobStorageIntegrations(); + + /** @return the Comments API */ + CommentsApi comments(); + + /** @return the async Comments API */ + com.langfuse.api.comments.async.CommentsApi asyncComments(); + + /** @return the Dataset Items API */ + DatasetItemsApi datasetItems(); + + /** @return the async Dataset Items API */ + com.langfuse.api.datasetItems.async.DatasetItemsApi asyncDatasetItems(); + + /** @return the Dataset Run Items API */ + DatasetRunItemsApi datasetRunItems(); + + /** @return the async Dataset Run Items API */ + com.langfuse.api.datasetRunItems.async.DatasetRunItemsApi asyncDatasetRunItems(); + + /** @return the Datasets API */ + DatasetsApi datasets(); + + /** @return the async Datasets API */ + com.langfuse.api.datasets.async.DatasetsApi asyncDatasets(); + + /** @return the Ingestion API */ + IngestionApi ingestion(); + + /** @return the async Ingestion API */ + com.langfuse.api.ingestion.async.IngestionApi asyncIngestion(); + + /** @return the Legacy Metrics V1 API */ + LegacyMetricsV1Api legacyMetricsV1(); + + /** @return the async Legacy Metrics V1 API */ + com.langfuse.api.legacyMetricsV1.async.LegacyMetricsV1Api asyncLegacyMetricsV1(); + + /** @return the Legacy Observations V1 API */ + LegacyObservationsV1Api legacyObservationsV1(); + + /** @return the async Legacy Observations V1 API */ + com.langfuse.api.legacyObservationsV1.async.LegacyObservationsV1Api asyncLegacyObservationsV1(); + + /** @return the Legacy Score V1 API */ + LegacyScoreV1Api legacyScoreV1(); + + /** @return the async Legacy Score V1 API */ + com.langfuse.api.legacyScoreV1.async.LegacyScoreV1Api asyncLegacyScoreV1(); + + /** @return the LLM Connections API */ + LlmConnectionsApi llmConnections(); + + /** @return the async LLM Connections API */ + com.langfuse.api.llmConnections.async.LlmConnectionsApi asyncLlmConnections(); + + /** @return the Media API */ + MediaApi media(); + + /** @return the async Media API */ + com.langfuse.api.media.async.MediaApi asyncMedia(); + + /** @return the Metrics API */ + MetricsApi metrics(); + + /** @return the async Metrics API */ + com.langfuse.api.metrics.async.MetricsApi asyncMetrics(); + + /** @return the Models API */ + ModelsApi models(); + + /** @return the async Models API */ + com.langfuse.api.models.async.ModelsApi asyncModels(); + + /** @return the Observations API */ + ObservationsApi observations(); + + /** @return the async Observations API */ + com.langfuse.api.observations.async.ObservationsApi asyncObservations(); + + /** @return the OpenTelemetry API */ + OpentelemetryApi opentelemetry(); + + /** @return the async OpenTelemetry API */ + com.langfuse.api.opentelemetry.async.OpentelemetryApi asyncOpentelemetry(); + + /** @return the Organizations API */ + OrganizationsApi organizations(); + + /** @return the async Organizations API */ + com.langfuse.api.organizations.async.OrganizationsApi asyncOrganizations(); + + /** @return the Projects API */ + ProjectsApi projects(); + + /** @return the async Projects API */ + com.langfuse.api.projects.async.ProjectsApi asyncProjects(); + + /** @return the Prompt Version API */ + PromptVersionApi promptVersion(); + + /** @return the async Prompt Version API */ + com.langfuse.api.promptVersion.async.PromptVersionApi asyncPromptVersion(); + + /** @return the Prompts API */ + PromptsApi prompts(); + + /** @return the async Prompts API */ + com.langfuse.api.prompts.async.PromptsApi asyncPrompts(); + + /** @return the SCIM API */ + ScimApi scim(); + + /** @return the async SCIM API */ + com.langfuse.api.scim.async.ScimApi asyncScim(); + + /** @return the Score Configs API */ + ScoreConfigsApi scoreConfigs(); + + /** @return the async Score Configs API */ + com.langfuse.api.scoreConfigs.async.ScoreConfigsApi asyncScoreConfigs(); + + /** @return the Scores API */ + ScoresApi scores(); + + /** @return the async Scores API */ + com.langfuse.api.scores.async.ScoresApi asyncScores(); + + /** @return the Sessions API */ + SessionsApi sessions(); + + /** @return the async Sessions API */ + com.langfuse.api.sessions.async.SessionsApi asyncSessions(); + + /** @return the Trace API */ + TraceApi trace(); + + /** @return the async Trace API */ + com.langfuse.api.trace.async.TraceApi asyncTrace(); + + /** @return the Unstable Evaluation Rules API */ + UnstableEvaluationRulesApi unstableEvaluationRules(); + + /** @return the async Unstable Evaluation Rules API */ + com.langfuse.api.unstableEvaluationRules.async.UnstableEvaluationRulesApi asyncUnstableEvaluationRules(); + + /** @return the Unstable Evaluators API */ + UnstableEvaluatorsApi unstableEvaluators(); + + /** @return the async Unstable Evaluators API */ + com.langfuse.api.unstableEvaluators.async.UnstableEvaluatorsApi asyncUnstableEvaluators(); + + /** + * Creates a builder for constructing a {@link LangfuseApi} instance. + * + *

Uses {@link ServiceLoader} to discover exactly one {@link LangfuseApiBuilderFactory} + * on the classpath. If no factory is found, an {@link IllegalStateException} is thrown indicating + * a missing implementation dependency. If multiple factories are found, an + * {@link IllegalStateException} is thrown listing the conflicting implementations. + * + * @param the concrete {@link LangfuseApi} implementation type + * @param the concrete builder type + * @return a builder instance for constructing a {@link LangfuseApi} + * @throws IllegalStateException if zero or more than one factory is found on the classpath + */ + static > B builder() { + var factories = ServiceLoaderHelper.loadFactories(LangfuseApiBuilderFactory.class); + + if (factories.isEmpty()) { + throw new IllegalStateException( + "No instance of %s found to build a %s instance. You are probably missing a library on your classpath." + .formatted(LangfuseApiBuilderFactory.class.getName(), LangfuseApiBuilder.class.getName())); + } + + if (factories.size() > 1) { + throw new IllegalStateException( + "Multiple instances of %s found to build a %s instance: [%s]" + .formatted( + LangfuseApiBuilderFactory.class.getName(), + LangfuseApiBuilder.class.getName(), + factories.stream().map(f -> f.getClass().getName()).collect(Collectors.joining(", ")))); + } + + return factories.iterator() + .next() + .getBuilder(); + } + + /** + * Builder interface for constructing {@link LangfuseApi} implementations. + * + *

Implementations of this interface are provided by client libraries (e.g. {@code langfuse-java-client}). + * Downstream frameworks such as Spring or Quarkus may provide their own implementations using + * their preferred HTTP and serialization stacks. + * + * @author Eric Deandrea + * @param the concrete {@link LangfuseApi} implementation type being built + * @param the concrete builder type (for fluent method chaining) + */ + interface LangfuseApiBuilder> { + @SuppressWarnings("unchecked") + default B self() { + return (B) this; + } + + /** + * Sets the username (Langfuse public key) for Basic authentication. + * + * @param username the Langfuse public key + * @return this builder for method chaining + */ + B username(String username); + + /** + * Sets the password (Langfuse secret key) for Basic authentication. + * + * @param password the Langfuse secret key + * @return this builder for method chaining + */ + B password(String password); + + /** + * Sets the base URL of the Langfuse server. + * + * @param url the base URL (e.g. {@code "https://cloud.langfuse.com"}) + * @return this builder for method chaining + */ + B url(String url); + + /** + * Sets the read timeout for HTTP requests. + * + * @param readTimeout the read timeout duration + * @return this builder for method chaining + */ + B readTimeout(Duration readTimeout); + + /** + * Adds a custom HTTP header to be sent with every request. + * + * @param name the header name + * @param value the header value + * @return this builder for method chaining + */ + B addHeader(String name, String value); + + /** + * Enables logging of HTTP requests. Equivalent to {@code logRequests(true)}. + * + * @return this builder for method chaining + */ + default B logRequests() { + return logRequests(true); + } + + /** + * Configures whether HTTP request logging is enabled. + * + * @param logRequests {@code true} to enable request logging; {@code false} to disable it + * @return this builder for method chaining + */ + B logRequests(boolean logRequests); + + /** + * Enables logging of HTTP responses. Equivalent to {@code logResponses(true)}. + * + * @return this builder for method chaining + */ + default B logResponses() { + return logResponses(true); + } + + /** + * Configures whether HTTP response logging is enabled. + * + * @param logResponses {@code true} to enable response logging; {@code false} to disable it + * @return this builder for method chaining + */ + B logResponses(boolean logResponses); + + /** + * Enables pretty-printing of JSON in logged requests and responses. + * Equivalent to {@code prettyPrint(true)}. + * + * @return this builder for method chaining + */ + default B prettyPrint() { + return prettyPrint(true); + } + + /** + * Configures whether JSON in logged requests and responses is pretty-printed. + * + * @param prettyPrint {@code true} to enable pretty-printing; {@code false} for compact output + * @return this builder for method chaining + */ + B prettyPrint(boolean prettyPrint); + + /** + * Builds and returns a configured {@link LangfuseApi} instance. + * + * @return a new {@link LangfuseApi} instance + */ + T build(); + } +} diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApiException.java b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApiException.java new file mode 100644 index 0000000..eaf2b62 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApiException.java @@ -0,0 +1,68 @@ +package com.langfuse.api; + +/** + * Runtime exception thrown when a Langfuse API call fails. + * + *

This is an unchecked exception so that the {@link LangfuseApi} interface methods + * do not force callers to handle checked exceptions. Implementations convert + * transport-specific checked exceptions into this type. + * + * @author Eric Deandrea + */ +public class LangfuseApiException extends RuntimeException { + + private final int statusCode; + + /** + * @param message the error message + */ + public LangfuseApiException(String message) { + super(message); + this.statusCode = 0; + } + + /** + * @param message the error message + * @param cause the underlying cause + */ + public LangfuseApiException(String message, Throwable cause) { + super(message, cause); + this.statusCode = 0; + } + + /** + * @param message the error message + * @param statusCode the HTTP status code returned by the server + */ + public LangfuseApiException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + /** + * @param message the error message + * @param statusCode the HTTP status code returned by the server + * @param cause the underlying cause + */ + public LangfuseApiException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + + /** + * @param cause the underlying cause + */ + public LangfuseApiException(Throwable cause) { + super(cause); + this.statusCode = 0; + } + + /** + * Returns the HTTP status code from the failed API call, or {@code 0} if not applicable. + * + * @return the HTTP status code + */ + public int getStatusCode() { + return statusCode; + } +} diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApis.java b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApis.java new file mode 100644 index 0000000..8108aed --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApis.java @@ -0,0 +1,61 @@ +package com.langfuse.api; + +import com.langfuse.api.annotationQueues.AnnotationQueuesApi; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi; +import com.langfuse.api.comments.CommentsApi; +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.health.HealthApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyMetricsV1.LegacyMetricsV1Api; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api; +import com.langfuse.api.llmConnections.LlmConnectionsApi; +import com.langfuse.api.media.MediaApi; +import com.langfuse.api.metrics.MetricsApi; +import com.langfuse.api.models.ModelsApi; +import com.langfuse.api.observations.ObservationsApi; +import com.langfuse.api.opentelemetry.OpentelemetryApi; +import com.langfuse.api.organizations.OrganizationsApi; +import com.langfuse.api.projects.ProjectsApi; +import com.langfuse.api.promptVersion.PromptVersionApi; +import com.langfuse.api.prompts.PromptsApi; +import com.langfuse.api.scim.ScimApi; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; +import com.langfuse.api.scores.ScoresApi; +import com.langfuse.api.sessions.SessionsApi; +import com.langfuse.api.trace.TraceApi; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi; + +public interface LangfuseApis extends + AnnotationQueuesApi, + BlobStorageIntegrationsApi, + CommentsApi, + DatasetItemsApi, + DatasetRunItemsApi, + DatasetsApi, + HealthApi, + IngestionApi, + LegacyMetricsV1Api, + LegacyObservationsV1Api, + LegacyScoreV1Api, + LlmConnectionsApi, + MediaApi, + MetricsApi, + ModelsApi, + ObservationsApi, + OpentelemetryApi, + OrganizationsApi, + ProjectsApi, + PromptVersionApi, + PromptsApi, + ScimApi, + ScoreConfigsApi, + ScoresApi, + SessionsApi, + TraceApi, + UnstableEvaluationRulesApi, + UnstableEvaluatorsApi { +} diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseAsyncApis.java b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseAsyncApis.java new file mode 100644 index 0000000..8dd3af1 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseAsyncApis.java @@ -0,0 +1,61 @@ +package com.langfuse.api; + +import com.langfuse.api.annotationQueues.async.AnnotationQueuesApi; +import com.langfuse.api.blobStorageIntegrations.async.BlobStorageIntegrationsApi; +import com.langfuse.api.comments.async.CommentsApi; +import com.langfuse.api.datasetItems.async.DatasetItemsApi; +import com.langfuse.api.datasetRunItems.async.DatasetRunItemsApi; +import com.langfuse.api.datasets.async.DatasetsApi; +import com.langfuse.api.health.async.HealthApi; +import com.langfuse.api.ingestion.async.IngestionApi; +import com.langfuse.api.legacyMetricsV1.async.LegacyMetricsV1Api; +import com.langfuse.api.legacyObservationsV1.async.LegacyObservationsV1Api; +import com.langfuse.api.legacyScoreV1.async.LegacyScoreV1Api; +import com.langfuse.api.llmConnections.async.LlmConnectionsApi; +import com.langfuse.api.media.async.MediaApi; +import com.langfuse.api.metrics.async.MetricsApi; +import com.langfuse.api.models.async.ModelsApi; +import com.langfuse.api.observations.async.ObservationsApi; +import com.langfuse.api.opentelemetry.async.OpentelemetryApi; +import com.langfuse.api.organizations.async.OrganizationsApi; +import com.langfuse.api.projects.async.ProjectsApi; +import com.langfuse.api.promptVersion.async.PromptVersionApi; +import com.langfuse.api.prompts.async.PromptsApi; +import com.langfuse.api.scim.async.ScimApi; +import com.langfuse.api.scoreConfigs.async.ScoreConfigsApi; +import com.langfuse.api.scores.async.ScoresApi; +import com.langfuse.api.sessions.async.SessionsApi; +import com.langfuse.api.trace.async.TraceApi; +import com.langfuse.api.unstableEvaluationRules.async.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.async.UnstableEvaluatorsApi; + +public interface LangfuseAsyncApis extends + AnnotationQueuesApi, + BlobStorageIntegrationsApi, + CommentsApi, + DatasetItemsApi, + DatasetRunItemsApi, + DatasetsApi, + HealthApi, + IngestionApi, + LegacyMetricsV1Api, + LegacyObservationsV1Api, + LegacyScoreV1Api, + LlmConnectionsApi, + MediaApi, + MetricsApi, + ModelsApi, + ObservationsApi, + OpentelemetryApi, + OrganizationsApi, + ProjectsApi, + PromptVersionApi, + PromptsApi, + ScimApi, + ScoreConfigsApi, + ScoresApi, + SessionsApi, + TraceApi, + UnstableEvaluationRulesApi, + UnstableEvaluatorsApi { +} diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/spi/LangfuseApiBuilderFactory.java b/langfuse-java-api/src/main/java/com/langfuse/api/spi/LangfuseApiBuilderFactory.java new file mode 100644 index 0000000..1e70f38 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/spi/LangfuseApiBuilderFactory.java @@ -0,0 +1,25 @@ +package com.langfuse.api.spi; + +import com.langfuse.api.LangfuseApi; +import com.langfuse.api.LangfuseApi.LangfuseApiBuilder; + +/** + * SPI factory interface for creating {@link LangfuseApiBuilder} instances. + * + *

Implementations are discovered via {@link java.util.ServiceLoader} and must be registered + * in {@code META-INF/services/com.langfuse.api.spi.LangfuseApiBuilderFactory}. + * + * @author Eric Deandrea + * @see LangfuseApi#builder() + */ +public interface LangfuseApiBuilderFactory { + + /** + * Creates a new builder for constructing a {@link LangfuseApi} implementation. + * + * @param the concrete {@link LangfuseApi} implementation type + * @param the concrete builder type + * @return a new builder instance + */ + > B getBuilder(); +} diff --git a/langfuse-java-api/src/main/java/com/langfuse/api/spi/ServiceLoaderHelper.java b/langfuse-java-api/src/main/java/com/langfuse/api/spi/ServiceLoaderHelper.java new file mode 100644 index 0000000..7d68475 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/spi/ServiceLoaderHelper.java @@ -0,0 +1,74 @@ +package com.langfuse.api.spi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; + +/** + * Utility for loading SPI implementations via {@link ServiceLoader}. + * + *

Attempts multiple classloader strategies to ensure discovery works across + * different deployment environments (application servers, OSGi, modular classloaders). + * + * @author Eric Deandrea + */ +public final class ServiceLoaderHelper { + + private ServiceLoaderHelper() { + } + + /** + * Loads a single factory of the given type, if available. + * + * @param the factory type + * @param clazz the factory class to load + * @return an {@link Optional} containing the factory, or empty if none found + */ + public static Optional loadFactory(Class clazz) { + var factories = loadFactories(clazz, null); + return factories.isEmpty() ? Optional.empty() : Optional.ofNullable(factories.iterator().next()); + } + + /** + * Loads all factories of the given type using the thread context classloader. + * + * @param the factory type + * @param clazz the factory class to load + * @return a collection of discovered factories (may be empty) + */ + public static Collection loadFactories(Class clazz) { + return loadFactories(clazz, null); + } + + /** + * Loads all factories of the given type using the specified classloader. + * Falls back to this class's classloader if the initial lookup finds nothing. + * + * @param the factory type + * @param clazz the factory class to load + * @param classLoader the classloader to use, or {@code null} for the thread context classloader + * @return a collection of discovered factories (may be empty) + */ + public static Collection loadFactories(Class clazz, ClassLoader classLoader) { + var factories = (classLoader != null) + ? loadAll(ServiceLoader.load(clazz, classLoader)) + : loadAll(ServiceLoader.load(clazz)); + + if (factories.isEmpty()) { + factories = loadAll(ServiceLoader.load(clazz, ServiceLoaderHelper.class.getClassLoader())); + } + + return factories.stream() + .filter(Objects::nonNull) + .toList(); + } + + private static List loadAll(ServiceLoader loader) { + List list = new ArrayList<>(); + loader.iterator().forEachRemaining(list::add); + return list; + } +} diff --git a/langfuse-java-api/src/main/java/module-info.java b/langfuse-java-api/src/main/java/module-info.java new file mode 100644 index 0000000..71a03f7 --- /dev/null +++ b/langfuse-java-api/src/main/java/module-info.java @@ -0,0 +1,73 @@ +open module com.langfuse.api { + requires static org.jspecify; + requires static jakarta.annotation; + requires static jakarta.validation; + requires static com.fasterxml.jackson.annotation; + requires static com.fasterxml.jackson.core; + requires static com.fasterxml.jackson.databind; + requires static tools.jackson.core; + requires static tools.jackson.databind; + requires java.logging; + + exports com.langfuse.api; + exports com.langfuse.api.spi; + exports com.langfuse.api.model; + exports com.langfuse.api.annotationQueues; + exports com.langfuse.api.annotationQueues.async; + exports com.langfuse.api.blobStorageIntegrations; + exports com.langfuse.api.blobStorageIntegrations.async; + exports com.langfuse.api.comments; + exports com.langfuse.api.comments.async; + exports com.langfuse.api.datasetItems; + exports com.langfuse.api.datasetItems.async; + exports com.langfuse.api.datasetRunItems; + exports com.langfuse.api.datasetRunItems.async; + exports com.langfuse.api.datasets; + exports com.langfuse.api.datasets.async; + exports com.langfuse.api.health; + exports com.langfuse.api.health.async; + exports com.langfuse.api.ingestion; + exports com.langfuse.api.ingestion.async; + exports com.langfuse.api.legacyMetricsV1; + exports com.langfuse.api.legacyMetricsV1.async; + exports com.langfuse.api.legacyObservationsV1; + exports com.langfuse.api.legacyObservationsV1.async; + exports com.langfuse.api.legacyScoreV1; + exports com.langfuse.api.legacyScoreV1.async; + exports com.langfuse.api.llmConnections; + exports com.langfuse.api.llmConnections.async; + exports com.langfuse.api.media; + exports com.langfuse.api.media.async; + exports com.langfuse.api.metrics; + exports com.langfuse.api.metrics.async; + exports com.langfuse.api.models; + exports com.langfuse.api.models.async; + exports com.langfuse.api.observations; + exports com.langfuse.api.observations.async; + exports com.langfuse.api.opentelemetry; + exports com.langfuse.api.opentelemetry.async; + exports com.langfuse.api.organizations; + exports com.langfuse.api.organizations.async; + exports com.langfuse.api.projects; + exports com.langfuse.api.projects.async; + exports com.langfuse.api.promptVersion; + exports com.langfuse.api.promptVersion.async; + exports com.langfuse.api.prompts; + exports com.langfuse.api.prompts.async; + exports com.langfuse.api.scim; + exports com.langfuse.api.scim.async; + exports com.langfuse.api.scoreConfigs; + exports com.langfuse.api.scoreConfigs.async; + exports com.langfuse.api.scores; + exports com.langfuse.api.scores.async; + exports com.langfuse.api.sessions; + exports com.langfuse.api.sessions.async; + exports com.langfuse.api.trace; + exports com.langfuse.api.trace.async; + exports com.langfuse.api.unstableEvaluationRules; + exports com.langfuse.api.unstableEvaluationRules.async; + exports com.langfuse.api.unstableEvaluators; + exports com.langfuse.api.unstableEvaluators.async; + + uses com.langfuse.api.spi.LangfuseApiBuilderFactory; +} diff --git a/langfuse-java-api/src/main/templates-async/api.mustache b/langfuse-java-api/src/main/templates-async/api.mustache new file mode 100644 index 0000000..3653df8 --- /dev/null +++ b/langfuse-java-api/src/main/templates-async/api.mustache @@ -0,0 +1,78 @@ +{{#operations}} +package {{package}}.{{classVarName}}.async; +{{/operations}} + +{{#imports}} +import {{import}}; +{{/imports}} +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +{{#operations}} +/** + * Langfuse {{classname}} - asynchronous API interface. + * + *

All methods return {@link CompletionStage} to support non-blocking usage + * and integration with reactive frameworks. + */ +public interface {{classname}} { +{{#operation}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + * @param apiRequest {@link com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + {{#returnType}} + * @return a {@link CompletionStage} containing {{{returnType}}} + {{/returnType}} + {{^returnType}} + * @return a {@link CompletionStage} completing when the operation finishes + {{/returnType}} + */ + {{#returnType}}CompletionStage<{{{returnType}}}>{{/returnType}}{{^returnType}}CompletionStage{{/returnType}} {{operationId}}(com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest); + {{/hasParams}} + {{^hasParams}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + {{#returnType}} + * @return a {@link CompletionStage} containing {{{returnType}}} + {{/returnType}} + {{^returnType}} + * @return a {@link CompletionStage} completing when the operation finishes + {{/returnType}} + */ + {{#returnType}}CompletionStage<{{{returnType}}}>{{/returnType}}{{^returnType}}CompletionStage{{/returnType}} {{operationId}}(); + {{/hasParams}} + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional){{/required}} + {{/allParams}} + {{#returnType}} + * @return a {@link CompletionStage} containing {{{returnType}}} + {{/returnType}} + {{^returnType}} + * @return a {@link CompletionStage} completing when the operation finishes + {{/returnType}} + */ + {{#returnType}}CompletionStage<{{{returnType}}}>{{/returnType}}{{^returnType}}CompletionStage{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + {{/vendorExtensions.x-group-parameters}} +{{/operation}} +} +{{/operations}} diff --git a/langfuse-java-api/src/main/templates/anyof_model.mustache b/langfuse-java-api/src/main/templates/anyof_model.mustache new file mode 100644 index 0000000..fd2352a --- /dev/null +++ b/langfuse-java-api/src/main/templates/anyof_model.mustache @@ -0,0 +1,400 @@ +{{^useJackson3}} +import java.io.IOException; +{{/useJackson3}} +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +{{^useJackson3}} +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +{{/useJackson3}} +{{#useJackson3}} +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.SerializationContext; +{{/useJackson3}} +import {{jacksonPackage}}.core.JsonGenerator; +import {{jacksonPackage}}.core.JsonParser; +import {{jacksonPackage}}.databind.DeserializationContext; +import {{jacksonPackage}}.databind.JsonNode; +import {{jacksonPackage}}.databind.annotation.JsonDeserialize; +import {{jacksonPackage}}.databind.annotation.JsonSerialize; +import {{jacksonPackage}}.databind.deser.std.StdDeserializer; +import {{jacksonPackage}}.databind.ser.std.StdSerializer; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>xmlAnnotation}} +@JsonDeserialize(using={{classname}}.{{classname}}Deserializer.class) +@JsonSerialize(using = {{classname}}.{{classname}}Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}} implements {{{.}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-implements}} { + private static final Logger log = Logger.getLogger({{classname}}.class.getName()); + + public static class {{classname}}Serializer extends StdSerializer<{{classname}}> { + public {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + public {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, JsonGenerator jgen, {{^useJackson3}}SerializerProvider provider{{/useJackson3}}{{#useJackson3}}SerializationContext serializationContext{{/useJackson3}}) throws {{^useJackson3}}IOException, JsonProcessingException{{/useJackson3}}{{#useJackson3}}JacksonException{{/useJackson3}} { +{{^useJackson3}} + jgen.writeObject(value.getActualInstance()); +{{/useJackson3}} +{{#useJackson3}} + serializationContext.writeValue(jgen, value.getActualInstance()); +{{/useJackson3}} + } + } + + public static class {{classname}}Deserializer extends StdDeserializer<{{classname}}> { + public {{classname}}Deserializer() { + this({{classname}}.class); + } + + public {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(JsonParser jp, DeserializationContext ctxt) throws {{^useJackson3}}IOException, JsonProcessingException{{/useJackson3}}{{#useJackson3}}JacksonException{{/useJackson3}} { + JsonNode tree = ctxt.readTree(jp); + + Object deserialized = null; + {{#discriminator}} + Class cls = null; // discriminator lookup not available in API module + if (cls != null) { + // When the OAS schema includes a discriminator, use the discriminator value to + // discriminate the anyOf schemas. + // Get the discriminator mapping value to get the class. +{{^useJackson3}} + deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); +{{/useJackson3}} +{{#useJackson3}} + deserialized = ctxt.readTreeAsValue(tree, ctxt.constructType(cls)); +{{/useJackson3}} + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + {{/discriminator}} + {{#anyOf}} + // deserialize {{{.}}} + try { +{{^useJackson3}} + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); +{{/useJackson3}} +{{#useJackson3}} + deserialized = ctxt.readTreeAsValue(tree, {{{.}}}.class); +{{/useJackson3}} + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } catch (Exception e) { + // deserialization failed, continue, log to help debugging + log.log(Level.FINER, "Input data does not match '{{classname}}'", e); + } + + {{/anyOf}} +{{^useJackson3}} + throw new IOException("Failed deserialization for {{classname}}: no match found"); +{{/useJackson3}} +{{#useJackson3}} + throw DatabindException.from(jp, "Failed deserialization for {{classname}}: no match found"); +{{/useJackson3}} + } + + /** + * Handle deserialization of the 'null' value. + */ + @Override +{{^useJackson3}} + public {{classname}} getNullValue(DeserializationContext ctxt) throws JsonMappingException { +{{/useJackson3}} +{{#useJackson3}} + public {{classname}} getNullValue(DeserializationContext ctxt) { +{{/useJackson3}} + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} +{{^useJackson3}} + throw new JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); +{{/useJackson3}} +{{#useJackson3}} + throw DatabindException.from(ctxt.getParser(), "{{classname}} cannot be null"); +{{/useJackson3}} + {{/isNullable}} + } + } + + // store a list of schema names defined in anyOf + public static final Map> schemas = new HashMap>(); + + public {{classname}}() { + super("anyOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + } +{{> libraries/native/additional_properties }} + + {{#additionalPropertiesType}} + /** + * Return true if this {{name}} object is equal to o. + */ + @Override + public boolean equals(Object o) { + return super.equals(o) && Objects.equals(this.additionalProperties, (({{classname}})o).additionalProperties); + } + + @Override + public int hashCode() { + return Objects.hash(getActualInstance(), isNullable(), getSchemaType(), additionalProperties); + } + {{/additionalPropertiesType}} + {{#anyOf}} + public {{classname}}({{{.}}} o) { + super("anyOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + setActualInstance(o); + } + + {{/anyOf}} + static { + {{#anyOf}} + schemas.put("{{{.}}}", {{{.}}}.class); + {{/anyOf}} + {{#discriminator}} + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + {{/discriminator}} + } + + @Override + public Map> getSchemas() { + return {{classname}}.schemas; + } + + /** + * Set the instance that matches the anyOf child schema, check + * the instance parameter is valid against the anyOf child schemas: + * {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}} + * + * It could be an instance of the 'anyOf' schemas. + * The anyOf child schemas may themselves be a composed schema (allOf, anyOf, anyOf). + */ + @Override + public void setActualInstance(Object instance) { + {{#isNullable}} + if (instance == null) { + super.setActualInstance(instance); + return; + } + + {{/isNullable}} + {{#anyOf}} + if ({{{.}}}.class.isAssignableFrom(instance.getClass())) { + super.setActualInstance(instance); + return; + } + + {{/anyOf}} + throw new RuntimeException("Invalid instance type. Must be {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}"); + } + + /** + * Get the actual instance, which can be the following: + * {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}} + * + * @return The actual instance ({{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}) + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + {{#anyOf}} + /** + * Get the actual instance of `{{{.}}}`. If the actual instance is not `{{{.}}}`, + * the ClassCastException will be thrown. + * + * @return The actual instance of `{{{.}}}` + * @throws ClassCastException if the instance is not `{{{.}}}` + */ + public {{{.}}} get{{{.}}}() throws ClassCastException { + return ({{{.}}})super.getActualInstance(); + } + + {{/anyOf}} + +{{! toUrlQueryString removed β€” references ApiClient which does not belong in the API module }} +{{#_dummyBlockToSkipUrlQuery_}} + + /** + * Convert the instance into URL query string. + * + * @return URL query string + */ + public String toUrlQueryString() { + return toUrlQueryString(null); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + String suffix = ""; + String containerSuffix = ""; + String containerPrefix = ""; + if (prefix == null) { + // style=form, explode=true, e.g. /pet?name=cat&type=manx + prefix = ""; + } else { + // deepObject style e.g. /pet?id[name]=cat&id[type]=manx + prefix = prefix + "["; + suffix = "]"; + containerSuffix = "]"; + containerPrefix = "["; + } + + StringJoiner joiner = new StringJoiner("&"); + + {{#composedSchemas.oneOf}} + {{^vendorExtensions.x-duplicated-data-type}} + if (getActualInstance() instanceof {{{dataType}}}) { + {{#isArray}} + {{#items.isPrimitiveType}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(_item)))); + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(getActualInstance().get(i))))); + } + } + {{/uniqueItems}} + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + {{#items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(_item.toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if ((({{{dataType}}})getActualInstance()).get(i) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(i).toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{^items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(_item)))); + } + i++; + } + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if (getActualInstance().get(i) != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf((({{{dataType}}})getActualInstance()).get(i))))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{/items.isPrimitiveType}} + {{/isArray}} + {{^isArray}} + {{#isMap}} + {{#items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, _key, containerSuffix), + getActualInstance().get(_key), ApiClient.urlEncode(String.valueOf((({{{dataType}}})getActualInstance()).get(_key))))); + } + } + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + if ((({{{dataType}}})getActualInstance()).get(_key) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(_key).toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, _key, containerSuffix)))); + } + } + } + {{/items.isPrimitiveType}} + {{/isMap}} + {{^isMap}} + {{#isPrimitiveType}} + if (getActualInstance() != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{{baseName}}}%s=%s", prefix, suffix, ApiClient.urlEncode(String.valueOf(getActualInstance())))); + } + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isModel}} + if (getActualInstance() != null) { + joiner.add((({{{dataType}}})getActualInstance()).toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); + } + {{/isModel}} + {{^isModel}} + if (getActualInstance() != null) { + joiner.add(String.format(Locale.ROOT, "%s{{{baseName}}}%s=%s", prefix, suffix, ApiClient.urlEncode(String.valueOf(getActualInstance())))); + } + {{/isModel}} + {{/isPrimitiveType}} + {{/isMap}} + {{/isArray}} + return joiner.toString(); + } + {{/vendorExtensions.x-duplicated-data-type}} + {{/composedSchemas.oneOf}} + return null; + } +{{/_dummyBlockToSkipUrlQuery_}} + +} diff --git a/langfuse-java-api/src/main/templates/api.mustache b/langfuse-java-api/src/main/templates/api.mustache new file mode 100644 index 0000000..6eaed84 --- /dev/null +++ b/langfuse-java-api/src/main/templates/api.mustache @@ -0,0 +1,127 @@ +{{#operations}} +package {{package}}.{{classVarName}}; +{{/operations}} + +{{#imports}} +import {{import}}; +{{/imports}} +import java.util.List; +import java.util.Map; +import java.util.Set; + +{{#operations}} +/** + * Langfuse {{classname}} - synchronous API interface. + */ +public interface {{classname}} { +{{#operation}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + * @param apiRequest {@link API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + {{#returnType}} + * @return {{{returnType}}} + {{/returnType}} + */ + {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}(API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest); + + /** + * Request object for {@link #{{operationId}}(API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request)}. + */ + final class API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request { + {{#allParams}} + private {{{dataType}}} {{paramName}}{{#isArray}} = new java.util.ArrayList<>(){{/isArray}}{{#isMap}} = new java.util.HashMap<>(){{/isMap}}; + {{/allParams}} + + private API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request(Builder builder) { + {{#allParams}} + this.{{paramName}} = builder.{{paramName}}; + {{/allParams}} + } + {{#allParams}} + + /** + * @return {{description}} + */ + public {{{dataType}}} {{paramName}}() { + return {{paramName}}; + } + {{/allParams}} + + /** + * Creates a new builder. + * + * @return a new {@link Builder} + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for {@link API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request}. + */ + public static class Builder { + {{#allParams}} + private {{{dataType}}} {{paramName}}{{#isArray}} = new java.util.ArrayList<>(){{/isArray}}{{#isMap}} = new java.util.HashMap<>(){{/isMap}}; + {{/allParams}} + {{#allParams}} + + /** + * @param {{paramName}} {{description}} + * @return this builder + */ + public Builder {{paramName}}({{{dataType}}} {{paramName}}) { + this.{{paramName}} = {{paramName}}; + return this; + } + {{/allParams}} + + /** + * Builds the request object. + * + * @return the request + */ + public API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request build() { + return new API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request(this); + } + } + } + {{/hasParams}} + {{^hasParams}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + {{#returnType}} + * @return {{{returnType}}} + {{/returnType}} + */ + {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}(); + {{/hasParams}} + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + + /** + * {{summary}} + *{{#notes}} + *

{{notes}}{{/notes}} + * + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional){{/required}} + {{/allParams}} + {{#returnType}} + * @return {{{returnType}}} + {{/returnType}} + */ + {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + {{/vendorExtensions.x-group-parameters}} +{{/operation}} +} +{{/operations}} diff --git a/langfuse-java-api/src/main/templates/javaBuilder.mustache b/langfuse-java-api/src/main/templates/javaBuilder.mustache new file mode 100644 index 0000000..5fdcda5 --- /dev/null +++ b/langfuse-java-api/src/main/templates/javaBuilder.mustache @@ -0,0 +1,84 @@ + @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public static class Builder {{#parentModel}}extends {{classname}}.Builder {{/parentModel}}{ + + private {{classname}} instance; + + public Builder() { + this(new {{classname}}()); + } + + protected Builder({{classname}} instance) { + {{#parentModel}} + super(instance); + {{/parentModel}} + this.instance = instance; + } + + {{#vars}} + public {{classname}}.Builder {{name}}({{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.instance.{{name}} = JsonNullable.<{{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.instance.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + {{#vendorExtensions.x-is-jackson-optional-nullable}} + public {{classname}}.Builder {{name}}(JsonNullable<{{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}}> {{name}}) { + this.instance.{{name}} = {{name}}; + return this; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{/vars}} + +{{#parentVars}} + public {{classname}}.Builder {{name}}({{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}) { // inherited: {{isInherited}} + super.{{name}}({{name}}); + return this; + } + {{#vendorExtensions.x-is-jackson-optional-nullable}} + public {{classname}}.Builder {{name}}(JsonNullable<{{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}}> {{name}}) { + this.instance.{{name}} = {{name}}; + return this; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{/parentVars}} + + /** + * returns a built {{classname}} instance. + * + * The builder is not reusable. + */ + public {{classname}} build() { + try { + return this.instance; + } finally { + // ensure that this.instance is not reused{{#parentModel}} + super.build();{{/parentModel}} + this.instance = null; + } + } + + @Override + public String toString() { + return getClass() + "=(" + instance + ")"; + } + } + + /** + * Create a builder with no initialized field. + */ + public static {{classname}}.Builder builder() { + return new {{classname}}.Builder(); + } + + /** + * Create a builder with a shallow copy of this instance. + */ + public {{classname}}.Builder toBuilder() { + return new {{classname}}.Builder(){{#allVars}} + .{{name}}({{getter}}()){{/allVars}}; + } diff --git a/langfuse-java-api/src/main/templates/libraries/native/additional_properties.mustache b/langfuse-java-api/src/main/templates/libraries/native/additional_properties.mustache new file mode 100644 index 0000000..e935084 --- /dev/null +++ b/langfuse-java-api/src/main/templates/libraries/native/additional_properties.mustache @@ -0,0 +1,39 @@ +{{#additionalPropertiesType}} + /** + * A container for additional, undeclared properties. + * This is a holder for any undeclared properties as specified with + * the 'additionalProperties' keyword in the OAS document. + */ + private Map additionalProperties = new java.util.HashMap<>(); + + /** + * Set the additional (undeclared) property with the specified name and value. + * If the property does not already exist, create it otherwise replace it. + * @param key the name of the property + * @param value the value of the property + * @return self reference + */ + @JsonAnySetter + public {{classname}} putAdditionalProperty(String key, {{{.}}} value) { + this.additionalProperties.put(key, value); + return this; + } + + /** + * Return the additional (undeclared) properties. + * @return the additional (undeclared) properties + */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + /** + * Return the additional (undeclared) property with the specified name. + * @param key the name of the property + * @return the additional (undeclared) property with the specified name + */ + public java.util.Optional<{{{.}}}> getAdditionalProperty(String key) { + return java.util.Optional.ofNullable(this.additionalProperties.get(key)); + } +{{/additionalPropertiesType}} diff --git a/langfuse-java-api/src/main/templates/model.mustache b/langfuse-java-api/src/main/templates/model.mustache new file mode 100644 index 0000000..e3d33ef --- /dev/null +++ b/langfuse-java-api/src/main/templates/model.mustache @@ -0,0 +1,59 @@ +{{>licenseInfo}} + +package {{package}}; + +{{#useReflectionEqualsHashCode}} +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +{{/useReflectionEqualsHashCode}} +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +{{#supportUrlQuery}} +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.StringJoiner; +{{/supportUrlQuery}} +import java.util.Objects; +import java.util.Map; +import java.util.HashMap; +{{#imports}} +import {{import}}; +{{/imports}} +{{#serializableModel}} +import java.io.Serializable; +{{/serializableModel}} +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +{{#withXml}} +import com.fasterxml.jackson.dataformat.xml.annotation.*; +{{/withXml}} +{{#vendorExtensions.x-has-readonly-properties}} +import com.fasterxml.jackson.annotation.JsonCreator; +{{/vendorExtensions.x-has-readonly-properties}} +{{/jackson}} +{{#withXml}} +import {{javaxPackage}}.xml.bind.annotation.*; +{{/withXml}} +{{#parcelableModel}} +import android.os.Parcelable; +import android.os.Parcel; +{{/parcelableModel}} +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} +{{#performBeanValidation}} +import org.hibernate.validator.constraints.*; +{{/performBeanValidation}} + +{{#models}} +{{#model}} +{{#oneOf}} +{{#-first}} +import {{jacksonPackage}}.core.type.TypeReference; +{{/-first}} +{{/oneOf}} + +{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>oneof_model}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>anyof_model}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>pojo}}{{/anyOf}}{{/oneOf}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/langfuse-java-api/src/main/templates/oneof_model.mustache b/langfuse-java-api/src/main/templates/oneof_model.mustache new file mode 100644 index 0000000..6907454 --- /dev/null +++ b/langfuse-java-api/src/main/templates/oneof_model.mustache @@ -0,0 +1,448 @@ +{{^useJackson3}} +import java.io.IOException; +{{/useJackson3}} +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +{{^useJackson3}} +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +{{/useJackson3}} +{{#useJackson3}} +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.SerializationContext; +{{/useJackson3}} +import {{jacksonPackage}}.core.JsonGenerator; +import {{jacksonPackage}}.core.JsonParser; +import {{jacksonPackage}}.core.JsonToken; +import {{jacksonPackage}}.databind.DeserializationContext; +import {{jacksonPackage}}.databind.JsonNode; +import {{jacksonPackage}}.databind.annotation.JsonDeserialize; +import {{jacksonPackage}}.databind.annotation.JsonSerialize; +import {{jacksonPackage}}.databind.deser.std.StdDeserializer; +import {{jacksonPackage}}.databind.ser.std.StdSerializer; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>xmlAnnotation}} +@JsonDeserialize(using = {{classname}}.{{classname}}Deserializer.class) +@JsonSerialize(using = {{classname}}.{{classname}}Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}} implements {{{.}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-implements}} { + private static final Logger log = Logger.getLogger({{classname}}.class.getName()); + + public static class {{classname}}Serializer extends StdSerializer<{{classname}}> { + public {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + public {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, JsonGenerator jgen, {{^useJackson3}}SerializerProvider provider{{/useJackson3}}{{#useJackson3}}SerializationContext serializationContext{{/useJackson3}}) throws {{^useJackson3}}IOException, JsonProcessingException{{/useJackson3}}{{#useJackson3}}JacksonException{{/useJackson3}} { +{{^useJackson3}} + jgen.writeObject(value.getActualInstance()); +{{/useJackson3}} +{{#useJackson3}} + serializationContext.writeValue(jgen, value.getActualInstance()); +{{/useJackson3}} + } + } + + public static class {{classname}}Deserializer extends StdDeserializer<{{classname}}> { + public {{classname}}Deserializer() { + this({{classname}}.class); + } + + public {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(JsonParser jp, DeserializationContext ctxt) throws {{^useJackson3}}IOException, JsonProcessingException{{/useJackson3}}{{#useJackson3}}JacksonException{{/useJackson3}} { + JsonNode tree = ctxt.readTree(jp); + Object deserialized = null; + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + {{classname}} new{{classname}} = new {{classname}}(); +{{^useJackson3}} + Map result2 = tree.traverse(jp.getCodec()).readValueAs(new TypeReference>() {}); + String discriminatorValue = (String)result2.get("{{{propertyBaseName}}}"); +{{/useJackson3}} +{{#useJackson3}} + String discriminatorValue = tree.path("{{{propertyBaseName}}}").asText(null); +{{/useJackson3}} + switch (discriminatorValue) { + {{#mappedModels}} + case "{{{mappingName}}}": +{{^useJackson3}} + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{modelName}}}.class); +{{/useJackson3}} +{{#useJackson3}} + deserialized = ctxt.readTreeAsValue(tree, {{{modelName}}}.class); +{{/useJackson3}} + new{{classname}}.setActualInstance(deserialized); + return new{{classname}}; + {{/mappedModels}} + default: + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", discriminatorValue)); + } + + {{/discriminator}} + {{/useOneOfDiscriminatorLookup}} +{{^useJackson3}} + boolean typeCoercion = ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS); +{{/useJackson3}} +{{#useJackson3}} + boolean typeCoercion = false; // MapperFeature.ALLOW_COERCION_OF_SCALARS was removed in Jackson 3 +{{/useJackson3}} + int match = 0; +{{^useJackson3}} + JsonToken token = tree.traverse(jp.getCodec()).nextToken(); +{{/useJackson3}} +{{#useJackson3}} + JsonToken token = tree.asToken(); +{{/useJackson3}} + {{#oneOf}} + // deserialize {{{.}}} + try { + boolean attemptParsing = true; + // ensure that we respect type coercion as set on the client ObjectMapper + if ({{{.}}}.class.equals(Integer.class) || {{{.}}}.class.equals(Long.class) || {{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class) || {{{.}}}.class.equals(Boolean.class) || {{{.}}}.class.equals(String.class)) { + attemptParsing = typeCoercion; + if (!attemptParsing) { + attemptParsing |= (({{{.}}}.class.equals(Integer.class) || {{{.}}}.class.equals(Long.class)) && token == JsonToken.VALUE_NUMBER_INT); + attemptParsing |= (({{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class)) && token == JsonToken.VALUE_NUMBER_FLOAT); + attemptParsing |= ({{{.}}}.class.equals(Boolean.class) && (token == JsonToken.VALUE_FALSE || token == JsonToken.VALUE_TRUE)); + attemptParsing |= ({{{.}}}.class.equals(String.class) && token == JsonToken.VALUE_STRING); + {{#isNullable}} + attemptParsing |= (token == JsonToken.VALUE_NULL); + {{/isNullable}} + } + } + if (attemptParsing) { +{{^useJackson3}} + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); +{{/useJackson3}} +{{#useJackson3}} + deserialized = ctxt.readTreeAsValue(tree, {{{.}}}.class); +{{/useJackson3}} + // TODO: there is no validation against JSON schema constraints + // (min, max, enum, pattern...), this does not perform a strict JSON + // validation, which means the 'match' count may be higher than it should be. + match++; + log.log(Level.FINER, "Input data matches schema '{{{.}}}'"); + } + } catch (Exception e) { + // deserialization failed, continue + log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e); + } + + {{/oneOf}} + if (match == 1) { + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } +{{^useJackson3}} + throw new IOException(String.format(java.util.Locale.ROOT, "Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); +{{/useJackson3}} +{{#useJackson3}} + throw DatabindException.from(jp, String.format(java.util.Locale.ROOT, "Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); +{{/useJackson3}} + } + + /** + * Handle deserialization of the 'null' value. + */ + @Override +{{^useJackson3}} + public {{classname}} getNullValue(DeserializationContext ctxt) throws JsonMappingException { +{{/useJackson3}} +{{#useJackson3}} + public {{classname}} getNullValue(DeserializationContext ctxt) { +{{/useJackson3}} + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} +{{^useJackson3}} + throw new JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); +{{/useJackson3}} +{{#useJackson3}} + throw DatabindException.from(ctxt.getParser(), "{{classname}} cannot be null"); +{{/useJackson3}} + {{/isNullable}} + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + public {{classname}}() { + super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + } +{{> libraries/native/additional_properties }} + + {{#additionalPropertiesType}} + /** + * Return true if this {{name}} object is equal to o. + */ + @Override + public boolean equals(Object o) { + return super.equals(o) && Objects.equals(this.additionalProperties, (({{classname}})o).additionalProperties); + } + + @Override + public int hashCode() { + return Objects.hash(getActualInstance(), isNullable(), getSchemaType(), additionalProperties); + } + {{/additionalPropertiesType}} + {{#oneOf}} + public {{classname}}({{{.}}} o) { + super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + setActualInstance(o); + } + + {{/oneOf}} + static { + {{#oneOf}} + schemas.put("{{{.}}}", {{{.}}}.class); + {{/oneOf}} + {{#discriminator}} + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + {{/discriminator}} + } + + @Override + public Map> getSchemas() { + return {{classname}}.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check + * the instance parameter is valid against the oneOf child schemas: + * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}} + * + * It could be an instance of the 'oneOf' schemas. + * The oneOf child schemas may themselves be a composed schema (allOf, anyOf, oneOf). + */ + @Override + public void setActualInstance(Object instance) { + {{#isNullable}} + if (instance == null) { + super.setActualInstance(instance); + return; + } + + {{/isNullable}} + {{#oneOf}} + if ({{{.}}}.class.isAssignableFrom(instance.getClass())) { + super.setActualInstance(instance); + return; + } + + {{/oneOf}} + throw new RuntimeException("Invalid instance type. Must be {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}"); + } + + /** + * Get the actual instance, which can be the following: + * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}} + * + * @return The actual instance ({{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}) + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + {{#oneOf}} + /** + * Get the actual instance of `{{{.}}}`. If the actual instance is not `{{{.}}}`, + * the ClassCastException will be thrown. + * + * @return The actual instance of `{{{.}}}` + * @throws ClassCastException if the instance is not `{{{.}}}` + */ + public {{{.}}} get{{{.}}}() throws ClassCastException { + return ({{{.}}})super.getActualInstance(); + } + + {{/oneOf}} + +{{! toUrlQueryString removed β€” references ApiClient which does not belong in the API module }} +{{#_dummyBlockToSkipUrlQuery_}} + + /** + * Convert the instance into URL query string. + * + * @return URL query string + */ + public String toUrlQueryString() { + return toUrlQueryString(null); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + String suffix = ""; + String containerSuffix = ""; + String containerPrefix = ""; + if (prefix == null) { + // style=form, explode=true, e.g. /pet?name=cat&type=manx + prefix = ""; + } else { + // deepObject style e.g. /pet?id[name]=cat&id[type]=manx + prefix = prefix + "["; + suffix = "]"; + containerSuffix = "]"; + containerPrefix = "["; + } + + StringJoiner joiner = new StringJoiner("&"); + + {{#composedSchemas.oneOf}} + {{^vendorExtensions.x-duplicated-data-type}} + if (getActualInstance() instanceof {{{dataType}}}) { + {{#isArray}} + {{#items.isPrimitiveType}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(_item)))); + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(getActualInstance().get(i))))); + } + } + {{/uniqueItems}} + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + {{#items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(_item.toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if ((({{{dataType}}})getActualInstance()).get(i) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(i).toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{^items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf(_item)))); + } + i++; + } + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if (getActualInstance().get(i) != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, i, containerSuffix), + ApiClient.urlEncode(String.valueOf((({{{dataType}}})getActualInstance()).get(i))))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{/items.isPrimitiveType}} + {{/isArray}} + {{^isArray}} + {{#isMap}} + {{#items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, _key, containerSuffix), + getActualInstance().get(_key), ApiClient.urlEncode(String.valueOf((({{{dataType}}})getActualInstance()).get(_key))))); + } + } + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + if ((({{{dataType}}})getActualInstance()).get(_key) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(_key).toUrlQueryString(String.format(java.util.Locale.ROOT, "%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format(java.util.Locale.ROOT, "%s%d%s", containerPrefix, _key, containerSuffix)))); + } + } + } + {{/items.isPrimitiveType}} + {{/isMap}} + {{^isMap}} + {{#isPrimitiveType}} + if (getActualInstance() != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{{baseName}}}%s=%s", prefix, suffix, ApiClient.urlEncode(String.valueOf(getActualInstance())))); + } + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isModel}} + if (getActualInstance() != null) { + joiner.add((({{{dataType}}})getActualInstance()).toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); + } + {{/isModel}} + {{^isModel}} + if (getActualInstance() != null) { + joiner.add(String.format(java.util.Locale.ROOT, "%s{{{baseName}}}%s=%s", prefix, suffix, ApiClient.urlEncode(String.valueOf(getActualInstance())))); + } + {{/isModel}} + {{/isPrimitiveType}} + {{/isMap}} + {{/isArray}} + return joiner.toString(); + } + {{/vendorExtensions.x-duplicated-data-type}} + {{/composedSchemas.oneOf}} + return null; + } +{{/_dummyBlockToSkipUrlQuery_}} + +} diff --git a/langfuse-java-api/src/main/templates/pojo.mustache b/langfuse-java-api/src/main/templates/pojo.mustache new file mode 100644 index 0000000..d1677fa --- /dev/null +++ b/langfuse-java-api/src/main/templates/pojo.mustache @@ -0,0 +1,438 @@ +{{#discriminator}} +import {{invokerPackage}}.JSON; +{{/discriminator}} +/** + * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} + * @deprecated{{/isDeprecated}} + */{{#isDeprecated}} +@Deprecated{{/isDeprecated}} +{{#swagger1AnnotationLibrary}} +{{#description}} +@ApiModel(description = "{{{.}}}") +{{/description}} +{{/swagger1AnnotationLibrary}} +{{#swagger2AnnotationLibrary}} +{{#description}} +@Schema(description = "{{{.}}}") +{{/description}} +{{/swagger2AnnotationLibrary}} +{{#jackson}} +@JsonPropertyOrder({ +{{#vars}} + {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} +{{/vars}} +}) +{{/jackson}} +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +{{#generateBuilders}} +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = {{classname}}.Builder.class) +@tools.jackson.databind.annotation.JsonDeserialize(builder = {{classname}}.Builder.class) +{{/generateBuilders}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +@com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY) +public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ +{{#serializableModel}} + private static final long serialVersionUID = 1L; + +{{/serializableModel}} + {{#vars}} + {{#isEnum}} + {{^isContainer}} + {{^vendorExtensions.x-enum-as-string}} +{{>modelInnerEnum}} + + {{/vendorExtensions.x-enum-as-string}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} +{{>modelInnerEnum}} + + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#gson}} + public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; + {{/gson}} + {{#jackson}} + public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; + {{/jackson}} + {{#withXml}} + @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) + {{#isXmlWrapped}} + @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) + {{/isXmlWrapped}} + {{/withXml}} + {{#gson}} + @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) + {{/gson}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{.}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); + {{/isContainer}} + {{^isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + {{/isContainer}} + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + {{>nullable_var_annotations}}{{! prevent indent}} + private {{>nullableDatatypeWithEnum}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{^defaultValue}}{{#isArray}} = new java.util.ArrayList<>(){{/isArray}}{{#isMap}} = new java.util.HashMap<>(){{/isMap}}{{#isSet}} = new java.util.LinkedHashSet<>(){{/isSet}}{{/defaultValue}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{/vars}} + protected {{classname}}() { {{#parent}}{{#parcelableModel}} + super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}} + this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}} + }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}} + + {{#jackson}}@JsonCreator{{/jackson}} + public {{classname}}( + {{#readOnlyVars}} + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} + {{/readOnlyVars}} + ) { + this(); + {{#readOnlyVars}} + this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; + {{/readOnlyVars}} + }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} + {{#vars}} + + {{^isReadOnly}} + {{#vendorExtensions.x-enum-as-string}} + public static final Set {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList( + {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} + )); + + {{/vendorExtensions.x-enum-as-string}} + public {{classname}} {{name}}({{>nullableArgumentWithEnum}} {{name}}) { + {{#vendorExtensions.x-enum-as-string}} + if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { + throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); + } + + {{/vendorExtensions.x-enum-as-string}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent()) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().add({{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + this.{{name}}.add({{name}}Item); + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent()) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().put(key, {{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + this.{{name}}.put(key, {{name}}Item); + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isMap}} + + {{/isReadOnly}} + /** + {{#description}} + * {{.}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + * @return {{name}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ +{{#deprecated}} + @Deprecated +{{/deprecated}} + {{>nullable_var_annotations}}{{! prevent indent}} +{{#useBeanValidation}} +{{>beanValidation}} + +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") +{{/swagger1AnnotationLibrary}} +{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") +{{/swagger2AnnotationLibrary}} +{{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} +{{/vendorExtensions.x-extra-annotation}} +{{#vendorExtensions.x-is-jackson-optional-nullable}} + {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} + @JsonIgnore +{{/vendorExtensions.x-is-jackson-optional-nullable}} +{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} public {{>nullableDatatypeWithEnum}} {{getter}}() { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} + if ({{name}} == null) { + {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + } + {{/isReadOnly}} + return {{name}}.orElse(null); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + return {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#vendorExtensions.x-is-jackson-optional-nullable}} +{{> jackson_annotations}} + + public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}} + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) + {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { + {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} + this.{{name}} = {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{^isReadOnly}} +{{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} +{{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{>nullableArgumentWithEnum}} {{name}}) { + {{#vendorExtensions.x-enum-as-string}} + if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { + throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); + } + + {{/vendorExtensions.x-enum-as-string}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isReadOnly}} + + {{/vars}} +{{>libraries/native/additional_properties}} +{{^additionalPropertiesType}} + private Map additionalProperties = new java.util.HashMap<>(); + + @JsonAnySetter + public void putAdditionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public java.util.Optional getAdditionalProperty(String key) { + return java.util.Optional.ofNullable(this.additionalProperties.get(key)); + } +{{/additionalPropertiesType}} + {{#parent}} + {{#allVars}} + {{#isOverridden}} + @Override + public {{classname}} {{name}}({{>nullableArgumentWithEnum}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}})); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{setter}}({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + + {{/isOverridden}} + {{/allVars}} + {{/parent}} + /** + * Return true if this {{name}} object is equal to o. + */ + @Override + public boolean equals(Object o) { + {{#useReflectionEqualsHashCode}} + return EqualsBuilder.reflectionEquals(this, o, false, null, true); + {{/useReflectionEqualsHashCode}} + {{^useReflectionEqualsHashCode}} + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && + {{/-last}}{{/vars}}&& + Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} + {{/useReflectionEqualsHashCode}} + }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static boolean equalsNullable(JsonNullable a, JsonNullable b) { + return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); + }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public int hashCode() { + {{#useReflectionEqualsHashCode}} + return HashCodeBuilder.reflectionHashCode(this); + {{/useReflectionEqualsHashCode}} + {{^useReflectionEqualsHashCode}} + return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}, additionalProperties); + {{/useReflectionEqualsHashCode}} + }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static int hashCodeNullable(JsonNullable a) { + if (a == null) { + return 1; + } + return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; + }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}} + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + {{/parent}} + {{#vars}} + sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}} + sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + return o == null ? "null" : o.toString().replace("\n", "\n "); + } +{{! toUrlQueryString removed β€” references ApiClient which does not belong in the API module }} +{{#parcelableModel}} + + public void writeToParcel(Parcel out, int flags) { +{{#model}} +{{#isArray}} + out.writeList(this); +{{/isArray}} +{{^isArray}} +{{#parent}} + super.writeToParcel(out, flags); +{{/parent}} +{{#vars}} + out.writeValue({{name}}); +{{/vars}} +{{/isArray}} +{{/model}} + } + + {{classname}}(Parcel in) { +{{#isArray}} + in.readTypedList(this, {{arrayModelType}}.CREATOR); +{{/isArray}} +{{^isArray}} +{{#parent}} + super(in); +{{/parent}} +{{#vars}} +{{#isPrimitiveType}} + {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); +{{/isPrimitiveType}} +{{^isPrimitiveType}} + {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); +{{/isPrimitiveType}} +{{/vars}} +{{/isArray}} + } + + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { + public {{classname}} createFromParcel(Parcel in) { +{{#model}} +{{#isArray}} + {{classname}} result = new {{classname}}(); + result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); + return result; +{{/isArray}} +{{^isArray}} + return new {{classname}}(in); +{{/isArray}} +{{/model}} + } + public {{classname}}[] newArray(int size) { + return new {{classname}}[size]; + } + }; +{{/parcelableModel}} +{{#discriminator}} +static { + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); +} +{{/discriminator}} +{{#generateBuilders}} + + {{>javaBuilder}}{{! prevent indent}} +{{/generateBuilders}} +} diff --git a/langfuse-java-client/.mvn/wrapper/maven-wrapper.properties b/langfuse-java-client/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/langfuse-java-client/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/langfuse-java-client/README.md b/langfuse-java-client/README.md new file mode 100644 index 0000000..ac7d9a1 --- /dev/null +++ b/langfuse-java-client/README.md @@ -0,0 +1,125 @@ +# langfuse-java-client + +Reference HTTP client implementation for the Langfuse Java SDK. Uses `java.net.http.HttpClient` for HTTP and supports both Jackson 2 and Jackson 3 for JSON serialization. + +## Maven Coordinates + +```xml + + com.langfuse + langfuse-java-client + 0.2.1-SNAPSHOT + +``` + +You also need a Jackson implementation on the classpath (provided scope): + +```xml + + + com.fasterxml.jackson.core + jackson-databind + ${jackson2.version} + + + + + tools.jackson.core + jackson-databind + ${jackson3.version} + +``` + +When both are present, Jackson 3 is preferred. + +## Usage + +### Basic client + +```java +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .build(); +``` + +The `builder()` method uses `ServiceLoader` to discover the client implementation. You can also instantiate a specific Jackson version directly: + +```java +// Jackson 2 +var client = LangfuseJackson2Client.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .build(); + +// Jackson 3 +var client = LangfuseJackson3Client.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .build(); +``` + +### Custom ObjectMapper + +```java +var client = LangfuseJackson2Client.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .objectMapper(myCustomObjectMapper) + .build(); +``` + +### Configuration options + +See the [API module builder configuration](../langfuse-java-api/README.md#builder-configuration) for all available options (`username`, `password`, `url`, `readTimeout`, `logRequests`, `logResponses`, `prettyPrint`, etc.). + +This module adds one additional option per Jackson version: + +| Method | Description | +|--------|-------------| +| `objectMapper(ObjectMapper)` | Custom Jackson 2 ObjectMapper (on `LangfuseJackson2Client.Builder`) | +| `jsonMapper(JsonMapper)` | Custom Jackson 3 JsonMapper (on `LangfuseJackson3Client.Builder`) | + +### Request/response logging + +```java +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") + .password("sk-lf-...") + .url("https://cloud.langfuse.com") + .logRequests() + .logResponses() + .prettyPrint() + .build(); +``` + +Log output uses SLF4J at `INFO` level. Sensitive headers (`Authorization`) are automatically masked: + +``` +-> REQUEST: POST https://cloud.langfuse.com/api/public/ingestion + HEADERS: + Authorization: ********************************** + Content-Type: application/json + +-> REQUEST BODY: +{ + "batch" : [ ... ] +} + +<- RESPONSE: 207 + HEADERS: + content-type: application/json; charset=utf-8 + BODY: +{ + "successes" : [ ... ], + "errors" : [ ] +} +``` + +## HTTP/1.1 Fallback + +When the base URL uses `http` (not `https`), the client automatically falls back to HTTP/1.1 to avoid protocol negotiation issues with servers that don't support HTTP/2 upgrade over plain HTTP. HTTPS connections use HTTP/2 with ALPN negotiation. diff --git a/langfuse-java-client/mvnw b/langfuse-java-client/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/langfuse-java-client/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/langfuse-java-client/mvnw.cmd b/langfuse-java-client/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/langfuse-java-client/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/langfuse-java-client/pom.xml b/langfuse-java-client/pom.xml new file mode 100644 index 0000000..25a44fa --- /dev/null +++ b/langfuse-java-client/pom.xml @@ -0,0 +1,246 @@ + + 4.0.0 + + + com.langfuse + langfuse-java-parent + 0.2.1-SNAPSHOT + + + langfuse-java-client + jar + + langfuse-java-client + Reference client implementation for the Langfuse Java SDK API + + + + + com.langfuse + langfuse-java-api + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + provided + + + + + tools.jackson.core + jackson-databind + provided + + + + + org.slf4j + slf4j-api + + + + + org.jspecify + jspecify + 1.0.0 + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + com.langfuse + langfuse-java-testcontainers + test + + + org.slf4j + slf4j-simple + test + + + org.awaitility + awaitility + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + + + generate-client + + generate + + + ${project.basedir}/../openapi.yml + java + ${project.basedir}/src/main/templates + true + false + true + ApiClient.java,ApiResponse.java,Pair.java + false + false + false + false + + true + + + native + java8 + jackson + false + true + true + true + false + true + + com.langfuse.client + com.langfuse.client + com.langfuse.api.model + + + + generate-async-client + + generate + + + ${project.basedir}/../openapi.yml + java + ${project.basedir}/src/main/templates + ${project.build.directory}/generated-sources/openapi-async + true + false + false + false + false + false + false + + true + + + native + java8 + jackson + false + true + true + true + true + + com.langfuse.client + com.langfuse.client + com.langfuse.api.model + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + fix-generic-type-references + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + dev.jbang + jbang-maven-plugin + + + relocate-to-subpackages + generate-sources + + run + + + + + ${project.build.directory}/generated-sources/openapi + ${project.build.directory}/generated-sources/openapi-async + -- + *Api.java + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + diff --git a/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClient.java b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClient.java new file mode 100644 index 0000000..a8d70b5 --- /dev/null +++ b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClient.java @@ -0,0 +1,541 @@ +package com.langfuse.client; + +import java.time.Duration; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import com.langfuse.api.LangfuseApi; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi; +import com.langfuse.api.comments.CommentsApi; +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.health.HealthApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyMetricsV1.LegacyMetricsV1Api; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api; +import com.langfuse.api.llmConnections.LlmConnectionsApi; +import com.langfuse.api.media.MediaApi; +import com.langfuse.api.metrics.MetricsApi; +import com.langfuse.api.models.ModelsApi; +import com.langfuse.api.observations.ObservationsApi; +import com.langfuse.api.opentelemetry.OpentelemetryApi; +import com.langfuse.api.organizations.OrganizationsApi; +import com.langfuse.api.projects.ProjectsApi; +import com.langfuse.api.promptVersion.PromptVersionApi; +import com.langfuse.api.prompts.PromptsApi; +import com.langfuse.api.scim.ScimApi; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; +import com.langfuse.api.scores.ScoresApi; +import com.langfuse.api.sessions.SessionsApi; +import com.langfuse.api.trace.TraceApi; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi; + +/** + * Abstract base implementation of {@link LangfuseApi}. + * Extends {@link ApiClient} to provide HTTP infrastructure and delegates + * JSON serialization to Jackson-version-specific subclasses. + * + *

Use {@link LangfuseApi#builder()} for automatic Jackson version detection, + * or instantiate a specific subclass directly. + * + * @author Eric Deandrea + * @see LangfuseJackson2Client + * @see LangfuseJackson3Client + */ +public abstract class LangfuseClient extends ApiClient implements LangfuseApi { + private final AnnotationQueuesApi annotationQueuesApi; + private final com.langfuse.api.annotationQueues.async.AnnotationQueuesApi asyncAnnotationQueuesApi; + private final BlobStorageIntegrationsApi blobStorageIntegrationsApi; + private final com.langfuse.api.blobStorageIntegrations.async.BlobStorageIntegrationsApi asyncBlobStorageIntegrationsApi; + private final CommentsApi commentsApi; + private final com.langfuse.api.comments.async.CommentsApi asyncCommentsApi; + private final DatasetItemsApi datasetItemsApi; + private final com.langfuse.api.datasetItems.async.DatasetItemsApi asyncDatasetItemsApi; + private final DatasetRunItemsApi datasetRunItemsApi; + private final com.langfuse.api.datasetRunItems.async.DatasetRunItemsApi asyncDatasetRunItemsApi; + private final DatasetsApi datasetsApi; + private final com.langfuse.api.datasets.async.DatasetsApi asyncDatasetsApi; + private final HealthApi healthApi; + private final com.langfuse.api.health.async.HealthApi asyncHealthApi; + private final IngestionApi ingestionApi; + private final com.langfuse.api.ingestion.async.IngestionApi asyncIngestionApi; + private final LegacyMetricsV1Api legacyMetricsV1Api; + private final com.langfuse.api.legacyMetricsV1.async.LegacyMetricsV1Api asyncLegacyMetricsV1Api; + private final LegacyObservationsV1Api legacyObservationsV1Api; + private final com.langfuse.api.legacyObservationsV1.async.LegacyObservationsV1Api asyncLegacyObservationsV1Api; + private final LegacyScoreV1Api legacyScoreV1Api; + private final com.langfuse.api.legacyScoreV1.async.LegacyScoreV1Api asyncLegacyScoreV1Api; + private final LlmConnectionsApi llmConnectionsApi; + private final com.langfuse.api.llmConnections.async.LlmConnectionsApi asyncLlmConnectionsApi; + private final MediaApi mediaApi; + private final com.langfuse.api.media.async.MediaApi asyncMediaApi; + private final MetricsApi metricsApi; + private final com.langfuse.api.metrics.async.MetricsApi asyncMetricsApi; + private final ModelsApi modelsApi; + private final com.langfuse.api.models.async.ModelsApi asyncModelsApi; + private final ObservationsApi observationsApi; + private final com.langfuse.api.observations.async.ObservationsApi asyncObservationsApi; + private final OpentelemetryApi opentelemetryApi; + private final com.langfuse.api.opentelemetry.async.OpentelemetryApi asyncOpentelemetryApi; + private final OrganizationsApi organizationsApi; + private final com.langfuse.api.organizations.async.OrganizationsApi asyncOrganizationsApi; + private final ProjectsApi projectsApi; + private final com.langfuse.api.projects.async.ProjectsApi asyncProjectsApi; + private final PromptVersionApi promptVersionApi; + private final com.langfuse.api.promptVersion.async.PromptVersionApi asyncPromptVersionApi; + private final PromptsApi promptsApi; + private final com.langfuse.api.prompts.async.PromptsApi asyncPromptsApi; + private final ScimApi scimApi; + private final com.langfuse.api.scim.async.ScimApi asyncScimApi; + private final ScoreConfigsApi scoreConfigsApi; + private final com.langfuse.api.scoreConfigs.async.ScoreConfigsApi asyncScoreConfigsApi; + private final ScoresApi scoresApi; + private final com.langfuse.api.scores.async.ScoresApi asyncScoresApi; + private final SessionsApi sessionsApi; + private final com.langfuse.api.sessions.async.SessionsApi asyncSessionsApi; + private final TraceApi traceApi; + private final com.langfuse.api.trace.async.TraceApi asyncTraceApi; + private final UnstableEvaluationRulesApi unstableEvaluationRulesApi; + private final com.langfuse.api.unstableEvaluationRules.async.UnstableEvaluationRulesApi asyncUnstableEvaluationRulesApi; + private final UnstableEvaluatorsApi unstableEvaluatorsApi; + private final com.langfuse.api.unstableEvaluators.async.UnstableEvaluatorsApi asyncUnstableEvaluatorsApi; + + protected LangfuseClient(LangfuseClientBuilder builder) { + super(); + + if (builder.url != null) { + updateBaseUri(builder.url); + } + + if (builder.readTimeout != null) { + setReadTimeout(builder.readTimeout); + } + + setRequestInterceptor(requestBuilder -> { + if ((builder.username != null) && (builder.password != null)) { + String encoded = Base64.getEncoder().encodeToString("%s:%s".formatted(builder.username, builder.password).getBytes()); + requestBuilder.header("Authorization", "Basic " + encoded); + } + + for (var entry : builder.headers.entrySet()) { + requestBuilder.header(entry.getKey(), entry.getValue()); + } + }); + + setLogRequests(builder.logRequests); + setLogResponses(builder.logResponses); + setPrettyPrintJson(builder.prettyPrintJson); + + this.annotationQueuesApi = new com.langfuse.client.annotationQueues.AnnotationQueuesApi(this); + this.asyncAnnotationQueuesApi = new com.langfuse.client.annotationQueues.async.AnnotationQueuesApi(this); + this.blobStorageIntegrationsApi = new com.langfuse.client.blobStorageIntegrations.BlobStorageIntegrationsApi(this); + this.asyncBlobStorageIntegrationsApi = new com.langfuse.client.blobStorageIntegrations.async.BlobStorageIntegrationsApi(this); + this.commentsApi = new com.langfuse.client.comments.CommentsApi(this); + this.asyncCommentsApi = new com.langfuse.client.comments.async.CommentsApi(this); + this.datasetItemsApi = new com.langfuse.client.datasetItems.DatasetItemsApi(this); + this.asyncDatasetItemsApi = new com.langfuse.client.datasetItems.async.DatasetItemsApi(this); + this.datasetRunItemsApi = new com.langfuse.client.datasetRunItems.DatasetRunItemsApi(this); + this.asyncDatasetRunItemsApi = new com.langfuse.client.datasetRunItems.async.DatasetRunItemsApi(this); + this.datasetsApi = new com.langfuse.client.datasets.DatasetsApi(this); + this.asyncDatasetsApi = new com.langfuse.client.datasets.async.DatasetsApi(this); + this.healthApi = new com.langfuse.client.health.HealthApi(this); + this.asyncHealthApi = new com.langfuse.client.health.async.HealthApi(this); + this.ingestionApi = new com.langfuse.client.ingestion.IngestionApi(this); + this.asyncIngestionApi = new com.langfuse.client.ingestion.async.IngestionApi(this); + this.legacyMetricsV1Api = new com.langfuse.client.legacyMetricsV1.LegacyMetricsV1Api(this); + this.asyncLegacyMetricsV1Api = new com.langfuse.client.legacyMetricsV1.async.LegacyMetricsV1Api(this); + this.legacyObservationsV1Api = new com.langfuse.client.legacyObservationsV1.LegacyObservationsV1Api(this); + this.asyncLegacyObservationsV1Api = new com.langfuse.client.legacyObservationsV1.async.LegacyObservationsV1Api(this); + this.legacyScoreV1Api = new com.langfuse.client.legacyScoreV1.LegacyScoreV1Api(this); + this.asyncLegacyScoreV1Api = new com.langfuse.client.legacyScoreV1.async.LegacyScoreV1Api(this); + this.llmConnectionsApi = new com.langfuse.client.llmConnections.LlmConnectionsApi(this); + this.asyncLlmConnectionsApi = new com.langfuse.client.llmConnections.async.LlmConnectionsApi(this); + this.mediaApi = new com.langfuse.client.media.MediaApi(this); + this.asyncMediaApi = new com.langfuse.client.media.async.MediaApi(this); + this.metricsApi = new com.langfuse.client.metrics.MetricsApi(this); + this.asyncMetricsApi = new com.langfuse.client.metrics.async.MetricsApi(this); + this.modelsApi = new com.langfuse.client.models.ModelsApi(this); + this.asyncModelsApi = new com.langfuse.client.models.async.ModelsApi(this); + this.observationsApi = new com.langfuse.client.observations.ObservationsApi(this); + this.asyncObservationsApi = new com.langfuse.client.observations.async.ObservationsApi(this); + this.opentelemetryApi = new com.langfuse.client.opentelemetry.OpentelemetryApi(this); + this.asyncOpentelemetryApi = new com.langfuse.client.opentelemetry.async.OpentelemetryApi(this); + this.organizationsApi = new com.langfuse.client.organizations.OrganizationsApi(this); + this.asyncOrganizationsApi = new com.langfuse.client.organizations.async.OrganizationsApi(this); + this.projectsApi = new com.langfuse.client.projects.ProjectsApi(this); + this.asyncProjectsApi = new com.langfuse.client.projects.async.ProjectsApi(this); + this.promptVersionApi = new com.langfuse.client.promptVersion.PromptVersionApi(this); + this.asyncPromptVersionApi = new com.langfuse.client.promptVersion.async.PromptVersionApi(this); + this.promptsApi = new com.langfuse.client.prompts.PromptsApi(this); + this.asyncPromptsApi = new com.langfuse.client.prompts.async.PromptsApi(this); + this.scimApi = new com.langfuse.client.scim.ScimApi(this); + this.asyncScimApi = new com.langfuse.client.scim.async.ScimApi(this); + this.scoreConfigsApi = new com.langfuse.client.scoreConfigs.ScoreConfigsApi(this); + this.asyncScoreConfigsApi = new com.langfuse.client.scoreConfigs.async.ScoreConfigsApi(this); + this.scoresApi = new com.langfuse.client.scores.ScoresApi(this); + this.asyncScoresApi = new com.langfuse.client.scores.async.ScoresApi(this); + this.sessionsApi = new com.langfuse.client.sessions.SessionsApi(this); + this.asyncSessionsApi = new com.langfuse.client.sessions.async.SessionsApi(this); + this.traceApi = new com.langfuse.client.trace.TraceApi(this); + this.asyncTraceApi = new com.langfuse.client.trace.async.TraceApi(this); + this.unstableEvaluationRulesApi = new com.langfuse.client.unstableEvaluationRules.UnstableEvaluationRulesApi(this); + this.asyncUnstableEvaluationRulesApi = new com.langfuse.client.unstableEvaluationRules.async.UnstableEvaluationRulesApi(this); + this.unstableEvaluatorsApi = new com.langfuse.client.unstableEvaluators.UnstableEvaluatorsApi(this); + this.asyncUnstableEvaluatorsApi = new com.langfuse.client.unstableEvaluators.async.UnstableEvaluatorsApi(this); + } + + @Override + public AnnotationQueuesApi annotationQueues() { + return annotationQueuesApi; + } + + @Override + public com.langfuse.api.annotationQueues.async.AnnotationQueuesApi asyncAnnotationQueues() { + return asyncAnnotationQueuesApi; + } + + @Override + public BlobStorageIntegrationsApi blobStorageIntegrations() { + return blobStorageIntegrationsApi; + } + + @Override + public com.langfuse.api.blobStorageIntegrations.async.BlobStorageIntegrationsApi asyncBlobStorageIntegrations() { + return asyncBlobStorageIntegrationsApi; + } + + @Override + public CommentsApi comments() { + return commentsApi; + } + + @Override + public com.langfuse.api.comments.async.CommentsApi asyncComments() { + return asyncCommentsApi; + } + + @Override + public DatasetItemsApi datasetItems() { + return datasetItemsApi; + } + + @Override + public com.langfuse.api.datasetItems.async.DatasetItemsApi asyncDatasetItems() { + return asyncDatasetItemsApi; + } + + @Override + public DatasetRunItemsApi datasetRunItems() { + return datasetRunItemsApi; + } + + @Override + public com.langfuse.api.datasetRunItems.async.DatasetRunItemsApi asyncDatasetRunItems() { + return asyncDatasetRunItemsApi; + } + + @Override + public DatasetsApi datasets() { + return datasetsApi; + } + + @Override + public com.langfuse.api.datasets.async.DatasetsApi asyncDatasets() { + return asyncDatasetsApi; + } + + @Override + public HealthApi health() { + return healthApi; + } + + @Override + public com.langfuse.api.health.async.HealthApi asyncHealth() { + return asyncHealthApi; + } + + @Override + public IngestionApi ingestion() { + return ingestionApi; + } + + @Override + public com.langfuse.api.ingestion.async.IngestionApi asyncIngestion() { + return asyncIngestionApi; + } + + @Override + public LegacyMetricsV1Api legacyMetricsV1() { + return legacyMetricsV1Api; + } + + @Override + public com.langfuse.api.legacyMetricsV1.async.LegacyMetricsV1Api asyncLegacyMetricsV1() { + return asyncLegacyMetricsV1Api; + } + + @Override + public LegacyObservationsV1Api legacyObservationsV1() { + return legacyObservationsV1Api; + } + + @Override + public com.langfuse.api.legacyObservationsV1.async.LegacyObservationsV1Api asyncLegacyObservationsV1() { + return asyncLegacyObservationsV1Api; + } + + @Override + public LegacyScoreV1Api legacyScoreV1() { + return legacyScoreV1Api; + } + + @Override + public com.langfuse.api.legacyScoreV1.async.LegacyScoreV1Api asyncLegacyScoreV1() { + return asyncLegacyScoreV1Api; + } + + @Override + public LlmConnectionsApi llmConnections() { + return llmConnectionsApi; + } + + @Override + public com.langfuse.api.llmConnections.async.LlmConnectionsApi asyncLlmConnections() { + return asyncLlmConnectionsApi; + } + + @Override + public MediaApi media() { + return mediaApi; + } + + @Override + public com.langfuse.api.media.async.MediaApi asyncMedia() { + return asyncMediaApi; + } + + @Override + public MetricsApi metrics() { + return metricsApi; + } + + @Override + public com.langfuse.api.metrics.async.MetricsApi asyncMetrics() { + return asyncMetricsApi; + } + + @Override + public ModelsApi models() { + return modelsApi; + } + + @Override + public com.langfuse.api.models.async.ModelsApi asyncModels() { + return asyncModelsApi; + } + + @Override + public ObservationsApi observations() { + return observationsApi; + } + + @Override + public com.langfuse.api.observations.async.ObservationsApi asyncObservations() { + return asyncObservationsApi; + } + + @Override + public OpentelemetryApi opentelemetry() { + return opentelemetryApi; + } + + @Override + public com.langfuse.api.opentelemetry.async.OpentelemetryApi asyncOpentelemetry() { + return asyncOpentelemetryApi; + } + + @Override + public OrganizationsApi organizations() { + return organizationsApi; + } + + @Override + public com.langfuse.api.organizations.async.OrganizationsApi asyncOrganizations() { + return asyncOrganizationsApi; + } + + @Override + public ProjectsApi projects() { + return projectsApi; + } + + @Override + public com.langfuse.api.projects.async.ProjectsApi asyncProjects() { + return asyncProjectsApi; + } + + @Override + public PromptVersionApi promptVersion() { + return promptVersionApi; + } + + @Override + public com.langfuse.api.promptVersion.async.PromptVersionApi asyncPromptVersion() { + return asyncPromptVersionApi; + } + + @Override + public PromptsApi prompts() { + return promptsApi; + } + + @Override + public com.langfuse.api.prompts.async.PromptsApi asyncPrompts() { + return asyncPromptsApi; + } + + @Override + public ScimApi scim() { + return scimApi; + } + + @Override + public com.langfuse.api.scim.async.ScimApi asyncScim() { + return asyncScimApi; + } + + @Override + public ScoreConfigsApi scoreConfigs() { + return scoreConfigsApi; + } + + @Override + public com.langfuse.api.scoreConfigs.async.ScoreConfigsApi asyncScoreConfigs() { + return asyncScoreConfigsApi; + } + + @Override + public ScoresApi scores() { + return scoresApi; + } + + @Override + public com.langfuse.api.scores.async.ScoresApi asyncScores() { + return asyncScoresApi; + } + + @Override + public SessionsApi sessions() { + return sessionsApi; + } + + @Override + public com.langfuse.api.sessions.async.SessionsApi asyncSessions() { + return asyncSessionsApi; + } + + @Override + public TraceApi trace() { + return traceApi; + } + + @Override + public com.langfuse.api.trace.async.TraceApi asyncTrace() { + return asyncTraceApi; + } + + @Override + public UnstableEvaluationRulesApi unstableEvaluationRules() { + return unstableEvaluationRulesApi; + } + + @Override + public com.langfuse.api.unstableEvaluationRules.async.UnstableEvaluationRulesApi asyncUnstableEvaluationRules() { + return asyncUnstableEvaluationRulesApi; + } + + @Override + public UnstableEvaluatorsApi unstableEvaluators() { + return unstableEvaluatorsApi; + } + + @Override + public com.langfuse.api.unstableEvaluators.async.UnstableEvaluatorsApi asyncUnstableEvaluators() { + return asyncUnstableEvaluatorsApi; + } + + /** + * Abstract builder for constructing {@link LangfuseClient} instances. + * Implements {@link LangfuseApiBuilder} for Langfuse-specific settings + * and configures the generated {@link ApiClient} via its setter methods. + * + * @param the concrete client type + * @param the concrete builder type + * @author Eric Deandrea + */ + public abstract static class LangfuseClientBuilder> + implements LangfuseApiBuilder { + + private String username; + private String password; + private String url; + private Duration readTimeout; + private boolean logRequests; + private boolean logResponses; + private boolean prettyPrintJson; + private final Map headers = new HashMap<>(); + + @Override + public B username(String username) { + this.username = username; + return self(); + } + + @Override + public B password(String password) { + this.password = password; + return self(); + } + + @Override + public B url(String url) { + this.url = url; + return self(); + } + + @Override + public B readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return self(); + } + + @Override + public B addHeader(String name, String value) { + this.headers.put(name, value); + return self(); + } + + @Override + public B logRequests(boolean logRequests) { + this.logRequests = logRequests; + return self(); + } + + @Override + public B logResponses(boolean logResponses) { + this.logResponses = logResponses; + return self(); + } + + @Override + public B prettyPrint(boolean prettyPrint) { + this.prettyPrintJson = prettyPrint; + return self(); + } + } +} diff --git a/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClientBuilderFactory.java b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClientBuilderFactory.java new file mode 100644 index 0000000..9cd999c --- /dev/null +++ b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClientBuilderFactory.java @@ -0,0 +1,71 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import com.langfuse.api.LangfuseApi.LangfuseApiBuilder; +import com.langfuse.api.spi.LangfuseApiBuilderFactory; +import com.langfuse.client.LangfuseClient.LangfuseClientBuilder; + +/** + * {@link LangfuseApiBuilderFactory} implementation that auto-detects the Jackson version + * on the classpath and returns the appropriate builder. + * + *

Jackson 3 is preferred when both versions are present. + * + * @author Eric Deandrea + */ +public final class LangfuseClientBuilderFactory implements LangfuseApiBuilderFactory { + + @Override + @SuppressWarnings("unchecked") + public > B getBuilder() { + return (B) newBuilder(Thread.currentThread().getContextClassLoader()); + } + + private static LangfuseClientBuilder newBuilder(ClassLoader classLoader) { + if (JacksonVersion.JACKSON_3.isOnClasspath(classLoader)) { + return LangfuseJackson3Client.builder(); + } + + if (JacksonVersion.JACKSON_2.isOnClasspath(classLoader)) { + return LangfuseJackson2Client.builder(); + } + + throw new IllegalStateException( + "Neither Jackson 2 nor Jackson 3 found on the classpath. " + + "Add com.fasterxml.jackson.core:jackson-databind (Jackson 2) " + + "or tools.jackson.core:jackson-databind (Jackson 3) to your dependencies."); + } + + /** + * Detects Jackson versions by probing for well-known classes on the classpath. + * + * @author Eric Deandrea + */ + private enum JacksonVersion { + JACKSON_3("tools.jackson.databind.json.JsonMapper"), + JACKSON_2("com.fasterxml.jackson.databind.json.JsonMapper"); + + private final String className; + + JacksonVersion(String className) { + this.className = className; + } + + /** + * Checks whether this Jackson version is available on the given classloader. + * Uses {@link Class#forName(String, boolean, ClassLoader)} with {@code initialize=false} + * to avoid triggering static initializers. + * + * @param classLoader the classloader to check + * @return {@code true} if the Jackson version is available + */ + boolean isOnClasspath(ClassLoader classLoader) { + try { + Class.forName(className, false, classLoader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } +} diff --git a/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson2Client.java b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson2Client.java new file mode 100644 index 0000000..fecfa2e --- /dev/null +++ b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson2Client.java @@ -0,0 +1,123 @@ +package com.langfuse.client; + +import java.lang.reflect.Type; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.langfuse.api.LangfuseApiException; + +/** + * {@link LangfuseClient} implementation using Jackson 2 ({@code com.fasterxml.jackson}) for JSON serialization. + * + *

Configures a default {@link ObjectMapper} with {@link JavaTimeModule}, + * {@code FAIL_ON_UNKNOWN_PROPERTIES} disabled, and {@code WRITE_DATES_AS_TIMESTAMPS} disabled + * to match Jackson 3 defaults. + * + *

{@code
+ * LangfuseJackson2Client client = LangfuseJackson2Client.builder()
+ *     .username("pk-lf-...")
+ *     .password("sk-lf-...")
+ *     .url("https://cloud.langfuse.com")
+ *     .build();
+ * }
+ * + * @author Eric Deandrea + */ +public final class LangfuseJackson2Client extends LangfuseClient { + + private final ObjectMapper objectMapper; + + LangfuseJackson2Client(Builder builder) { + super(builder); + this.objectMapper = builder.objectMapper != null ? builder.objectMapper : createDefaultObjectMapper(); + } + + @Override + public T readValue(String body, Type type) { + try { + JavaType javaType = objectMapper.getTypeFactory().constructType(type); + return objectMapper.readValue(body, javaType); + } catch (Exception e) { + throw new LangfuseApiException("Failed to deserialize response", e); + } + } + + @Override + public byte[] writeValueAsBytes(Object value) { + try { + return objectMapper.writeValueAsBytes(value); + } catch (Exception e) { + throw new LangfuseApiException("Failed to serialize request", e); + } + } + + @Override + public String writeValueAsString(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (Exception e) { + throw new LangfuseApiException("Failed to serialize value", e); + } + } + + @Override + public String writeValueAsPrettyString(Object value) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value); + } catch (Exception e) { + throw new LangfuseApiException("Failed to serialize value", e); + } + } + + private static ObjectMapper createDefaultObjectMapper() { + return JsonMapper.builder() + .addModule(new JavaTimeModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + } + + /** + * Creates a new builder for constructing a {@link LangfuseJackson2Client}. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing {@link LangfuseJackson2Client} instances. + * + * @author Eric Deandrea + */ + public static class Builder + extends LangfuseClientBuilder { + + private ObjectMapper objectMapper; + + private Builder() { + } + + /** + * Sets a custom {@link ObjectMapper} to use for JSON serialization. + * If not set, a default mapper is created. + * + * @param objectMapper the custom ObjectMapper + * @return this builder for method chaining + */ + public Builder objectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + @Override + public LangfuseJackson2Client build() { + return new LangfuseJackson2Client(this); + } + } +} diff --git a/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson3Client.java b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson3Client.java new file mode 100644 index 0000000..199ab8d --- /dev/null +++ b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseJackson3Client.java @@ -0,0 +1,97 @@ +package com.langfuse.client; + +import java.lang.reflect.Type; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.json.JsonMapper; + +/** + * {@link LangfuseClient} implementation using Jackson 3 ({@code tools.jackson}) for JSON serialization. + * + *

Jackson 3 defaults already disable {@code FAIL_ON_UNKNOWN_PROPERTIES} and + * {@code WRITE_DATES_AS_TIMESTAMPS}, so no additional configuration is needed. + * + *

{@code
+ * LangfuseJackson3Client client = LangfuseJackson3Client.builder()
+ *     .username("pk-lf-...")
+ *     .password("sk-lf-...")
+ *     .url("https://cloud.langfuse.com")
+ *     .build();
+ * }
+ * + * @author Eric Deandrea + */ +public final class LangfuseJackson3Client extends LangfuseClient { + + private final JsonMapper jsonMapper; + + LangfuseJackson3Client(Builder builder) { + super(builder); + this.jsonMapper = builder.jsonMapper != null ? builder.jsonMapper : createDefaultJsonMapper(); + } + + @Override + public T readValue(String body, Type type) { + JavaType javaType = jsonMapper.getTypeFactory().constructType(type); + return jsonMapper.readValue(body, javaType); + } + + @Override + public byte[] writeValueAsBytes(Object value) { + return jsonMapper.writeValueAsBytes(value); + } + + @Override + public String writeValueAsString(Object value) { + return jsonMapper.writeValueAsString(value); + } + + @Override + public String writeValueAsPrettyString(Object value) { + return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value); + } + + private static JsonMapper createDefaultJsonMapper() { + return JsonMapper.builder().build(); + } + + /** + * Creates a new builder for constructing a {@link LangfuseJackson3Client}. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing {@link LangfuseJackson3Client} instances. + * + * @author Eric Deandrea + */ + public static class Builder + extends LangfuseClientBuilder { + + private JsonMapper jsonMapper; + + private Builder() { + } + + /** + * Sets a custom {@link JsonMapper} to use for JSON serialization. + * If not set, a default mapper is created. + * + * @param jsonMapper the custom JsonMapper + * @return this builder for method chaining + */ + public Builder jsonMapper(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + return this; + } + + @Override + public LangfuseJackson3Client build() { + return new LangfuseJackson3Client(this); + } + } +} diff --git a/langfuse-java-client/src/main/java/module-info.java b/langfuse-java-client/src/main/java/module-info.java new file mode 100644 index 0000000..705f538 --- /dev/null +++ b/langfuse-java-client/src/main/java/module-info.java @@ -0,0 +1,17 @@ +module com.langfuse.client { + requires transitive com.langfuse.api; + requires transitive org.slf4j; + requires static org.jspecify; + requires static jakarta.annotation; + requires static com.fasterxml.jackson.core; + requires static com.fasterxml.jackson.databind; + requires static com.fasterxml.jackson.datatype.jsr310; + requires static tools.jackson.core; + requires static tools.jackson.databind; + requires java.net.http; + + exports com.langfuse.client; + + provides com.langfuse.api.spi.LangfuseApiBuilderFactory + with com.langfuse.client.LangfuseClientBuilderFactory; +} diff --git a/langfuse-java-client/src/main/resources/META-INF/services/com.langfuse.api.spi.LangfuseApiBuilderFactory b/langfuse-java-client/src/main/resources/META-INF/services/com.langfuse.api.spi.LangfuseApiBuilderFactory new file mode 100644 index 0000000..c9f191d --- /dev/null +++ b/langfuse-java-client/src/main/resources/META-INF/services/com.langfuse.api.spi.LangfuseApiBuilderFactory @@ -0,0 +1 @@ +com.langfuse.client.LangfuseClientBuilderFactory diff --git a/langfuse-java-client/src/main/templates/ApiClient.mustache b/langfuse-java-client/src/main/templates/ApiClient.mustache new file mode 100644 index 0000000..79d9bd6 --- /dev/null +++ b/langfuse-java-client/src/main/templates/ApiClient.mustache @@ -0,0 +1,774 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import java.io.InputStream; +import java.io.IOException; +{{#useGzipFeature}} +import java.io.ByteArrayOutputStream; +{{/useGzipFeature}} +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.Optional; +import java.util.zip.GZIPInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +{{#useGzipFeature}} +import java.util.function.Supplier; +import java.util.Objects; +import java.util.zip.GZIPOutputStream; +{{/useGzipFeature}} +import java.util.stream.Collectors; +{{#useUnaryInterceptor}} +import java.util.function.UnaryOperator; +{{/useUnaryInterceptor}} + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Configuration and utility class for API clients. + * + *

This class can be constructed and modified, then used to instantiate the + * various API classes. The API classes use the settings in this class to + * configure themselves, but otherwise do not store a link to this class.

+ * + *

This class is mutable and not synchronized, so it is not thread-safe. + * The API classes generated from this are immutable and thread-safe.

+ * + *

The setter methods of this class return the current object to facilitate + * a fluent style of configuration.

+ */ +{{>generatedAnnotation}} + +public abstract class ApiClient { + + protected HttpClient.Builder builder; + protected String scheme; + protected String host; + protected int port; + protected String basePath; + protected Consumer interceptor; +{{#useUnaryInterceptor}} + protected UnaryOperator> responseInterceptor; + protected UnaryOperator> asyncResponseInterceptor; +{{/useUnaryInterceptor}} +{{^useUnaryInterceptor}} + protected Consumer> responseInterceptor; + protected Consumer> asyncResponseInterceptor; +{{/useUnaryInterceptor}} + protected Duration readTimeout; + protected Duration connectTimeout; + protected boolean logRequests; + protected boolean logResponses; + protected boolean prettyPrintJson; + + private static final Logger LOG = LoggerFactory.getLogger(ApiClient.class); + private static final String AUTHORIZATION_HEADER = "authorization"; + + public static String valueToString(Object value) { + if (value == null) { + return ""; + } + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + return value.toString(); + } + + /** + * URL encode a string in the UTF-8 encoding. + * + * @param s String to encode. + * @return URL-encoded representation of the input string. + */ + public static String urlEncode(String s) { + return URLEncoder.encode(s, UTF_8).replaceAll("\\+", "%20"); + } + + /** + * Convert a URL query name/value parameter to a list of encoded {@link Pair} + * objects. + * + *

The value can be null, in which case an empty list is returned.

+ * + * @param name The query name parameter. + * @param value The query value, which may not be a collection but may be + * null. + * @return A singleton list of the {@link Pair} objects representing the input + * parameters, which is encoded for use in a URL. If the value is null, an + * empty list is returned. + */ + public static List parameterToPairs(String name, Object value) { + if (name == null || name.isEmpty() || value == null) { + return Collections.emptyList(); + } + return Collections.singletonList(new Pair(urlEncode(name), urlEncode(valueToString(value)))); + } + + /** + * Convert a URL query name/collection parameter to a list of encoded + * {@link Pair} objects. + * + * @param collectionFormat The swagger collectionFormat string (csv, tsv, etc). + * @param name The query name parameter. + * @param values A collection of values for the given query name, which may be + * null. + * @return A list of {@link Pair} objects representing the input parameters, + * which is encoded for use in a URL. If the values collection is null, an + * empty list is returned. + */ + public static List parameterToPairs( + String collectionFormat, String name, Collection values) { + if (name == null || name.isEmpty() || values == null || values.isEmpty()) { + return Collections.emptyList(); + } + + // get the collection format (default: csv) + String format = collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat; + + // create the params based on the collection format + if ("multi".equals(format)) { + return values.stream() + .map(value -> new Pair(urlEncode(name), urlEncode(valueToString(value)))) + .collect(Collectors.toList()); + } + + String delimiter; + switch(format) { + case "csv": + delimiter = urlEncode(","); + break; + case "ssv": + delimiter = urlEncode(" "); + break; + case "tsv": + delimiter = urlEncode("\t"); + break; + case "pipes": + delimiter = urlEncode("|"); + break; + default: + throw new IllegalArgumentException("Illegal collection format: " + collectionFormat); + } + + StringJoiner joiner = new StringJoiner(delimiter); + for (Object value : values) { + joiner.add(urlEncode(valueToString(value))); + } + + return Collections.singletonList(new Pair(urlEncode(name), joiner.toString())); + } + + /** + * Create an instance of ApiClient. + */ + public ApiClient() { + this.builder = createDefaultHttpClientBuilder(); + updateBaseUri("{{{basePath}}}"); + interceptor = null; + readTimeout = null; + connectTimeout = null; + responseInterceptor = null; + asyncResponseInterceptor = null; + } + + /** + * Deserializes a JSON string into an object of the given type. + * + * @param the target type + * @param body the JSON string + * @param type the target type (may be parameterized for generic types) + * @return the deserialized object + */ + public abstract T readValue(String body, Type type); + + /** + * Serializes an object to a JSON byte array. + * + * @param value the object to serialize + * @return the JSON bytes + */ + public abstract byte[] writeValueAsBytes(Object value); + + /** + * Serializes an object to a JSON string. + * + * @param value the object to serialize + * @return the JSON string + */ + public abstract String writeValueAsString(Object value); + + /** + * Serializes an object to a pretty-printed JSON string. + * + * @param value the object to serialize + * @return the pretty-printed JSON string + */ + public abstract String writeValueAsPrettyString(Object value); + + protected final String getDefaultBaseUri() { + return basePath; + } + + public static HttpClient.Builder createDefaultHttpClientBuilder() { + return HttpClient.newBuilder(); + } + + public final void updateBaseUri(String baseUri) { + URI uri = URI.create(baseUri); + scheme = uri.getScheme(); + host = uri.getHost(); + port = uri.getPort(); + basePath = uri.getRawPath(); + + if (Objects.equals(scheme, "http")) { + builder.version(HttpClient.Version.HTTP_1_1); + } + else { + builder.version(HttpClient.Version.HTTP_2); + } + } + + /** + * Set a custom {@link HttpClient.Builder} object to use when creating the + * {@link HttpClient} that is used by the API client. + * + * @param builder Custom client builder. + * @return This object. + */ + public ApiClient setHttpClientBuilder(HttpClient.Builder builder) { + this.builder = builder; + return this; + } + + /** + * Get an {@link HttpClient} based on the current {@link HttpClient.Builder}. + * + *

The returned object is immutable and thread-safe.

+ * + * @return The HTTP client. + */ + public HttpClient getHttpClient() { + return builder.build(); + } + + /** + * Set a custom host name for the target service. + * + * @param host The host name of the target service. + * @return This object. + */ + public ApiClient setHost(String host) { + this.host = host; + return this; + } + + /** + * Set a custom port number for the target service. + * + * @param port The port of the target service. Set this to -1 to reset the + * value to the default for the scheme. + * @return This object. + */ + public ApiClient setPort(int port) { + this.port = port; + return this; + } + + /** + * Set a custom base path for the target service, for example '/v2'. + * + * @param basePath The base path against which the rest of the path is + * resolved. + * @return This object. + */ + public ApiClient setBasePath(String basePath) { + this.basePath = basePath; + return this; + } + + /** + * Get the base URI to resolve the endpoint paths against. + * + * @return The complete base URI that the rest of the API parameters are + * resolved against. + */ + public String getBaseUri() { + return scheme + "://" + host + (port == -1 ? "" : ":" + port) + basePath; + } + + /** + * Set a custom scheme for the target service, for example 'https'. + * + * @param scheme The scheme of the target service + * @return This object. + */ + public ApiClient setScheme(String scheme){ + this.scheme = scheme; + return this; + } + + /** + * Set a custom request interceptor. + * + *

A request interceptor is a mechanism for altering each request before it + * is sent. After the request has been fully configured but not yet built, the + * request builder is passed into this function for further modification, + * after which it is sent out.

+ * + *

This is useful for altering the requests in a custom manner, such as + * adding headers. It could also be used for logging and monitoring.

+ * + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setRequestInterceptor(Consumer interceptor) { + this.interceptor = interceptor; + return this; + } + + /** + * Get the custom interceptor. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public Consumer getRequestInterceptor() { + return interceptor; + } + + /** + * Set a custom response interceptor. + * + *

This is useful for logging, monitoring or extraction of header variables

+ *{{#useUnaryInterceptor}}

If you are using the UnaryInterceptor you can even manipulate the response to a certain degree

{{/useUnaryInterceptor}} + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setResponseInterceptor({{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}> interceptor) { + this.responseInterceptor = interceptor; + return this; + } + + /** + * Get the custom response interceptor. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public {{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}> getResponseInterceptor() { + return responseInterceptor; + } + + /** + * Set a custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. + * + *

This is useful for logging, monitoring or extraction of header variables

+ *{{#useUnaryInterceptor}}

If you are using the UnaryInterceptor you can even manipulate the response to a certain degree

{{/useUnaryInterceptor}} + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setAsyncResponseInterceptor({{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}> interceptor) { + this.asyncResponseInterceptor = interceptor; + return this; + } + + /** + * Get the custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public {{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}> getAsyncResponseInterceptor() { + return asyncResponseInterceptor; + } + + /** + * Set the read timeout for the http client. + * + *

This is the value used by default for each request, though it can be + * overridden on a per-request basis with a request interceptor.

+ * + * @param readTimeout The read timeout used by default by the http client. + * Setting this value to null resets the timeout to an + * effectively infinite value. + * @return This object. + */ + public ApiClient setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + /** + * Get the read timeout that was set. + * + * @return The read timeout, or null if no timeout was set. Null represents + * an infinite wait time. + */ + public Duration getReadTimeout() { + return readTimeout; + } + /** + * Sets the connect timeout (in milliseconds) for the http client. + * + *

In the case where a new connection needs to be established, if + * the connection cannot be established within the given {@code + * duration}, then {@link HttpClient#send(HttpRequest,BodyHandler) + * HttpClient::send} throws an {@link HttpConnectTimeoutException}, or + * {@link HttpClient#sendAsync(HttpRequest,BodyHandler) + * HttpClient::sendAsync} completes exceptionally with an + * {@code HttpConnectTimeoutException}. If a new connection does not + * need to be established, for example if a connection can be reused + * from a previous request, then this timeout duration has no effect. + * + * @param connectTimeout connection timeout in milliseconds + * + * @return This object. + */ + public ApiClient setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + this.builder.connectTimeout(connectTimeout); + return this; + } + + /** + * Get connection timeout (in milliseconds). + * + * @return Timeout in milliseconds + */ + public Duration getConnectTimeout() { + return connectTimeout; + } + + /** + * Returns whether request logging is enabled. + * + * @return {@code true} if request logging is enabled + */ + public boolean isLogRequests() { + return logRequests; + } + + /** + * Sets whether request logging is enabled. + * + * @param logRequests {@code true} to enable request logging + * @return this object + */ + public ApiClient setLogRequests(boolean logRequests) { + this.logRequests = logRequests; + return this; + } + + /** + * Returns whether response logging is enabled. + * + * @return {@code true} if response logging is enabled + */ + public boolean isLogResponses() { + return logResponses; + } + + /** + * Sets whether response logging is enabled. + * + * @param logResponses {@code true} to enable response logging + * @return this object + */ + public ApiClient setLogResponses(boolean logResponses) { + this.logResponses = logResponses; + return this; + } + + /** + * Returns whether JSON pretty-printing is enabled for logging. + * + * @return {@code true} if pretty-printing is enabled + */ + public boolean isPrettyPrintJson() { + return prettyPrintJson; + } + + /** + * Sets whether JSON pretty-printing is enabled for logging. + * + * @param prettyPrintJson {@code true} to enable pretty-printing + * @return this object + */ + public ApiClient setPrettyPrintJson(boolean prettyPrintJson) { + this.prettyPrintJson = prettyPrintJson; + return this; + } + + /** + * Logs the HTTP request method, URI, and headers. Sensitive headers + * (e.g. {@code Authorization}) are masked. + * + * @param request the HTTP request to log + */ + public void logRequest(HttpRequest request) { + if (LOG.isInfoEnabled()) { + var stringBuilder = new StringBuilder(); + stringBuilder.append("\nβ†’ REQUEST: %s %s\n".formatted(request.method(), request.uri())); + stringBuilder.append(" HEADERS:\n"); + + request.headers() + .map() + .entrySet() + .stream() + .map(this::maskSensitiveHeaderValues) + .forEach(entry -> stringBuilder.append(" %s: %s\n".formatted(entry.getKey(), String.join(", ", entry.getValue())))); + + LOG.info(stringBuilder.toString()); + } + } + + /** + * Logs the HTTP request body. If pretty-printing is enabled, the body is + * re-serialized with indentation. + * + * @param body the serialized request body bytes + */ + public void logRequestBody(byte[] body) { + if (LOG.isInfoEnabled() && body != null && body.length > 0) { + var bodyString = new String(body, java.nio.charset.StandardCharsets.UTF_8); + + if (prettyPrintJson) { + try { + bodyString = writeValueAsPrettyString(readValue(bodyString, Object.class)); + } + catch (Exception ignored) { + } + } + + LOG.info("\nβ†’ REQUEST BODY:\n{}", bodyString); + } + } + + /** + * Logs the HTTP response status code, headers, and optionally the response body. + * If pretty-printing is enabled, the body is re-serialized with indentation. + * + * @param statusCode the HTTP response status code + * @param headers the response headers + * @param responseBody the response body, or {@code null} if unavailable + */ + public void logResponse(int statusCode, Map> headers, String responseBody) { + if (LOG.isInfoEnabled()) { + var stringBuilder = new StringBuilder(); + stringBuilder.append("\n← RESPONSE: %s\n".formatted(statusCode)); + stringBuilder.append(" HEADERS:\n"); + + headers.forEach((key, values) -> + stringBuilder.append(" %s: %s\n".formatted(key, String.join(", ", values)))); + + if (responseBody != null && !responseBody.isBlank()) { + var body = responseBody; + + if (prettyPrintJson) { + try { + body = writeValueAsPrettyString(readValue(responseBody, Object.class)); + } + catch (Exception ignored) { + } + } + + stringBuilder.append(" BODY:\n%s".formatted(body)); + } + + LOG.info(stringBuilder.toString()); + } + } + + private boolean isSensitiveHeader(String headerName) { + return AUTHORIZATION_HEADER.equalsIgnoreCase(headerName); + } + + private Map.Entry> maskSensitiveHeaderValues(Map.Entry> entry) { + return Map.entry( + entry.getKey(), + entry.getValue().stream() + .map(value -> isSensitiveHeader(entry.getKey()) ? "*".repeat(value.length()) : value) + .toList() + ); + } + + /** + * Returns the response body InputStream, transparently decoding gzip-compressed + * payloads when the server sets {@code Content-Encoding: gzip}. + * + * @param response HTTP response whose body should be consumed + * @return Original or decompressed InputStream for the response body + * @throws IOException if the response body cannot be accessed or wrapping fails + */ + public static InputStream getResponseBody(HttpResponse response) throws IOException { + if (response == null) { + return null; + } + InputStream body = response.body(); + if (body == null) { + return null; + } + Optional encoding = response.headers().firstValue("Content-Encoding"); + if (encoding.isPresent()) { + for (String token : encoding.get().split(",")) { + if ("gzip".equalsIgnoreCase(token.trim())) { + return new GZIPInputStream(body, 8192); + } + } + } + return body; + } + +{{#useGzipFeature}} + /** + * Wraps a request body supplier with a streaming GZIP compressor so large payloads + * can be sent without buffering the entire contents in memory. + * + * @param bodySupplier Supplies the original request body InputStream + * @return BodyPublisher that emits gzip-compressed bytes from the supplied stream + */ + public static HttpRequest.BodyPublisher gzipRequestBody(Supplier bodySupplier) { + Objects.requireNonNull(bodySupplier, "bodySupplier must not be null"); + return HttpRequest.BodyPublishers.ofInputStream(() -> new GzipCompressingInputStream(bodySupplier)); + } + + private static final class GzipCompressingInputStream extends InputStream { + private final Supplier supplier; + private final byte[] readBuffer = new byte[8192]; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private InputStream source; + private GZIPOutputStream gzipStream; + private byte[] currentChunk = new byte[0]; + private int chunkPosition = 0; + private boolean finished = false; + + private GzipCompressingInputStream(Supplier supplier) { + this.supplier = Objects.requireNonNull(supplier, "bodySupplier must not be null"); + } + + private void ensureInitialized() throws IOException { + if (source == null) { + source = Objects.requireNonNull(supplier.get(), "bodySupplier returned null InputStream"); + gzipStream = new GZIPOutputStream(buffer, true); + } + } + + private boolean fillBuffer() throws IOException { + while (chunkPosition >= currentChunk.length) { + buffer.reset(); + ensureInitialized(); + if (finished) { + return false; + } + int bytesRead = source.read(readBuffer); + if (bytesRead == -1) { + gzipStream.finish(); + gzipStream.close(); + source.close(); + finished = true; + } else { + gzipStream.write(readBuffer, 0, bytesRead); + gzipStream.flush(); + } + currentChunk = buffer.toByteArray(); + chunkPosition = 0; + if (currentChunk.length == 0 && !finished) { + continue; + } + if (currentChunk.length == 0 && finished) { + return false; + } + return true; + } + return true; + } + + @Override + public int read() throws IOException { + if (!fillBuffer()) { + return -1; + } + return currentChunk[chunkPosition++] & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + if (!fillBuffer()) { + return -1; + } + int bytesToCopy = Math.min(len, currentChunk.length - chunkPosition); + System.arraycopy(currentChunk, chunkPosition, b, off, bytesToCopy); + chunkPosition += bytesToCopy; + return bytesToCopy; + } + + @Override + public void close() throws IOException { + IOException exception = null; + if (source != null) { + try { + source.close(); + } catch (IOException e) { + exception = e; + } finally { + source = null; + } + } + if (gzipStream != null) { + try { + gzipStream.close(); + } catch (IOException e) { + exception = exception == null ? e : exception; + } finally { + gzipStream = null; + } + } + if (exception != null) { + throw exception; + } + } + } +{{/useGzipFeature}} + + /** + * Captures generic type information at runtime via anonymous subclassing. + * Used to pass type information to JSON serializers without depending on a specific Jackson version. + * + *

Usage: {@code new ApiClient.TypeToken>() {}.getType()} + * + * @param the type to capture + */ + public abstract static class TypeToken { + private final Type type; + + protected TypeToken() { + Type superclass = getClass().getGenericSuperclass(); + this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } + + public Type getType() { + return type; + } + } +} diff --git a/langfuse-java-client/src/main/templates/api.mustache b/langfuse-java-client/src/main/templates/api.mustache new file mode 100644 index 0000000..15c863d --- /dev/null +++ b/langfuse-java-client/src/main/templates/api.mustache @@ -0,0 +1,856 @@ +{{>licenseInfo}} +{{#operations}} +package {{package}}.{{classVarName}}{{#asyncNative}}.async{{/asyncNative}}; +{{/operations}} + +import {{invokerPackage}}.ApiClient; +import {{invokerPackage}}.ApiResponse; +import {{invokerPackage}}.Pair; +import com.langfuse.api.LangfuseApiException; + +{{#imports}} +import {{import}}; +{{/imports}} + +import java.lang.reflect.Type; + +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; + +{{/useBeanValidation}} +{{#hasFormParamsInSpec}} +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; + +{{/hasFormParamsInSpec}} +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.http.HttpRequest; +import java.nio.channels.Channels; +import java.nio.channels.Pipe; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import java.util.ArrayList; +import java.util.StringJoiner; +import java.util.List; +import java.util.Map; +import java.util.Set; +{{#useUnaryInterceptor}} +import java.util.function.UnaryOperator; +{{/useUnaryInterceptor}} +import java.util.function.Consumer; +{{#useGzipFeature}} +import java.util.function.Supplier; +{{/useGzipFeature}} +{{#asyncNative}} + +import java.util.concurrent.CompletableFuture; +{{/asyncNative}} + +{{>generatedAnnotation}} + +{{#operations}} +/** + * {{classname}} - {{^asyncNative}}synchronous{{/asyncNative}}{{#asyncNative}}asynchronous{{/asyncNative}} client implementation + * using Java's built-in {@link java.net.http.HttpClient}. + */ +public class {{classname}} implements com.langfuse.api.{{classVarName}}{{#asyncNative}}.async{{/asyncNative}}.{{classname}} { + /** + * Utility class for extending HttpRequest.Builder functionality. + */ + private static class HttpRequestBuilderExtensions { + /** + * Adds additional headers to the provided HttpRequest.Builder. Useful for adding method/endpoint specific headers. + * + * @param builder the HttpRequest.Builder to which headers will be added + * @param headers a map of header names and values to add; may be null + * @return the same HttpRequest.Builder instance with the additional headers set + */ + static HttpRequest.Builder withAdditionalHeaders(HttpRequest.Builder builder, Map headers) { + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + } + return builder; + } + } + private final ApiClient memberVarApiClient; + private final HttpClient memberVarHttpClient; + private final String memberVarBaseUri; + private final Consumer memberVarInterceptor; + private final Duration memberVarReadTimeout; + private final Consumer> memberVarResponseInterceptor; + private final Consumer> memberVarAsyncResponseInterceptor; + + public {{classname}}(ApiClient apiClient) { + memberVarApiClient = apiClient; + memberVarHttpClient = apiClient.getHttpClient(); + memberVarBaseUri = apiClient.getBaseUri(); + memberVarInterceptor = apiClient.getRequestInterceptor(); + memberVarReadTimeout = apiClient.getReadTimeout(); + memberVarResponseInterceptor = apiClient.getResponseInterceptor(); + memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor(); + } + + {{#asyncNative}} + + private LangfuseApiException getApiException(String operationId, HttpResponse response) { + try { + InputStream responseBody = ApiClient.getResponseBody(response); + String body = null; + if (responseBody != null) { + body = new String(responseBody.readAllBytes()); + responseBody.close(); + } + String message = formatExceptionMessage(operationId, response.statusCode(), body); + return new LangfuseApiException(message, response.statusCode()); + } catch (IOException e) { + return new LangfuseApiException(e); + } + } + {{/asyncNative}} + {{^asyncNative}} + + protected LangfuseApiException getApiException(String operationId, HttpResponse response) { + try { + InputStream responseBody = ApiClient.getResponseBody(response); + String body = null; + try { + body = responseBody == null ? null : new String(responseBody.readAllBytes()); + } finally { + if (responseBody != null) { + responseBody.close(); + } + } + String message = formatExceptionMessage(operationId, response.statusCode(), body); + return new LangfuseApiException(message, response.statusCode()); + } catch (IOException e) { + return new LangfuseApiException(e); + } + } + {{/asyncNative}} + + private String formatExceptionMessage(String operationId, int statusCode, String body) { + if (body == null || body.isEmpty()) { + body = "[no body]"; + } + return operationId + " call failed with: " + statusCode + " - " + body; + } + + /** + * Download file from the given response. + * + * @param response Response + * @return File + * @throws LangfuseApiException If fail to read file content from response and write to disk + */ + public File downloadFileFromResponse(HttpResponse response, InputStream responseBody) { + if (responseBody == null) { + throw new LangfuseApiException(new IOException("Response body is empty")); + } + try { + File file = prepareDownloadFile(response); + java.nio.file.Files.copy(responseBody, file.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + return file; + } catch (IOException e) { + throw new LangfuseApiException(e); + } + } + + /** + *

Prepare the file for download from the response.

+ * + * @param response a {@link java.net.http.HttpResponse} object. + * @return a {@link java.io.File} object. + * @throws java.io.IOException if any. + */ + private File prepareDownloadFile(HttpResponse response) throws IOException { + String filename = null; + java.util.Optional contentDisposition = response.headers().firstValue("Content-Disposition"); + if (contentDisposition.isPresent() && !"".equals(contentDisposition.get())) { + // Get filename from the Content-Disposition header. + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?"); + java.util.regex.Matcher matcher = pattern.matcher(contentDisposition.get()); + if (matcher.find()) + filename = matcher.group(1); + } + File file = null; + if (filename != null) { + java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("swagger-gen-native"); + java.nio.file.Path filePath = java.nio.file.Files.createFile(tempDir.resolve(filename)); + file = filePath.toFile(); + tempDir.toFile().deleteOnExit(); // best effort cleanup + file.deleteOnExit(); // best effort cleanup + } else { + file = java.nio.file.Files.createTempFile("download-", "").toFile(); + file.deleteOnExit(); // best effort cleanup + } + return file; + } + + {{#operation}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + {{#returnType}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} + {{/returnType}} + {{^returnType}} + {{#asyncNative}} + * @return CompletableFuture<Void> + {{/asyncNative}} + {{/returnType}} + * @throws LangfuseApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + @Override + public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest) { + {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}(apiRequest, null); + } + + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + * @param headers Optional headers to include in the request + {{#returnType}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} + {{/returnType}} + {{^returnType}} + {{#asyncNative}} + * @return CompletableFuture<Void> + {{/asyncNative}} + {{/returnType}} + * @throws LangfuseApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest, Map headers) { + {{#allParams}} + {{>nullable_var_annotations}}{{! prevent indent}} + {{>nullableDataType}} {{paramName}} = apiRequest.{{paramName}}(); + {{/allParams}} + {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + } + + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws LangfuseApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest) { + return {{operationId}}WithHttpInfo(apiRequest, null); + } + + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request} + * @param headers Optional headers to include in the request + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws LangfuseApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(com.langfuse.api.{{classVarName}}.{{classname}}.API{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request apiRequest, Map headers) { + {{#allParams}} + {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); + {{/allParams}} + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + } + + {{/hasParams}} + {{/vendorExtensions.x-group-parameters}} + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + {{#vendorExtensions.x-group-parameters}}{{#hasParams}}private{{/hasParams}}{{^hasParams}}public{{/hasParams}}{{/vendorExtensions.x-group-parameters}}{{^vendorExtensions.x-group-parameters}}public{{/vendorExtensions.x-group-parameters}} {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{>nullableArgument}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) { + {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}null); + } + + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + {{#vendorExtensions.x-group-parameters}}{{#hasParams}}private{{/hasParams}}{{^hasParams}}public{{/hasParams}}{{/vendorExtensions.x-group-parameters}}{{^vendorExtensions.x-group-parameters}}public{{/vendorExtensions.x-group-parameters}} {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{>nullableArgument}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}Map headers) { + {{^asyncNative}} + {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + {{#returnType}} + return localVarResponse.getData(); + {{/returnType}} + {{/asyncNative}} + {{#asyncNative}} + try { + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers) + .thenApply(ApiResponse::getData); + } + catch (LangfuseApiException e) { + return CompletableFuture.failedFuture(e); + } + {{/asyncNative}} + } + + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}{{/isContainer}}){{/required}} + {{/allParams}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws LangfuseApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + {{#vendorExtensions.x-group-parameters}}{{#hasParams}}private{{/hasParams}}{{^hasParams}}public{{/hasParams}}{{/vendorExtensions.x-group-parameters}}{{^vendorExtensions.x-group-parameters}}public{{/vendorExtensions.x-group-parameters}} {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{>nullableArgument}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) { + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}null); + } + + {{#vendorExtensions.x-group-parameters}}{{#hasParams}}private{{/hasParams}}{{^hasParams}}public{{/hasParams}}{{/vendorExtensions.x-group-parameters}}{{^vendorExtensions.x-group-parameters}}public{{/vendorExtensions.x-group-parameters}} {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{>nullableArgument}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}Map headers) { + {{^asyncNative}} + HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + try { + HttpRequest localVarRequest = localVarRequestBuilder.build(); + if (memberVarApiClient.isLogRequests()) { + memberVarApiClient.logRequest(localVarRequest); + } + HttpResponse localVarResponse = memberVarHttpClient.send( + localVarRequest, + HttpResponse.BodyHandlers.ofInputStream()); + if (memberVarResponseInterceptor != null) { + {{#useUnaryInterceptor}} + localVarResponse = memberVarResponseInterceptor.apply(localVarResponse); + {{/useUnaryInterceptor}} + {{^useUnaryInterceptor}} + memberVarResponseInterceptor.accept(localVarResponse); + {{/useUnaryInterceptor}} + } + InputStream localVarResponseBody = null; + try { + if (localVarResponse.statusCode()/ 100 != 2) { + throw getApiException("{{operationId}}", localVarResponse); + } + {{#vendorExtensions.x-java-text-plain-string}} + // for plain text response + if (localVarResponse.headers().map().containsKey("Content-Type") && + "text/plain".equalsIgnoreCase(localVarResponse.headers().map().get("Content-Type").get(0).split(";")[0].trim())) { + localVarResponseBody = ApiClient.getResponseBody(localVarResponse); + java.util.Scanner s = new java.util.Scanner(localVarResponseBody == null ? InputStream.nullInputStream() : localVarResponseBody).useDelimiter("\\A"); + String responseBodyText = s.hasNext() ? s.next() : ""; + return new ApiResponse( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseBodyText + ); + } else { + throw new RuntimeException("Error! The response Content-Type is supposed to be `text/plain` but it's not: " + localVarResponse); + } + {{/vendorExtensions.x-java-text-plain-string}} + {{^vendorExtensions.x-java-text-plain-string}} + {{#returnType}} + {{! Fix for https://github.com/OpenAPITools/openapi-generator/issues/13968 }} + {{! This part had a bugfix for an empty response in the past, but this part of that PR was reverted because it was not doing anything. }} + {{! Keep this documentation here, because the problem is not obvious. }} + {{! `InputStream.available()` was used, but that only works for inputstreams that are already in memory, it will not give the right result if it is a remote stream. We only work with remote streams here. }} + {{! https://github.com/OpenAPITools/openapi-generator/pull/13993/commits/3e!37411d2acef0311c82e6d941a8e40b3bc0b6da }} + {{! The `available` method would work with a `PushbackInputStream`, because we could read 1 byte to check if it exists then push it back so Jackson can read it again. The issue with that is that it will also insert an ascii character for "head of input" and that will break Jackson as it does not handle special whitespace characters. }} + {{! A fix for that problem is to read it into a string and remove those characters, but if we need to read it before giving it to jackson to fix the string then just reading it into a string as is to do an emptiness check is the cleaner solution. }} + {{! We could also manipulate the inputstream to remove that bad character, but string manipulation is easier to read and this codepath is not asyncronus so we do not gain anything by reading the stream later. }} + {{! This fix does make it unsuitable for large amounts of data because `InputStream.readAllbytes` is not meant for it, but a synchronous client is already not the right tool for that.}} + localVarResponseBody = ApiClient.getResponseBody(localVarResponse); + if (localVarResponseBody == null) { + return new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + null + ); + } + + {{^isResponseFile}}{{#isResponseBinary}} + Byte[] responseValue = localVarResponseBody.readAllBytes(); + {{/isResponseBinary}}{{/isResponseFile}} + {{#isResponseFile}} + // Handle file downloading. + File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody); + {{/isResponseFile}} + {{^isResponseBinary}}{{^isResponseFile}} + String responseBody = new String(localVarResponseBody.readAllBytes()); + if (memberVarApiClient.isLogResponses()) { + memberVarApiClient.logResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), responseBody); + } + {{{returnType}}} responseValue = responseBody.isBlank()? null: memberVarApiClient.readValue(responseBody, new ApiClient.TypeToken<{{{returnType}}}>() {}.getType()); + {{/isResponseFile}}{{/isResponseBinary}} + + return new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseValue + ); + {{/returnType}} + {{^returnType}} + localVarResponseBody = ApiClient.getResponseBody(localVarResponse); + String responseBody = null; + if (localVarResponseBody != null) { + responseBody = new String(localVarResponseBody.readAllBytes()); + } + if (memberVarApiClient.isLogResponses()) { + memberVarApiClient.logResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), responseBody); + } + return new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + null + ); + {{/returnType}} + {{/vendorExtensions.x-java-text-plain-string}} + } finally { + if (localVarResponseBody != null) { + localVarResponseBody.close(); + } + } + } catch (IOException {{#returnType}}{{#useJackson3}}| JacksonException {{/useJackson3}}{{/returnType}}e) { + throw new LangfuseApiException(e); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new LangfuseApiException(e); + } + {{/asyncNative}} + {{#asyncNative}} + try { + HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}headers); + HttpRequest localVarRequest = localVarRequestBuilder.build(); + if (memberVarApiClient.isLogRequests()) { + memberVarApiClient.logRequest(localVarRequest); + } + return memberVarHttpClient.sendAsync( + localVarRequest, + HttpResponse.BodyHandlers.ofInputStream()).thenComposeAsync(localVarResponse -> { + if (memberVarAsyncResponseInterceptor != null) { + {{#useUnaryInterceptor}} + localVarResponse = memberVarAsyncResponseInterceptor.apply(localVarResponse); + {{/useUnaryInterceptor}} + {{^useUnaryInterceptor}} + memberVarAsyncResponseInterceptor.accept(localVarResponse); + {{/useUnaryInterceptor}} + } + if (localVarResponse.statusCode()/ 100 != 2) { + return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); + } + try { + InputStream localVarResponseBody = ApiClient.getResponseBody(localVarResponse); + try { + {{#vendorExtensions.x-java-text-plain-string}} + if (localVarResponse.headers().map().containsKey("Content-Type") && + "text/plain".equalsIgnoreCase(localVarResponse.headers().map().get("Content-Type").get(0).split(";")[0].trim())) { + java.util.Scanner s = new java.util.Scanner(localVarResponseBody == null ? InputStream.nullInputStream() : localVarResponseBody).useDelimiter("\\A"); + String responseBodyText = s.hasNext() ? s.next() : ""; + return CompletableFuture.completedFuture( + new ApiResponse( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseBodyText + ) + ); + } else { + return CompletableFuture.failedFuture(new RuntimeException("Error! The response Content-Type is supposed to be `text/plain` but it's not: " + localVarResponse)); + } + {{/vendorExtensions.x-java-text-plain-string}} + {{^vendorExtensions.x-java-text-plain-string}} + {{#returnType}} + if (localVarResponseBody == null) { + return CompletableFuture.completedFuture( + new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + null + ) + ); + } + {{^isResponseFile}}{{#isResponseBinary}} + Byte[] responseValue = localVarResponseBody.readAllBytes(); + {{/isResponseBinary}}{{/isResponseFile}} + {{#isResponseFile}} + File responseValue = downloadFileFromResponse(localVarResponse, localVarResponseBody); + {{/isResponseFile}} + {{^isResponseBinary}}{{^isResponseFile}} + String responseBody = new String(localVarResponseBody.readAllBytes()); + if (memberVarApiClient.isLogResponses()) { + memberVarApiClient.logResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), responseBody); + } + {{{returnType}}} responseValue = responseBody.isBlank()? null: memberVarApiClient.readValue(responseBody, new ApiClient.TypeToken<{{{returnType}}}>() {}.getType()); + {{/isResponseFile}}{{/isResponseBinary}} + return CompletableFuture.completedFuture( + new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseValue + ) + ); + {{/returnType}} + {{^returnType}} + String responseBody = null; + if (localVarResponseBody != null) { + responseBody = new String(localVarResponseBody.readAllBytes()); + } + if (memberVarApiClient.isLogResponses()) { + memberVarApiClient.logResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), responseBody); + } + return CompletableFuture.completedFuture( + new ApiResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), null) + ); + {{/returnType}} + {{/vendorExtensions.x-java-text-plain-string}} + } finally { + if (localVarResponseBody != null) { + localVarResponseBody.close(); + } + } + } catch (IOException {{#returnType}}{{#useJackson3}}| JacksonException {{/useJackson3}}{{/returnType}}e) { + return CompletableFuture.failedFuture(new LangfuseApiException(e)); + } + } + ); + } + catch (LangfuseApiException e) { + return CompletableFuture.failedFuture(e); + } + {{/asyncNative}} + } + + private HttpRequest.Builder {{operationId}}RequestBuilder({{#allParams}}{{>nullableArgument}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}Map headers) { + {{#allParams}} + {{#required}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == null) { + throw new LangfuseApiException("Missing the required parameter '{{paramName}}' when calling {{operationId}}", 400); + } + {{/required}} + {{/allParams}} + + HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + + {{! Switch delimiters for baseName so we can write constants like "{query}" }} + String localVarPath = "{{{path}}}"{{#pathParams}} + .replace({{=<% %>=}}"{<%baseName%>}"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}}; + + {{#hasQueryParams}} + List localVarQueryParams = new ArrayList<>(); + StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); + String localVarQueryParameterBaseName; + {{#queryParams}} + localVarQueryParameterBaseName = "{{{baseName}}}"; + {{#collectionFormat}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}})); + {{/collectionFormat}} + {{^collectionFormat}} + {{#isDeepObject}} + if ({{paramName}} != null) { + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format(java.util.Locale.ROOT, "{{baseName}}[%d]", i))); + } + {{/isArray}} + {{^isArray}} + String queryString = {{paramName}}.toUrlQueryString("{{baseName}}"); + if (!queryString.isBlank()) { + localVarQueryStringJoiner.add(queryString); + } + {{/isArray}} + } + {{/isDeepObject}} + {{^isDeepObject}} + {{#isExplode}} + {{#hasVars}} + {{#vars}} + {{#isArray}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("multi", "{{baseName}}", {{paramName}}.{{getter}}())); + {{/isArray}} + {{^isArray}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}}.{{getter}}())); + {{/isArray}} + {{/vars}} + {{/hasVars}} + {{^hasVars}} + {{#isModel}} + localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString()); + {{/isModel}} + {{^isModel}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); + {{/isModel}} + {{/hasVars}} + {{/isExplode}} + {{^isExplode}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); + {{/isExplode}} + {{/isDeepObject}} + {{/collectionFormat}} + {{/queryParams}} + + if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) { + StringJoiner queryJoiner = new StringJoiner("&"); + localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue())); + if (localVarQueryStringJoiner.length() != 0) { + queryJoiner.add(localVarQueryStringJoiner.toString()); + } + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString())); + } else { + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); + } + {{/hasQueryParams}} + {{^hasQueryParams}} + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); + {{/hasQueryParams}} + + {{#headerParams}} + if ({{paramName}} != null) { + localVarRequestBuilder.header("{{baseName}}", {{paramName}}.toString()); + } + {{/headerParams}} + {{#bodyParam}} + localVarRequestBuilder.header("Content-Type", "{{#hasConsumes}}{{#consumes}}{{#-first}}{{{mediaType}}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}"); + {{/bodyParam}} + localVarRequestBuilder.header("Accept", "{{#hasProduces}}{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}"); + {{#useGzipFeature}} + localVarRequestBuilder.header("Accept-Encoding", "gzip"); + {{/useGzipFeature}} + + {{#bodyParam}} + {{#isString}} + {{#useGzipFeature}} + Supplier localVarRequestBodySupplier = () -> new ByteArrayInputStream({{paramName}}.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + localVarRequestBuilder.header("Content-Encoding", "gzip"); + localVarRequestBuilder.method("{{httpMethod}}", ApiClient.gzipRequestBody(localVarRequestBodySupplier)); + {{/useGzipFeature}} + {{^useGzipFeature}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofString({{paramName}})); + {{/useGzipFeature}} + {{/isString}} + {{^isString}} + byte[] localVarPostBody = memberVarApiClient.writeValueAsBytes({{paramName}}); + if (memberVarApiClient.isLogRequests()) { + memberVarApiClient.logRequestBody(localVarPostBody); + } + {{#useGzipFeature}} + Supplier localVarRequestBodySupplier = () -> new ByteArrayInputStream(localVarPostBody); + localVarRequestBuilder.header("Content-Encoding", "gzip"); + localVarRequestBuilder.method("{{httpMethod}}", ApiClient.gzipRequestBody(localVarRequestBodySupplier)); + {{/useGzipFeature}} + {{^useGzipFeature}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); + {{/useGzipFeature}} + {{/isString}} + {{/bodyParam}} + {{^bodyParam}} + {{#hasFormParams}} + {{#isMultipart}} + MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create(); + boolean hasFiles = false; + {{#formParams}} + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + {{#isFile}} + multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}.get(i)); + hasFiles = true; + {{/isFile}} + {{^isFile}} + if ({{paramName}}.get(i) != null) { + multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.get(i).toString()); + } + {{/isFile}} + } + {{/isArray}} + {{^isArray}} + {{#isFile}} + multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}); + hasFiles = true; + {{/isFile}} + {{^isFile}} + if ({{paramName}} != null) { + multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.toString()); + } + {{/isFile}} + {{/isArray}} + {{/formParams}} + HttpEntity entity = multiPartBuilder.build(); + {{#useGzipFeature}} + Supplier formDataSupplier; + if (hasFiles) { + Pipe pipe; + try { + pipe = Pipe.open(); + } catch (IOException e) { + throw new RuntimeException(e); + } + new Thread(() -> { + try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) { + entity.writeTo(outputStream); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + formDataSupplier = () -> Channels.newInputStream(pipe.source()); + } else { + ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); + try { + entity.writeTo(formOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] formBytes = formOutputStream.toByteArray(); + formDataSupplier = () -> new ByteArrayInputStream(formBytes); + } + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .header("Content-Encoding", "gzip") + .method("{{httpMethod}}", ApiClient.gzipRequestBody(formDataSupplier)); + {{/useGzipFeature}} + {{^useGzipFeature}} + HttpRequest.BodyPublisher formDataPublisher; + if (hasFiles) { + Pipe pipe; + try { + pipe = Pipe.open(); + } catch (IOException e) { + throw new RuntimeException(e); + } + new Thread(() -> { + try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) { + entity.writeTo(outputStream); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + formDataPublisher = HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source())); + } else { + ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); + try { + entity.writeTo(formOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] formBytes = formOutputStream.toByteArray(); + formDataPublisher = HttpRequest.BodyPublishers + .ofInputStream(() -> new ByteArrayInputStream(formBytes)); + } + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .method("{{httpMethod}}", formDataPublisher); + {{/useGzipFeature}} + {{/isMultipart}} + {{^isMultipart}} + List formValues = new ArrayList<>(); + {{#formParams}} + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + if ({{paramName}}.get(i) != null) { + formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.get(i).toString())); + } + } + {{/isArray}} + {{^isArray}} + if ({{paramName}} != null) { + formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.toString())); + } + {{/isArray}} + {{/formParams}} + HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8); + ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); + try { + entity.writeTo(formOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] formBytes = formOutputStream.toByteArray(); + {{#useGzipFeature}} + Supplier formDataSupplier = () -> new ByteArrayInputStream(formBytes); + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .header("Content-Encoding", "gzip") + .method("{{httpMethod}}", ApiClient.gzipRequestBody(formDataSupplier)); + {{/useGzipFeature}} + {{^useGzipFeature}} + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .method("{{httpMethod}}", HttpRequest.BodyPublishers + .ofInputStream(() -> new ByteArrayInputStream(formBytes))); + {{/useGzipFeature}} + {{/isMultipart}} + {{/hasFormParams}} + {{^hasFormParams}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.noBody()); + {{/hasFormParams}} + {{/bodyParam}} + if (memberVarReadTimeout != null) { + localVarRequestBuilder.timeout(memberVarReadTimeout); + } + // Add custom headers if provided + localVarRequestBuilder = HttpRequestBuilderExtensions.withAdditionalHeaders(localVarRequestBuilder, headers); + if (memberVarInterceptor != null) { + memberVarInterceptor.accept(localVarRequestBuilder); + } + return localVarRequestBuilder; + } + + {{/operation}} +} +{{/operations}} \ No newline at end of file diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AbstractLangfuseClientTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/AbstractLangfuseClientTest.java new file mode 100644 index 0000000..99b658b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AbstractLangfuseClientTest.java @@ -0,0 +1,66 @@ +package com.langfuse.client; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.TestWatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.langfuse.api.LangfuseApi; +import com.langfuse.testcontainers.LangfuseContainer; + +/** + * Abstract base class for Langfuse client integration tests. + * + *

Uses the Testcontainers singleton container pattern to start a single + * {@link LangfuseContainer} instance that is shared across all test classes. + * The container starts once per JVM and is cleaned up automatically by Ryuk. + * + * @author Eric Deandrea + * @see Singleton Containers + */ +abstract class AbstractLangfuseClientTest { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractLangfuseClientTest.class); + + static LangfuseContainer langfuse = new LangfuseContainer(); + static LangfuseApi client; + + static { + langfuse.start(); + client = createClient(); + } + + @RegisterExtension + TestWatcher watcher = new TestWatcher() { + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + LOG.error("Test {}.{} failed: {}", + context.getTestClass().map(Class::getName).orElse(""), + context.getTestMethod().map(Method::getName).orElse(""), + Optional.ofNullable(cause).map(Throwable::getMessage).orElse("")); + + langfuse.getAllLogs().forEach((container, logs) -> + LOG.error("=== {} ===\n{}", container, logs)); + } + }; + + /** + * Creates a new {@link LangfuseApi} client configured to connect to the shared container. + * + * @return a configured client instance + */ + static LangfuseApi createClient() { + return LangfuseApi.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .logRequests() + .logResponses() + .prettyPrint() + .build(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiTest.java new file mode 100644 index 0000000..ae98547 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiTest.java @@ -0,0 +1,85 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.annotationQueues.AnnotationQueuesApi; +import com.langfuse.api.model.AnnotationQueue; +import com.langfuse.api.model.CreateAnnotationQueueRequest; +import com.langfuse.api.model.CreateScoreConfigRequest; +import com.langfuse.api.model.ScoreConfigDataType; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; + +/** + * Integration tests for the Annotation Queues API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AnnotationQueuesApiTest extends AbstractLangfuseClientTest { + + private static final String QUEUE_NAME = "test-queue-" + UUID.randomUUID().toString().substring(0, 8); + private static String queueId; + + @Test + @Order(1) + void createQueue() { + var scoreConfig = client.scoreConfigs().scoreConfigsCreate( + ScoreConfigsApi.APIScoreConfigsCreateRequest.newBuilder() + .createScoreConfigRequest(CreateScoreConfigRequest.builder() + .name("queue-cfg-" + UUID.randomUUID().toString().substring(0, 6)) + .dataType(ScoreConfigDataType.NUMERIC) + .minValue(0.0) + .maxValue(1.0) + .build()) + .build()); + + assertThat(client.annotationQueues().annotationQueuesCreateQueue( + AnnotationQueuesApi.APIAnnotationQueuesCreateQueueRequest.newBuilder() + .createAnnotationQueueRequest(CreateAnnotationQueueRequest.builder() + .name(QUEUE_NAME) + .description("Test annotation queue") + .scoreConfigIds(List.of(scoreConfig.getId())) + .build()) + .build())) + .satisfies(queue -> { + assertThat(queue.getId()).isNotBlank(); + assertThat(queue.getName()).isEqualTo(QUEUE_NAME); + assertThat(queue.getDescription()).isEqualTo("Test annotation queue"); + assertThat(queue.getCreatedAt()).isNotNull(); + queueId = queue.getId(); + }); + } + + @Test + @Order(2) + void getQueue() { + assertThat(client.annotationQueues().annotationQueuesGetQueue( + AnnotationQueuesApi.APIAnnotationQueuesGetQueueRequest.newBuilder() + .queueId(queueId) + .build())) + .extracting(AnnotationQueue::getId, AnnotationQueue::getName) + .containsExactly(queueId, QUEUE_NAME); + } + + @Test + @Order(2) + void listQueues() { + assertThat(client.annotationQueues().annotationQueuesListQueues( + AnnotationQueuesApi.APIAnnotationQueuesListQueuesRequest.newBuilder() + .build())) + .satisfies(queues -> { + assertThat(queues.getData()) + .isNotEmpty() + .anyMatch(q -> QUEUE_NAME.equals(q.getName())); + assertThat(queues.getMeta().getTotalItems()).isGreaterThan(0); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncTest.java new file mode 100644 index 0000000..d641097 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncTest.java @@ -0,0 +1,113 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.comments.CommentsApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.Comment; +import com.langfuse.api.model.CreateCommentRequest; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; + +/** + * Async integration tests for the Comments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class CommentsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static String commentId; + + @Test + @Order(1) + void ingestTrace() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("async-comments-test-trace") + .build()) + .build(); + + var response = client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build()); + + assertThat(response.getSuccesses()) + .isNotEmpty(); + } + + @Test + @Order(2) + void createComment() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> { + var projectId = client.projects().projectsGet().getData().get(0).getId(); + + assertThat(client.asyncComments().commentsCreate( + CommentsApi.APICommentsCreateRequest.newBuilder() + .createCommentRequest(CreateCommentRequest.builder() + .projectId(projectId) + .objectType("TRACE") + .objectId(TRACE_ID) + .content("Async test comment") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getId()).isNotBlank(); + commentId = response.getId(); + }); + }); + } + + @Test + @Order(3) + void getCommentById() { + assertThat(client.asyncComments().commentsGetById( + CommentsApi.APICommentsGetByIdRequest.newBuilder() + .commentId(commentId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(Comment::getContent, Comment::getObjectId) + .containsExactly("Async test comment", TRACE_ID); + } + + @Test + @Order(3) + void listComments() { + assertThat(client.asyncComments().commentsGet( + CommentsApi.APICommentsGetRequest.newBuilder() + .objectType("TRACE") + .objectId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(comments -> + assertThat(comments.getData()) + .isNotEmpty() + .anyMatch(c -> commentId.equals(c.getId()))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiTest.java new file mode 100644 index 0000000..1b2a6fd --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiTest.java @@ -0,0 +1,117 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.comments.CommentsApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.Comment; +import com.langfuse.api.model.CommentObjectType; +import com.langfuse.api.model.CreateCommentRequest; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; + +/** + * Integration tests for the Comments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class CommentsApiTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static String commentId; + + @Test + @Order(1) + void ingestTrace() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("comments-test-trace") + .build()) + .build(); + + var response = client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build()); + + assertThat(response.getSuccesses()) + .hasSize(1) + .first() + .satisfies(s -> assertThat(s.getStatus()).isEqualTo(201)); + } + + @Test + @Order(2) + void createComment() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> { + var projectId = client.projects().projectsGet().getData().get(0).getId(); + + var response = client.comments().commentsCreate( + CommentsApi.APICommentsCreateRequest.newBuilder() + .createCommentRequest(CreateCommentRequest.builder() + .projectId(projectId) + .objectType("TRACE") + .objectId(TRACE_ID) + .content("This is a test comment") + .build()) + .build()); + + assertThat(response.getId()) + .isNotBlank(); + + commentId = response.getId(); + }); + } + + @Test + @Order(3) + void getCommentById() { + assertThat(client.comments().commentsGetById( + CommentsApi.APICommentsGetByIdRequest.newBuilder() + .commentId(commentId) + .build())) + .satisfies(comment -> assertThat(comment.getCreatedAt()).isNotNull()) + .extracting(Comment::getId, Comment::getContent, Comment::getObjectType, Comment::getObjectId) + .containsExactly(commentId, "This is a test comment", CommentObjectType.TRACE, TRACE_ID); + } + + @Test + @Order(3) + void listComments() { + var comments = client.comments().commentsGet( + CommentsApi.APICommentsGetRequest.newBuilder() + .objectType("TRACE") + .objectId(TRACE_ID) + .build()); + + assertThat(comments.getData()) + .hasSize(1) + .first() + .extracting(Comment::getId, Comment::getContent, Comment::getObjectType, Comment::getObjectId) + .containsExactly(commentId, "This is a test comment", CommentObjectType.TRACE, TRACE_ID); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncTest.java new file mode 100644 index 0000000..9c1c771 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncTest.java @@ -0,0 +1,80 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; + +/** + * Async integration tests for the Dataset Items API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatasetItemsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String DATASET_NAME = "async-test-dataset-items-" + UUID.randomUUID(); + private static String datasetItemId; + + @Test + @Order(1) + void createDatasetAndItem() { + client.datasets().datasetsCreate( + DatasetsApi.APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + assertThat(client.asyncDatasetItems().datasetItemsCreate( + DatasetItemsApi.APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "What is async?")) + .expectedOutput(Map.of("answer", "Non-blocking execution")) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(item -> { + assertThat(item.getId()).isNotBlank(); + datasetItemId = item.getId(); + }); + } + + @Test + @Order(2) + void getDatasetItem() { + assertThat(client.asyncDatasetItems().datasetItemsGet( + DatasetItemsApi.APIDatasetItemsGetRequest.newBuilder() + .id(datasetItemId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(item -> + assertThat(item.getDatasetName()).isEqualTo(DATASET_NAME)); + } + + @Test + @Order(2) + void listDatasetItems() { + assertThat(client.asyncDatasetItems().datasetItemsList( + DatasetItemsApi.APIDatasetItemsListRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(items -> + assertThat(items.getData()) + .isNotEmpty() + .anyMatch(i -> datasetItemId.equals(i.getId()))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiTest.java new file mode 100644 index 0000000..688207a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiTest.java @@ -0,0 +1,91 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.DatasetItem; +import com.langfuse.api.model.CreateDatasetRequest; + +/** + * Integration tests for the Dataset Items API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatasetItemsApiTest extends AbstractLangfuseClientTest { + + private static final String DATASET_NAME = "test-dataset-items-" + UUID.randomUUID(); + private static String datasetItemId; + + @Test + @Order(1) + void createDatasetAndItem() { + var dataset = client.datasets().datasetsCreate( + DatasetsApi.APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + assertThat(dataset.getName()).isEqualTo(DATASET_NAME); + + assertThat(client.datasetItems().datasetItemsCreate( + DatasetItemsApi.APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "What is Java?")) + .expectedOutput(Map.of("answer", "A programming language")) + .build()) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isNotBlank(); + assertThat(item.getDatasetName()).isEqualTo(DATASET_NAME); + assertThat(item.getCreatedAt()).isNotNull(); + datasetItemId = item.getId(); + }); + } + + @Test + @Order(2) + void getDatasetItem() { + assertThat(client.datasetItems().datasetItemsGet( + DatasetItemsApi.APIDatasetItemsGetRequest.newBuilder() + .id(datasetItemId) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isEqualTo(datasetItemId); + assertThat(item.getDatasetName()).isEqualTo(DATASET_NAME); + assertThat(item.getInput()).isNotNull(); + assertThat(item.getExpectedOutput()).isNotNull(); + assertThat(item.getCreatedAt()).isNotNull(); + }); + } + + @Test + @Order(2) + void listDatasetItems() { + var items = client.datasetItems().datasetItemsList( + DatasetItemsApi.APIDatasetItemsListRequest.newBuilder() + .datasetName(DATASET_NAME) + .build()); + + assertThat(items.getData()) + .hasSize(1) + .first() + .extracting(DatasetItem::getId, DatasetItem::getDatasetName) + .containsExactly(datasetItemId, DATASET_NAME); + + assertThat(items.getMeta().getTotalItems()) + .isEqualTo(1); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiTest.java new file mode 100644 index 0000000..c5ad4c8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiTest.java @@ -0,0 +1,97 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.datasetItems.DatasetItemsApi; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi; +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.CreateDatasetRunItemRequest; +import com.langfuse.api.model.DatasetRunItem; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; + +/** + * Integration tests for the Dataset Run Items API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatasetRunItemsApiTest extends AbstractLangfuseClientTest { + + private static final String DATASET_NAME = "test-run-items-dataset-" + UUID.randomUUID(); + private static final String RUN_NAME = "test-run-" + UUID.randomUUID().toString().substring(0, 8); + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static String datasetItemId; + + @Test + @Order(1) + void setupDatasetAndTrace() { + client.datasets().datasetsCreate( + DatasetsApi.APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + assertThat(client.datasetItems().datasetItemsCreate( + DatasetItemsApi.APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "What is testing?")) + .build()) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isNotBlank(); + datasetItemId = item.getId(); + }); + + client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("run-items-test-trace") + .build()) + .build()))) + .build()) + .build()); + } + + @Test + @Order(2) + void createDatasetRunItem() { + assertThat(client.datasetRunItems().datasetRunItemsCreate( + DatasetRunItemsApi.APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(datasetItemId) + .traceId(TRACE_ID) + .build()) + .build())) + .satisfies(runItem -> { + assertThat(runItem.getId()).isNotBlank(); + assertThat(runItem.getCreatedAt()).isNotNull(); + }) + .extracting(DatasetRunItem::getDatasetRunName, DatasetRunItem::getDatasetItemId, DatasetRunItem::getTraceId) + .containsExactly(RUN_NAME, datasetItemId, TRACE_ID); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncTest.java new file mode 100644 index 0000000..dd13062 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncTest.java @@ -0,0 +1,70 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.Dataset; + +/** + * Async integration tests for the Datasets API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatasetsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String DATASET_NAME = "async-test-dataset-" + UUID.randomUUID(); + private static String datasetId; + + @Test + @Order(1) + void createDataset() { + assertThat(client.asyncDatasets().datasetsCreate( + DatasetsApi.APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .description("Async test dataset") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(dataset -> { + assertThat(dataset.getId()).isNotBlank(); + assertThat(dataset.getName()).isEqualTo(DATASET_NAME); + datasetId = dataset.getId(); + }); + } + + @Test + @Order(2) + void getDatasetByName() { + assertThat(client.asyncDatasets().datasetsGet( + DatasetsApi.APIDatasetsGetRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(Dataset::getId, Dataset::getDescription) + .containsExactly(datasetId, "Async test dataset"); + } + + @Test + @Order(2) + void listDatasetsContainsCreated() { + assertThat(client.asyncDatasets().datasetsList( + DatasetsApi.APIDatasetsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(datasets -> + assertThat(datasets.getData()) + .isNotEmpty() + .anyMatch(d -> DATASET_NAME.equals(d.getName()))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiTest.java new file mode 100644 index 0000000..8a561f8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiTest.java @@ -0,0 +1,72 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.datasets.DatasetsApi; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.Dataset; + +/** + * Integration tests for the Datasets API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatasetsApiTest extends AbstractLangfuseClientTest { + + private static final String DATASET_NAME = "test-dataset-" + UUID.randomUUID(); + private static String datasetId; + + @Test + @Order(1) + void createDataset() { + assertThat(client.datasets().datasetsCreate( + DatasetsApi.APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .description("Test dataset for integration tests") + .build()) + .build())) + .satisfies(dataset -> { + assertThat(dataset.getId()).isNotBlank(); + assertThat(dataset.getName()).isEqualTo(DATASET_NAME); + assertThat(dataset.getDescription()).isEqualTo("Test dataset for integration tests"); + assertThat(dataset.getProjectId()).isNotBlank(); + assertThat(dataset.getCreatedAt()).isNotNull(); + assertThat(dataset.getUpdatedAt()).isNotNull(); + datasetId = dataset.getId(); + }); + } + + @Test + @Order(2) + void getDatasetByName() { + assertThat(client.datasets().datasetsGet( + DatasetsApi.APIDatasetsGetRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .extracting(Dataset::getId, Dataset::getName, Dataset::getDescription) + .containsExactly(datasetId, DATASET_NAME, "Test dataset for integration tests"); + } + + @Test + @Order(2) + void listDatasetsContainsCreated() { + assertThat(client.datasets().datasetsList( + DatasetsApi.APIDatasetsListRequest.newBuilder() + .build())) + .satisfies(datasets -> { + assertThat(datasets.getData()) + .isNotEmpty() + .anyMatch(d -> DATASET_NAME.equals(d.getName())); + assertThat(datasets.getMeta().getTotalItems()).isGreaterThan(0); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncTest.java new file mode 100644 index 0000000..a12dfe3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncTest.java @@ -0,0 +1,29 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + + +/** + * Async integration tests for the Health API. + * + * @author Eric Deandrea + */ +class HealthApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void healthEndpointReturnsOk() { + assertThat(client.asyncHealth().healthHealth()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(health -> { + assertThat(health.getStatus()) + .isEqualTo("OK"); + + assertThat(health.getVersion()) + .isNotBlank(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiTest.java new file mode 100644 index 0000000..72ef02c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiTest.java @@ -0,0 +1,22 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Integration tests for the Health API. + * + * @author Eric Deandrea + */ +class HealthApiTest extends AbstractLangfuseClientTest { + + @Test + void healthEndpointReturnsOk() { + assertThat(client.health().healthHealth()) + .satisfies(health -> { + assertThat(health.getStatus()).isEqualTo("OK"); + assertThat(health.getVersion()).isNotBlank().matches("\\d+\\.\\d+\\.\\d+.*"); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncTest.java new file mode 100644 index 0000000..5c82923 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncTest.java @@ -0,0 +1,93 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; + +/** + * Async integration tests for the Ingestion API. + * + * @author Eric Deandrea + */ +class IngestionApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void ingestSingleTrace() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("async-test-trace") + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .isNotEmpty(); + + assertThat(response.getErrors()) + .isEmpty(); + }); + } + + @Test + void ingestMultipleTraces() { + var events = List.of( + new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("async-batch-trace-1") + .build()) + .build()), + new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("async-batch-trace-2") + .build()) + .build()) + ); + + assertThat(client.asyncIngestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(events) + .build()) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .hasSize(2); + + assertThat(response.getErrors()) + .isEmpty(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiTest.java new file mode 100644 index 0000000..7ca0451 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiTest.java @@ -0,0 +1,95 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionSuccess; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; + +/** + * Integration tests for the Ingestion API. + * + * @author Eric Deandrea + */ +class IngestionApiTest extends AbstractLangfuseClientTest { + + @Test + void ingestSingleTrace() { + var eventId = UUID.randomUUID().toString(); + + var traceEvent = IngestionEventOneOf.builder() + .id(eventId) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("test-trace") + .build()) + .build(); + + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .hasSize(1) + .first() + .extracting(IngestionSuccess::getId, IngestionSuccess::getStatus) + .containsExactly(eventId, 201); + assertThat(response.getErrors()).isEmpty(); + }); + } + + @Test + void ingestMultipleTraces() { + var eventId1 = UUID.randomUUID().toString(); + var eventId2 = UUID.randomUUID().toString(); + + List events = List.of( + new IngestionEvent(IngestionEventOneOf.builder() + .id(eventId1) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("batch-trace-1") + .build()) + .build()), + new IngestionEvent(IngestionEventOneOf.builder() + .id(eventId2) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("batch-trace-2") + .build()) + .build()) + ); + + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(events) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .hasSize(2) + .extracting("id") + .containsExactlyInAnyOrder(eventId1, eventId2); + assertThat(response.getErrors()).isEmpty(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LangfuseApiSpiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LangfuseApiSpiTest.java new file mode 100644 index 0000000..4e21ecc --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LangfuseApiSpiTest.java @@ -0,0 +1,22 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.langfuse.api.LangfuseApi; + +import org.junit.jupiter.api.Test; + +class LangfuseApiSpiTest { + + @Test + void serviceLoaderDiscoversFactory() { + LangfuseApi api = LangfuseApi.builder() + .username("test-pk") + .password("test-sk") + .url("http://localhost:3000") + .build(); + + assertThat(api).isInstanceOf(LangfuseClient.class); + assertThat(api.health()).isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncTest.java new file mode 100644 index 0000000..ccbf555 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncTest.java @@ -0,0 +1,86 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.CreateModelRequest; +import com.langfuse.api.model.Model; +import com.langfuse.api.model.ModelUsageUnit; +import com.langfuse.api.models.ModelsApi; + +/** + * Async integration tests for the Models API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ModelsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String MODEL_NAME = "async-model-" + UUID.randomUUID().toString().substring(0, 8); + private static String modelId; + + @Test + @Order(1) + void createModel() { + assertThat(client.asyncModels().modelsCreate( + ModelsApi.APIModelsCreateRequest.newBuilder() + .createModelRequest(CreateModelRequest.builder() + .modelName(MODEL_NAME) + .matchPattern("(?i)^(%s)(-.+)?$".formatted(MODEL_NAME)) + .unit(ModelUsageUnit.TOKENS) + .inputPrice(0.001) + .outputPrice(0.002) + .tokenizerId("openai") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(model -> { + assertThat(model.getId()).isNotBlank(); + assertThat(model.getModelName()).isEqualTo(MODEL_NAME); + modelId = model.getId(); + }); + } + + @Test + @Order(2) + void getModel() { + assertThat(client.asyncModels().modelsGet( + ModelsApi.APIModelsGetRequest.newBuilder() + .id(modelId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(Model::getModelName, Model::getUnit) + .containsExactly(MODEL_NAME, ModelUsageUnit.TOKENS); + } + + @Test + @Order(2) + void listModels() { + assertThat(client.asyncModels().modelsList( + ModelsApi.APIModelsListRequest.newBuilder() + .limit(100) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(models -> { + assertThat(models.getData()).isNotEmpty(); + assertThat(models.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(3) + void deleteModel() { + assertThat(client.asyncModels().modelsDelete( + ModelsApi.APIModelsDeleteRequest.newBuilder() + .id(modelId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiTest.java new file mode 100644 index 0000000..25fdf12 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiTest.java @@ -0,0 +1,94 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.CreateModelRequest; +import com.langfuse.api.model.Model; +import com.langfuse.api.model.ModelUsageUnit; +import com.langfuse.api.models.ModelsApi; + +/** + * Integration tests for the Models API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ModelsApiTest extends AbstractLangfuseClientTest { + + private static final String MODEL_NAME = "test-model-" + UUID.randomUUID().toString().substring(0, 8); + private static String modelId; + + @Test + @Order(1) + void createModel() { + assertThat(client.models().modelsCreate( + ModelsApi.APIModelsCreateRequest.newBuilder() + .createModelRequest(CreateModelRequest.builder() + .modelName(MODEL_NAME) + .matchPattern("(?i)^(%s)(-.+)?$".formatted(MODEL_NAME)) + .unit(ModelUsageUnit.TOKENS) + .inputPrice(0.001) + .outputPrice(0.002) + .tokenizerId("openai") + .build()) + .build())) + .satisfies(model -> { + assertThat(model.getId()).isNotBlank(); + assertThat(model.getModelName()).isEqualTo(MODEL_NAME); + assertThat(model.getMatchPattern()).isEqualTo("(?i)^(%s)(-.+)?$".formatted(MODEL_NAME)); + assertThat(model.getUnit()).isEqualTo(ModelUsageUnit.TOKENS); + assertThat(model.getTokenizerId()).isEqualTo("openai"); + assertThat(model.getIsLangfuseManaged()).isFalse(); + modelId = model.getId(); + }); + } + + @Test + @Order(2) + void getModel() { + assertThat(client.models().modelsGet( + ModelsApi.APIModelsGetRequest.newBuilder() + .id(modelId) + .build())) + .satisfies(model -> assertThat(model.getCreatedAt()).isNotNull()) + .extracting(Model::getId, Model::getModelName, Model::getUnit) + .containsExactly(modelId, MODEL_NAME, ModelUsageUnit.TOKENS); + } + + @Test + @Order(2) + void listModels() { + assertThat(client.models().modelsList( + ModelsApi.APIModelsListRequest.newBuilder() + .limit(100) + .build())) + .satisfies(models -> { + assertThat(models.getData()).isNotEmpty(); + assertThat(models.getMeta().getTotalItems()).isGreaterThan(0); + assertThat(models.getMeta().getPage()).isEqualTo(1); + }); + } + + @Test + @Order(3) + void deleteModel() { + client.models().modelsDelete( + ModelsApi.APIModelsDeleteRequest.newBuilder() + .id(modelId) + .build()); + + assertThat(client.models().modelsList( + ModelsApi.APIModelsListRequest.newBuilder() + .build())) + .satisfies(models -> + assertThat(models.getData()) + .noneMatch(m -> modelId.equals(m.getId()))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncTest.java new file mode 100644 index 0000000..b2080d0 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncTest.java @@ -0,0 +1,90 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api; +import com.langfuse.api.model.CreateSpanBody; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.IngestionEventOneOf2; +import com.langfuse.api.model.TraceBody; + +/** + * Async integration tests for the Observations API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ObservationsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String SPAN_NAME = "async-observations-test-span-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSpan() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("async-observations-test-trace") + .build()) + .build(); + + var spanEvent = IngestionEventOneOf2.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf2.TypeEnum.SPAN_CREATE) + .body(CreateSpanBody.builder() + .id(UUID.randomUUID().toString()) + .traceId(TRACE_ID) + .name(SPAN_NAME) + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of( + new IngestionEvent(traceEvent), + new IngestionEvent(spanEvent))) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getSuccesses()).hasSize(2)); + } + + @Test + @Order(2) + void listObservationsViaLegacyApi() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncLegacyObservationsV1().legacyObservationsV1GetMany( + LegacyObservationsV1Api.APILegacyObservationsV1GetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(observations -> + assertThat(observations.getData()) + .isNotEmpty() + .anyMatch(o -> SPAN_NAME.equals(o.getName())))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiTest.java new file mode 100644 index 0000000..756f5d5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiTest.java @@ -0,0 +1,101 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api; +import com.langfuse.api.model.CreateSpanBody; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.ObservationsView; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.IngestionEventOneOf2; +import com.langfuse.api.model.TraceBody; + +/** + * Integration tests for the Observations API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ObservationsApiTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String SPAN_ID = UUID.randomUUID().toString(); + private static final String SPAN_NAME = "observations-test-span-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSpan() { + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of( + new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("observations-test-trace") + .build()) + .build()), + new IngestionEvent(IngestionEventOneOf2.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf2.TypeEnum.SPAN_CREATE) + .body(CreateSpanBody.builder() + .id(SPAN_ID) + .traceId(TRACE_ID) + .name(SPAN_NAME) + .build()) + .build()))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()).hasSize(2) + .allSatisfy(s -> assertThat(s.getStatus()).isEqualTo(201)); + assertThat(response.getErrors()).isEmpty(); + }); + } + + @Test + @Order(2) + void listObservationsViaLegacyApi() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.legacyObservationsV1().legacyObservationsV1GetMany( + LegacyObservationsV1Api.APILegacyObservationsV1GetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(observations -> { + assertThat(observations.getData()) + .isNotEmpty() + .anyMatch(o -> SPAN_NAME.equals(o.getName())); + + var span = observations.getData().stream() + .filter(o -> SPAN_NAME.equals(o.getName())) + .findFirst() + .orElseThrow(); + + assertThat(span) + .satisfies(s -> assertThat(s.getStartTime()).isNotNull()) + .extracting(ObservationsView::getTraceId, ObservationsView::getType) + .containsExactly(TRACE_ID, "SPAN"); + })); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncTest.java new file mode 100644 index 0000000..dac3c9b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncTest.java @@ -0,0 +1,23 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +/** + * Async integration tests for the Projects API. + * + * @author Eric Deandrea + */ +class ProjectsApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void getProjects() { + assertThat(client.asyncProjects().projectsGet()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(projects -> + assertThat(projects.getData()).isNotEmpty()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiTest.java new file mode 100644 index 0000000..123b5b3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiTest.java @@ -0,0 +1,28 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Integration tests for the Projects API. + * + * @author Eric Deandrea + */ +class ProjectsApiTest extends AbstractLangfuseClientTest { + + @Test + void getProjects() { + assertThat(client.projects().projectsGet()) + .satisfies(projects -> { + assertThat(projects.getData()) + .isNotEmpty() + .first() + .satisfies(project -> { + assertThat(project.getId()).isNotBlank(); + assertThat(project.getName()).isNotBlank(); + assertThat(project.getOrganization()).isNotNull(); + }); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiTest.java new file mode 100644 index 0000000..f0b0f90 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiTest.java @@ -0,0 +1,70 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.CreatePromptRequest; +import com.langfuse.api.model.CreateTextPromptRequest; +import com.langfuse.api.model.CreateTextPromptType; +import com.langfuse.api.model.PromptVersionUpdateRequest; +import com.langfuse.api.promptVersion.PromptVersionApi; +import com.langfuse.api.prompts.PromptsApi; + +/** + * Integration tests for the Prompt Version API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PromptVersionApiTest extends AbstractLangfuseClientTest { + + private static final String PROMPT_NAME = "test-prompt-version-" + UUID.randomUUID(); + + @Test + @Order(1) + void createPrompt() { + assertThat(client.prompts().promptsCreate( + PromptsApi.APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateTextPromptRequest.builder() + .name(PROMPT_NAME) + .prompt("Version 1: Hello {{name}}") + .type(CreateTextPromptType.TEXT) + .labels(List.of("staging")) + .build())) + .build())) + .isNotNull(); + } + + @Test + @Order(2) + void updatePromptVersion() { + assertThat(client.promptVersion().promptVersionUpdate( + PromptVersionApi.APIPromptVersionUpdateRequest.newBuilder() + .name(PROMPT_NAME) + .version(1) + .promptVersionUpdateRequest(PromptVersionUpdateRequest.builder() + .newLabels(List.of("production")) + .build()) + .build())) + .isNotNull(); + } + + @Test + @Order(3) + void fetchUpdatedPromptVersion() { + assertThat(client.prompts().promptsGet( + PromptsApi.APIPromptsGetRequest.newBuilder() + .promptName(PROMPT_NAME) + .label("production") + .build())) + .isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncTest.java new file mode 100644 index 0000000..772f700 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncTest.java @@ -0,0 +1,109 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.ChatMessage; +import com.langfuse.api.model.ChatMessageWithPlaceholders; +import com.langfuse.api.model.CreateChatPromptRequest; +import com.langfuse.api.model.CreateChatPromptType; +import com.langfuse.api.model.CreatePromptRequest; +import com.langfuse.api.model.CreateTextPromptRequest; +import com.langfuse.api.model.CreateTextPromptType; +import com.langfuse.api.prompts.PromptsApi; + +/** + * Async integration tests for the Prompts API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PromptsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String TEXT_PROMPT_NAME = "async-test-text-prompt-" + UUID.randomUUID(); + private static final String CHAT_PROMPT_NAME = "async-test-chat-prompt-" + UUID.randomUUID(); + + @Test + @Order(1) + void createTextPrompt() { + assertThat(client.asyncPrompts().promptsCreate( + PromptsApi.APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateTextPromptRequest.builder() + .name(TEXT_PROMPT_NAME) + .prompt("Hello {{name}}, welcome to async Langfuse!") + .type(CreateTextPromptType.TEXT) + .labels(List.of("production")) + .build())) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(1) + void createChatPrompt() { + assertThat(client.asyncPrompts().promptsCreate( + PromptsApi.APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateChatPromptRequest.builder() + .name(CHAT_PROMPT_NAME) + .type(CreateChatPromptType.CHAT) + .prompt(List.of(new ChatMessageWithPlaceholders( + ChatMessage.builder() + .role("system") + .content("You are an async assistant.") + .build()))) + .labels(List.of("production")) + .build())) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(2) + void fetchTextPrompt() { + assertThat(client.asyncPrompts().promptsGet( + PromptsApi.APIPromptsGetRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(2) + void fetchChatPrompt() { + assertThat(client.asyncPrompts().promptsGet( + PromptsApi.APIPromptsGetRequest.newBuilder() + .promptName(CHAT_PROMPT_NAME) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(2) + void listPromptsContainsCreatedPrompts() { + assertThat(client.asyncPrompts().promptsList( + PromptsApi.APIPromptsListRequest.newBuilder() + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(prompts -> + assertThat(prompts.getData()).isNotEmpty()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiTest.java new file mode 100644 index 0000000..f9974c9 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiTest.java @@ -0,0 +1,118 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.ChatMessage; +import com.langfuse.api.model.ChatMessageWithPlaceholders; +import com.langfuse.api.model.CreateChatPromptRequest; +import com.langfuse.api.model.CreateChatPromptType; +import com.langfuse.api.model.CreatePromptRequest; +import com.langfuse.api.model.CreateTextPromptRequest; +import com.langfuse.api.model.CreateTextPromptType; +import com.langfuse.api.prompts.PromptsApi; + +/** + * Integration tests for the Prompts API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PromptsApiTest extends AbstractLangfuseClientTest { + + private static final String TEXT_PROMPT_NAME = "test-text-prompt-" + UUID.randomUUID(); + private static final String CHAT_PROMPT_NAME = "test-chat-prompt-" + UUID.randomUUID(); + + @Test + @Order(1) + void createTextPrompt() { + var prompt = client.prompts().promptsCreate( + PromptsApi.APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateTextPromptRequest.builder() + .name(TEXT_PROMPT_NAME) + .prompt("Hello {{name}}, welcome to Langfuse!") + .type(CreateTextPromptType.TEXT) + .labels(List.of("production")) + .build())) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(1) + void createChatPrompt() { + var prompt = client.prompts().promptsCreate( + PromptsApi.APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateChatPromptRequest.builder() + .name(CHAT_PROMPT_NAME) + .type(CreateChatPromptType.CHAT) + .prompt(List.of(new ChatMessageWithPlaceholders( + ChatMessage.builder() + .role("system") + .content("You are a helpful assistant.") + .build()))) + .labels(List.of("production")) + .build())) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(2) + void fetchTextPrompt() { + var prompt = client.prompts().promptsGet( + PromptsApi.APIPromptsGetRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(2) + void fetchChatPrompt() { + var prompt = client.prompts().promptsGet( + PromptsApi.APIPromptsGetRequest.newBuilder() + .promptName(CHAT_PROMPT_NAME) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(2) + void listPromptsContainsCreatedPrompts() { + var prompts = client.prompts().promptsList( + PromptsApi.APIPromptsListRequest.newBuilder() + .build()); + + assertThat(prompts.getData()) + .hasSizeGreaterThanOrEqualTo(2); + + assertThat(prompts.getMeta().getTotalItems()) + .isGreaterThanOrEqualTo(2); + + assertThat(prompts.getData()) + .anyMatch(p -> TEXT_PROMPT_NAME.equals(p.getName())) + .anyMatch(p -> CHAT_PROMPT_NAME.equals(p.getName())); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncTest.java new file mode 100644 index 0000000..513aa3d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncTest.java @@ -0,0 +1,75 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.CreateScoreConfigRequest; +import com.langfuse.api.model.ScoreConfig; +import com.langfuse.api.model.ScoreConfigDataType; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; + +/** + * Async integration tests for the Score Configs API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ScoreConfigsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String CONFIG_NAME = "async-cfg-" + UUID.randomUUID().toString().substring(0, 8); + private static String configId; + + @Test + @Order(1) + void createNumericScoreConfig() { + assertThat(client.asyncScoreConfigs().scoreConfigsCreate( + ScoreConfigsApi.APIScoreConfigsCreateRequest.newBuilder() + .createScoreConfigRequest(CreateScoreConfigRequest.builder() + .name(CONFIG_NAME) + .dataType(ScoreConfigDataType.NUMERIC) + .minValue(0.0) + .maxValue(1.0) + .description("Async numeric score config") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(config -> { + assertThat(config.getId()).isNotBlank(); + assertThat(config.getName()).isEqualTo(CONFIG_NAME); + assertThat(config.getDataType()).isEqualTo(ScoreConfigDataType.NUMERIC); + configId = config.getId(); + }); + } + + @Test + @Order(2) + void getScoreConfigById() { + assertThat(client.asyncScoreConfigs().scoreConfigsGetById( + ScoreConfigsApi.APIScoreConfigsGetByIdRequest.newBuilder() + .configId(configId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(ScoreConfig::getName, ScoreConfig::getMinValue, ScoreConfig::getMaxValue) + .containsExactly(CONFIG_NAME, 0.0, 1.0); + } + + @Test + @Order(2) + void listScoreConfigs() { + assertThat(client.asyncScoreConfigs().scoreConfigsGet( + ScoreConfigsApi.APIScoreConfigsGetRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(configs -> + assertThat(configs.getData()) + .isNotEmpty() + .anyMatch(c -> CONFIG_NAME.equals(c.getName()))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiTest.java new file mode 100644 index 0000000..951397a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiTest.java @@ -0,0 +1,76 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.model.CreateScoreConfigRequest; +import com.langfuse.api.model.ScoreConfig; +import com.langfuse.api.model.ScoreConfigDataType; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi; + +/** + * Integration tests for the Score Configs API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ScoreConfigsApiTest extends AbstractLangfuseClientTest { + + private static final String CONFIG_NAME = "test-config-" + UUID.randomUUID().toString().substring(0, 8); + private static String configId; + + @Test + @Order(1) + void createNumericScoreConfig() { + assertThat(client.scoreConfigs().scoreConfigsCreate( + ScoreConfigsApi.APIScoreConfigsCreateRequest.newBuilder() + .createScoreConfigRequest(CreateScoreConfigRequest.builder() + .name(CONFIG_NAME) + .dataType(ScoreConfigDataType.NUMERIC) + .minValue(0.0) + .maxValue(1.0) + .description("Numeric score for integration test") + .build()) + .build())) + .satisfies(config -> { + assertThat(config.getId()).isNotBlank(); + assertThat(config.getName()).isEqualTo(CONFIG_NAME); + assertThat(config.getDataType()).isEqualTo(ScoreConfigDataType.NUMERIC); + assertThat(config.getDescription()).isEqualTo("Numeric score for integration test"); + assertThat(config.getIsArchived()).isFalse(); + assertThat(config.getCreatedAt()).isNotNull(); + configId = config.getId(); + }); + } + + @Test + @Order(2) + void getScoreConfigById() { + assertThat(client.scoreConfigs().scoreConfigsGetById( + ScoreConfigsApi.APIScoreConfigsGetByIdRequest.newBuilder() + .configId(configId) + .build())) + .extracting(ScoreConfig::getId, ScoreConfig::getName, ScoreConfig::getMinValue, ScoreConfig::getMaxValue, ScoreConfig::getDataType) + .containsExactly(configId, CONFIG_NAME, 0.0, 1.0, ScoreConfigDataType.NUMERIC); + } + + @Test + @Order(2) + void listScoreConfigs() { + assertThat(client.scoreConfigs().scoreConfigsGet( + ScoreConfigsApi.APIScoreConfigsGetRequest.newBuilder() + .build())) + .satisfies(configs -> { + assertThat(configs.getData()) + .isNotEmpty() + .anyMatch(c -> CONFIG_NAME.equals(c.getName())); + assertThat(configs.getMeta().getTotalItems()).isGreaterThan(0); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncTest.java new file mode 100644 index 0000000..b7dc75c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncTest.java @@ -0,0 +1,97 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.LegacyCreateScoreRequest; +import com.langfuse.api.model.LegacyCreateScoreSource; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.scores.ScoresApi; + +/** + * Async integration tests for the Scores API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ScoresApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String SCORE_NAME = "async-test-score"; + + @Test + @Order(1) + void ingestTrace() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("async-score-test-trace") + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getSuccesses()).isNotEmpty()); + } + + @Test + @Order(1) + void createScore() { + assertThat(client.asyncLegacyScoreV1().legacyScoreV1Create( + LegacyScoreV1Api.APILegacyScoreV1CreateRequest.newBuilder() + .legacyCreateScoreRequest(LegacyCreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.85)) + .dataType(ScoreDataType.NUMERIC) + .source(LegacyCreateScoreSource.API) + .environment("default") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(2) + void listScoresForTrace() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(e -> e instanceof LangfuseApiException) + .untilAsserted(() -> + assertThat(client.asyncScores().scoresGetMany( + ScoresApi.APIScoresGetManyRequest.newBuilder() + .name(SCORE_NAME) + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(scores -> + assertThat(scores.getData()).isNotEmpty())); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiTest.java new file mode 100644 index 0000000..c5a0a18 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiTest.java @@ -0,0 +1,98 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.LegacyCreateScoreRequest; +import com.langfuse.api.model.LegacyCreateScoreSource; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.scores.ScoresApi; + +/** + * Integration tests for the Scores API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ScoresApiTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String SCORE_NAME = "test-score"; + + @Test + @Order(1) + void ingestTrace() { + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name("score-test-trace") + .build()) + .build()))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()).hasSize(1); + assertThat(response.getErrors()).isEmpty(); + }); + } + + @Test + @Order(1) + void createScore() { + assertThat(client.legacyScoreV1().legacyScoreV1Create( + LegacyScoreV1Api.APILegacyScoreV1CreateRequest.newBuilder() + .legacyCreateScoreRequest(LegacyCreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.95)) + .dataType(ScoreDataType.NUMERIC) + .source(LegacyCreateScoreSource.API) + .environment("default") + .build()) + .build())) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(2) + void listScoresForTrace() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.scores().scoresGetMany( + ScoresApi.APIScoresGetManyRequest.newBuilder() + .name(SCORE_NAME) + .traceId(TRACE_ID) + .build())) + .satisfies(scores -> { + assertThat(scores.getData()).isNotEmpty(); + assertThat(scores.getMeta().getTotalItems()).isGreaterThan(0); + })); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncTest.java new file mode 100644 index 0000000..395e064 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncTest.java @@ -0,0 +1,90 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.sessions.SessionsApi; + +/** + * Async integration tests for the Sessions API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SessionsApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String SESSION_ID = "async-test-session-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSession() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("async-sessions-test-trace") + .sessionId(SESSION_ID) + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getSuccesses()).isNotEmpty()); + } + + @Test + @Order(2) + void getSession() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncSessions().sessionsGet( + SessionsApi.APISessionsGetRequest.newBuilder() + .sessionId(SESSION_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(session -> + assertThat(session.getId()).isEqualTo(SESSION_ID))); + } + + @Test + @Order(2) + void listSessions() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncSessions().sessionsList( + SessionsApi.APISessionsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(sessions -> + assertThat(sessions.getData()) + .isNotEmpty() + .anyMatch(s -> SESSION_ID.equals(s.getId())))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiTest.java new file mode 100644 index 0000000..71e5b62 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiTest.java @@ -0,0 +1,93 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.sessions.SessionsApi; + +/** + * Integration tests for the Sessions API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SessionsApiTest extends AbstractLangfuseClientTest { + + private static final String SESSION_ID = "test-session-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSession() { + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(UUID.randomUUID().toString()) + .name("sessions-test-trace") + .sessionId(SESSION_ID) + .build()) + .build()))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()).hasSize(1); + assertThat(response.getErrors()).isEmpty(); + }); + } + + @Test + @Order(2) + void getSession() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.sessions().sessionsGet( + SessionsApi.APISessionsGetRequest.newBuilder() + .sessionId(SESSION_ID) + .build())) + .satisfies(session -> { + assertThat(session.getId()).isEqualTo(SESSION_ID); + assertThat(session.getCreatedAt()).isNotNull(); + assertThat(session.getTraces()).isNotEmpty(); + })); + } + + @Test + @Order(2) + void listSessions() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.sessions().sessionsList( + SessionsApi.APISessionsListRequest.newBuilder() + .build())) + .satisfies(sessions -> { + assertThat(sessions.getData()) + .isNotEmpty() + .anyMatch(s -> SESSION_ID.equals(s.getId())); + assertThat(sessions.getMeta().getTotalItems()).isGreaterThan(0); + })); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncTest.java new file mode 100644 index 0000000..64910e1 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncTest.java @@ -0,0 +1,96 @@ +package com.langfuse.client; + +import static com.langfuse.api.ingestion.IngestionApi.APIIngestionBatchRequest; +import static com.langfuse.api.trace.TraceApi.APITraceGetRequest; +import static com.langfuse.api.trace.TraceApi.APITraceListRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.model.TraceWithFullDetails; + +/** + * Async integration tests for the Trace API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TraceApiAsyncTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String TRACE_NAME = "async-trace-api-test-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTrace() { + var traceEvent = IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name(TRACE_NAME) + .userId("async-test-user") + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getSuccesses()).isNotEmpty()); + } + + @Test + @Order(2) + void getTraceById() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(e -> e instanceof LangfuseApiException) + .untilAsserted(() -> + assertThat(client.asyncTrace().traceGet( + APITraceGetRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(TraceWithFullDetails::getId, TraceWithFullDetails::getName) + .containsExactly(TRACE_ID, TRACE_NAME) + ); + } + + @Test + @Order(2) + void listTracesContainsIngestedTrace() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(e -> e instanceof LangfuseApiException) + .untilAsserted(() -> + assertThat(client.asyncTrace().traceList( + APITraceListRequest.newBuilder() + .name(TRACE_NAME) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(traces -> + assertThat(traces.getData()) + .isNotEmpty() + .anyMatch(t -> TRACE_ID.equals(t.getId())))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiTest.java new file mode 100644 index 0000000..de44ba8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiTest.java @@ -0,0 +1,94 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.ingestion.IngestionApi; +import com.langfuse.api.model.IngestionBatchRequest; +import com.langfuse.api.model.IngestionEvent; +import com.langfuse.api.model.IngestionEventOneOf; +import com.langfuse.api.model.TraceBody; +import com.langfuse.api.model.TraceWithFullDetails; +import com.langfuse.api.trace.TraceApi; + +/** + * Integration tests for the Trace API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TraceApiTest extends AbstractLangfuseClientTest { + + private static final String TRACE_ID = UUID.randomUUID().toString(); + private static final String TRACE_NAME = "trace-api-test-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTrace() { + assertThat(client.ingestion().ingestionBatch( + IngestionApi.APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(IngestionEventOneOf.builder() + .id(UUID.randomUUID().toString()) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf.TypeEnum.TRACE_CREATE) + .body(TraceBody.builder() + .id(TRACE_ID) + .name(TRACE_NAME) + .userId("test-user") + .build()) + .build()))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()).hasSize(1); + assertThat(response.getErrors()).isEmpty(); + }); + } + + @Test + @Order(2) + void getTraceById() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.trace().traceGet( + TraceApi.APITraceGetRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(trace -> assertThat(trace.getTimestamp()).isNotNull()) + .extracting(TraceWithFullDetails::getId, TraceWithFullDetails::getName, TraceWithFullDetails::getUserId) + .containsExactly(TRACE_ID, TRACE_NAME, "test-user")); + } + + @Test + @Order(2) + void listTracesContainsIngestedTrace() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.trace().traceList( + TraceApi.APITraceListRequest.newBuilder() + .name(TRACE_NAME) + .build())) + .satisfies(traces -> { + assertThat(traces.getData()) + .isNotEmpty() + .anyMatch(t -> TRACE_ID.equals(t.getId())); + assertThat(traces.getMeta().getTotalItems()).isGreaterThan(0); + })); + } +} diff --git a/langfuse-java-legacy/.mvn/wrapper/maven-wrapper.properties b/langfuse-java-legacy/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/langfuse-java-legacy/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/langfuse-java-legacy/README.md b/langfuse-java-legacy/README.md new file mode 100644 index 0000000..8e8123a --- /dev/null +++ b/langfuse-java-legacy/README.md @@ -0,0 +1,120 @@ +# langfuse-java + +This repository contains an auto-generated Langfuse API client for Java based on our [API specification](https://github.com/langfuse/langfuse/tree/main/fern/apis/server). +See the [Langfuse API reference](https://api.reference.langfuse.com) for more details on the available endpoints. + +**Note:** We recommend to solve tracing via the [OpenTelemetry Instrumentation](https://langfuse.com/docs/opentelemetry/get-started) instead of using the Ingestion API directly. You can use the [OpenTelemetry Java SDK](https://github.com/open-telemetry/opentelemetry-java) and export spans to the [Langfuse OTel endpoint](https://langfuse.com/integrations/native/opentelemetry). +This allows for a more detailed and standardized tracing experience without the need to handle batching and updates internally. +Check out our [Spring AI Example](https://langfuse.com/docs/integrations/spring-ai) for more details. + +## Installation + +The recommended way to install the langfuse-java API client is via Maven Central: + +```xml + + com.langfuse + langfuse-java + 0.2.0 + +``` + +## Usage + +Instantiate the Langfuse Client with the respective endpoint and your API Keys. + +```java +import com.langfuse.client.LangfuseClient; + +LangfuseClient client = LangfuseClient.builder() + .url("https://cloud.langfuse.com") // πŸ‡ͺπŸ‡Ί EU data region + // .url("https://us.cloud.langfuse.com") // πŸ‡ΊπŸ‡Έ US data region + // .url("http://localhost:3000") // 🏠 Local deployment + .credentials("pk-lf-...", "sk-lf-...") + .build(); +``` + +An async client is also available via `AsyncLangfuseClient.builder()` with the same configuration options. + +Make requests using the clients: + +```java +import com.langfuse.client.core.LangfuseClientApiException; +import com.langfuse.client.resources.prompts.types.PromptMetaListResponse; + +try { + PromptMetaListResponse prompts = client.prompts().list(); +} catch (LangfuseClientApiException error) { + System.out.println(error.body()); + System.out.println(error.statusCode()); +} +``` + +## Testing + +### Unit tests + +Unit tests (deserialization, query string mapping) run without any credentials: + +```bash +mvn test +``` + +### Integration tests + +Integration tests connect to a real Langfuse project. They require credentials and are excluded from `mvn test`. + +1. Copy `.env.example` to `.env` and fill in your API keys: + ```bash + cp .env.example .env + ``` + +2. Ensure your Langfuse project contains the following prompts: + - `test-chat-prompt` β€” chat type, at least one message with `role` and `content` + - `test-text-prompt` β€” text type, non-empty prompt text + +3. Run all tests (unit + integration): + ```bash + mvn verify + ``` + + Or run only integration tests: + ```bash + mvn failsafe:integration-test + ``` + +Integration tests skip gracefully when credentials are absent. + +## Drafting a Release + +Run `./mvnw release:prepare -DreleaseVersion=` with the version you want to create. +Push the changes including the tag. + +## Publishing to Maven Central + +This project is configured to publish to Maven Central. +To publish to Maven Central, you need to configure the following secrets in your GitHub repository: + +- `OSSRH_USERNAME`: Your Sonatype OSSRH username +- `OSSRH_PASSWORD`: Your Sonatype OSSRH password +- `GPG_PRIVATE_KEY`: Your GPG private key for signing artifacts +- `GPG_PASSPHRASE`: The passphrase for your GPG private key + +## Updating + +1. Ensure that langfuse-java is placed in the same directory as the main [langfuse](https://github.com/langfuse/langfuse) repository. +2. Setup a new Java fern generator using + ```yaml + - name: fernapi/fern-java-sdk + version: 3.38.1 + output: + location: local-file-system + path: ../../../../langfuse-java/src/main/java/com/langfuse/client/ + config: + client-class-name: LangfuseClient + ``` +3. Generate the new client code using `npx fern-api generate --api server`. +4. Manually set the `package` across all files to `com.langfuse.client`. +5. Verify that `LangfuseClientBuilder.setAuthentication()` uses `Basic` auth (not `Bearer`). +6. Adjust Javadoc strings with HTML properties as the apidocs package does not support them. +7. Commit the changes in langfuse-java and push them to the repository. diff --git a/langfuse-java-legacy/mvnw b/langfuse-java-legacy/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/langfuse-java-legacy/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/langfuse-java-legacy/mvnw.cmd b/langfuse-java-legacy/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/langfuse-java-legacy/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/langfuse-java-legacy/pom.xml b/langfuse-java-legacy/pom.xml new file mode 100644 index 0000000..71f5bfb --- /dev/null +++ b/langfuse-java-legacy/pom.xml @@ -0,0 +1,78 @@ + + 4.0.0 + + + com.langfuse + langfuse-java-parent + 0.2.1-SNAPSHOT + + + langfuse-java + jar + + langfuse-java + Java client for the Langfuse API (legacy fern-generated SDK) + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.squareup.okhttp3 + okhttp + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + io.github.cdimascio + dotenv-java + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + diff --git a/src/main/java/com/langfuse/client/.fern/metadata.json b/langfuse-java-legacy/src/main/java/com/langfuse/client/.fern/metadata.json similarity index 100% rename from src/main/java/com/langfuse/client/.fern/metadata.json rename to langfuse-java-legacy/src/main/java/com/langfuse/client/.fern/metadata.json diff --git a/src/main/java/com/langfuse/client/AsyncLangfuseClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/AsyncLangfuseClient.java similarity index 100% rename from src/main/java/com/langfuse/client/AsyncLangfuseClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/AsyncLangfuseClient.java diff --git a/src/main/java/com/langfuse/client/AsyncLangfuseClientBuilder.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/AsyncLangfuseClientBuilder.java similarity index 100% rename from src/main/java/com/langfuse/client/AsyncLangfuseClientBuilder.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/AsyncLangfuseClientBuilder.java diff --git a/src/main/java/com/langfuse/client/LangfuseClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/LangfuseClient.java similarity index 100% rename from src/main/java/com/langfuse/client/LangfuseClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/LangfuseClient.java diff --git a/src/main/java/com/langfuse/client/LangfuseClientBuilder.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/LangfuseClientBuilder.java similarity index 100% rename from src/main/java/com/langfuse/client/LangfuseClientBuilder.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/LangfuseClientBuilder.java diff --git a/src/main/java/com/langfuse/client/core/ClientOptions.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/ClientOptions.java similarity index 100% rename from src/main/java/com/langfuse/client/core/ClientOptions.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/ClientOptions.java diff --git a/src/main/java/com/langfuse/client/core/DateTimeDeserializer.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/DateTimeDeserializer.java similarity index 100% rename from src/main/java/com/langfuse/client/core/DateTimeDeserializer.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/DateTimeDeserializer.java diff --git a/src/main/java/com/langfuse/client/core/Environment.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/Environment.java similarity index 100% rename from src/main/java/com/langfuse/client/core/Environment.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/Environment.java diff --git a/src/main/java/com/langfuse/client/core/FileStream.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/FileStream.java similarity index 100% rename from src/main/java/com/langfuse/client/core/FileStream.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/FileStream.java diff --git a/src/main/java/com/langfuse/client/core/InputStreamRequestBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/InputStreamRequestBody.java similarity index 100% rename from src/main/java/com/langfuse/client/core/InputStreamRequestBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/InputStreamRequestBody.java diff --git a/src/main/java/com/langfuse/client/core/LangfuseClientApiException.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientApiException.java similarity index 100% rename from src/main/java/com/langfuse/client/core/LangfuseClientApiException.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientApiException.java diff --git a/src/main/java/com/langfuse/client/core/LangfuseClientException.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientException.java similarity index 100% rename from src/main/java/com/langfuse/client/core/LangfuseClientException.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientException.java diff --git a/src/main/java/com/langfuse/client/core/LangfuseClientHttpResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientHttpResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/core/LangfuseClientHttpResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/LangfuseClientHttpResponse.java diff --git a/src/main/java/com/langfuse/client/core/MediaTypes.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/MediaTypes.java similarity index 100% rename from src/main/java/com/langfuse/client/core/MediaTypes.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/MediaTypes.java diff --git a/src/main/java/com/langfuse/client/core/Nullable.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/Nullable.java similarity index 100% rename from src/main/java/com/langfuse/client/core/Nullable.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/Nullable.java diff --git a/src/main/java/com/langfuse/client/core/NullableNonemptyFilter.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/NullableNonemptyFilter.java similarity index 100% rename from src/main/java/com/langfuse/client/core/NullableNonemptyFilter.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/NullableNonemptyFilter.java diff --git a/src/main/java/com/langfuse/client/core/ObjectMappers.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/ObjectMappers.java similarity index 100% rename from src/main/java/com/langfuse/client/core/ObjectMappers.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/ObjectMappers.java diff --git a/src/main/java/com/langfuse/client/core/QueryStringMapper.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/QueryStringMapper.java similarity index 100% rename from src/main/java/com/langfuse/client/core/QueryStringMapper.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/QueryStringMapper.java diff --git a/src/main/java/com/langfuse/client/core/RequestOptions.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/RequestOptions.java similarity index 100% rename from src/main/java/com/langfuse/client/core/RequestOptions.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/RequestOptions.java diff --git a/src/main/java/com/langfuse/client/core/ResponseBodyInputStream.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/ResponseBodyInputStream.java similarity index 100% rename from src/main/java/com/langfuse/client/core/ResponseBodyInputStream.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/ResponseBodyInputStream.java diff --git a/src/main/java/com/langfuse/client/core/ResponseBodyReader.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/ResponseBodyReader.java similarity index 100% rename from src/main/java/com/langfuse/client/core/ResponseBodyReader.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/ResponseBodyReader.java diff --git a/src/main/java/com/langfuse/client/core/RetryInterceptor.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/RetryInterceptor.java similarity index 100% rename from src/main/java/com/langfuse/client/core/RetryInterceptor.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/RetryInterceptor.java diff --git a/src/main/java/com/langfuse/client/core/SseEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/SseEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/core/SseEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/SseEvent.java diff --git a/src/main/java/com/langfuse/client/core/SseEventParser.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/SseEventParser.java similarity index 100% rename from src/main/java/com/langfuse/client/core/SseEventParser.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/SseEventParser.java diff --git a/src/main/java/com/langfuse/client/core/Stream.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/Stream.java similarity index 100% rename from src/main/java/com/langfuse/client/core/Stream.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/Stream.java diff --git a/src/main/java/com/langfuse/client/core/Suppliers.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/core/Suppliers.java similarity index 100% rename from src/main/java/com/langfuse/client/core/Suppliers.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/core/Suppliers.java diff --git a/src/main/java/com/langfuse/client/prompt/PromptCompiler.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/prompt/PromptCompiler.java similarity index 100% rename from src/main/java/com/langfuse/client/prompt/PromptCompiler.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/prompt/PromptCompiler.java diff --git a/src/main/java/com/langfuse/client/prompt/PromptTemplate.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/prompt/PromptTemplate.java similarity index 100% rename from src/main/java/com/langfuse/client/prompt/PromptTemplate.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/prompt/PromptTemplate.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/AnnotationQueuesClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AnnotationQueuesClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/AnnotationQueuesClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AnnotationQueuesClient.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncAnnotationQueuesClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncAnnotationQueuesClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/AsyncAnnotationQueuesClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncAnnotationQueuesClient.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncRawAnnotationQueuesClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncRawAnnotationQueuesClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/AsyncRawAnnotationQueuesClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/AsyncRawAnnotationQueuesClient.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/RawAnnotationQueuesClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/RawAnnotationQueuesClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/RawAnnotationQueuesClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/RawAnnotationQueuesClient.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueueItemsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueueItemsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueueItemsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueueItemsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueuesRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueuesRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueuesRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/requests/GetAnnotationQueuesRequest.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueue.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueue.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueue.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueue.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueAssignmentRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueAssignmentRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueAssignmentRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueAssignmentRequest.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueItem.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueItem.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueItem.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueItem.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueObjectType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueObjectType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueObjectType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueObjectType.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueStatus.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueStatus.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueStatus.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/AnnotationQueueStatus.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueAssignmentResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueAssignmentResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueAssignmentResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueAssignmentResponse.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueItemRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueItemRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueItemRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueItemRequest.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/CreateAnnotationQueueRequest.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueAssignmentResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueAssignmentResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueAssignmentResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueAssignmentResponse.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueItemResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueItemResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueItemResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/DeleteAnnotationQueueItemResponse.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueueItems.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueueItems.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueueItems.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueueItems.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueues.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueues.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueues.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/PaginatedAnnotationQueues.java diff --git a/src/main/java/com/langfuse/client/resources/annotationqueues/types/UpdateAnnotationQueueItemRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/UpdateAnnotationQueueItemRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/annotationqueues/types/UpdateAnnotationQueueItemRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/annotationqueues/types/UpdateAnnotationQueueItemRequest.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncBlobStorageIntegrationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncBlobStorageIntegrationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncBlobStorageIntegrationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncBlobStorageIntegrationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncRawBlobStorageIntegrationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncRawBlobStorageIntegrationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncRawBlobStorageIntegrationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/AsyncRawBlobStorageIntegrationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/BlobStorageIntegrationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/BlobStorageIntegrationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/BlobStorageIntegrationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/BlobStorageIntegrationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/RawBlobStorageIntegrationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/RawBlobStorageIntegrationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/RawBlobStorageIntegrationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/RawBlobStorageIntegrationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportFrequency.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportFrequency.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportFrequency.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportFrequency.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportMode.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportMode.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportMode.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageExportMode.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationDeletionResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationDeletionResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationDeletionResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationDeletionResponse.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationFileType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationFileType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationFileType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationFileType.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationResponse.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationType.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationsResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationsResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationsResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/BlobStorageIntegrationsResponse.java diff --git a/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/CreateBlobStorageIntegrationRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/CreateBlobStorageIntegrationRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/CreateBlobStorageIntegrationRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/blobstorageintegrations/types/CreateBlobStorageIntegrationRequest.java diff --git a/src/main/java/com/langfuse/client/resources/comments/AsyncCommentsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/AsyncCommentsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/AsyncCommentsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/AsyncCommentsClient.java diff --git a/src/main/java/com/langfuse/client/resources/comments/AsyncRawCommentsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/AsyncRawCommentsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/AsyncRawCommentsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/AsyncRawCommentsClient.java diff --git a/src/main/java/com/langfuse/client/resources/comments/CommentsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/CommentsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/CommentsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/CommentsClient.java diff --git a/src/main/java/com/langfuse/client/resources/comments/RawCommentsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/RawCommentsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/RawCommentsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/RawCommentsClient.java diff --git a/src/main/java/com/langfuse/client/resources/comments/requests/GetCommentsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/requests/GetCommentsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/requests/GetCommentsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/requests/GetCommentsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/types/CreateCommentRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentRequest.java diff --git a/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/types/CreateCommentResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/CreateCommentResponse.java diff --git a/src/main/java/com/langfuse/client/resources/comments/types/GetCommentsResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/GetCommentsResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/comments/types/GetCommentsResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/comments/types/GetCommentsResponse.java diff --git a/src/main/java/com/langfuse/client/resources/commons/errors/AccessDeniedError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/AccessDeniedError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/errors/AccessDeniedError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/AccessDeniedError.java diff --git a/src/main/java/com/langfuse/client/resources/commons/errors/Error.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/Error.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/errors/Error.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/Error.java diff --git a/src/main/java/com/langfuse/client/resources/commons/errors/MethodNotAllowedError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/MethodNotAllowedError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/errors/MethodNotAllowedError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/MethodNotAllowedError.java diff --git a/src/main/java/com/langfuse/client/resources/commons/errors/NotFoundError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/NotFoundError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/errors/NotFoundError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/NotFoundError.java diff --git a/src/main/java/com/langfuse/client/resources/commons/errors/UnauthorizedError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/UnauthorizedError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/errors/UnauthorizedError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/errors/UnauthorizedError.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/BaseScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BaseScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/BaseScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BaseScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/BaseScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BaseScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/BaseScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BaseScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/BooleanScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BooleanScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/BooleanScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BooleanScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/BooleanScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BooleanScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/BooleanScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/BooleanScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/CategoricalScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/CategoricalScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CategoricalScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Comment.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Comment.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Comment.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Comment.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/CommentObjectType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CommentObjectType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/CommentObjectType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CommentObjectType.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ConfigCategory.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ConfigCategory.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ConfigCategory.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ConfigCategory.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/CorrectionScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CorrectionScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/CorrectionScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CorrectionScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/CreateScoreValue.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CreateScoreValue.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/CreateScoreValue.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/CreateScoreValue.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Dataset.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Dataset.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Dataset.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Dataset.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/DatasetItem.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetItem.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/DatasetItem.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetItem.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/DatasetRun.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRun.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/DatasetRun.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRun.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunItem.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunItem.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/DatasetRunItem.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunItem.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunWithItems.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunWithItems.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/DatasetRunWithItems.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetRunWithItems.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/DatasetStatus.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetStatus.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/DatasetStatus.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/DatasetStatus.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/IBaseScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBaseScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/IBaseScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBaseScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/IBaseScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBaseScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/IBaseScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBaseScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/IBooleanScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBooleanScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/IBooleanScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IBooleanScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ICategoricalScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ICategoricalScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ICategoricalScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ICategoricalScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ICorrectionScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ICorrectionScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ICorrectionScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ICorrectionScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/IDatasetRun.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IDatasetRun.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/IDatasetRun.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IDatasetRun.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/INumericScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/INumericScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/INumericScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/INumericScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/IObservation.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IObservation.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/IObservation.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/IObservation.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ISession.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ISession.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ISession.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ISession.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ITrace.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ITrace.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ITrace.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ITrace.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/MapValue.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/MapValue.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/MapValue.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/MapValue.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Model.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Model.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Model.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Model.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ModelPrice.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ModelPrice.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ModelPrice.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ModelPrice.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ModelUsageUnit.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ModelUsageUnit.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ModelUsageUnit.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ModelUsageUnit.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/NumericScore.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/NumericScore.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/NumericScore.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/NumericScore.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/NumericScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/NumericScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/NumericScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/NumericScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Observation.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Observation.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Observation.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Observation.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ObservationLevel.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ObservationLevel.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ObservationLevel.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ObservationLevel.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ObservationsView.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ObservationsView.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ObservationsView.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ObservationsView.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/PricingTier.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTier.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/PricingTier.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTier.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/PricingTierCondition.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierCondition.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/PricingTierCondition.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierCondition.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/PricingTierInput.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierInput.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/PricingTierInput.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierInput.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/PricingTierOperator.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierOperator.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/PricingTierOperator.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/PricingTierOperator.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Score.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Score.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Score.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Score.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfig.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfig.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ScoreConfig.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfig.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfigDataType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfigDataType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ScoreConfigDataType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreConfigDataType.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ScoreDataType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreDataType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ScoreDataType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreDataType.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ScoreSource.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreSource.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ScoreSource.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreSource.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/ScoreV1.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreV1.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/ScoreV1.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/ScoreV1.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Session.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Session.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Session.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Session.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/SessionWithTraces.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/SessionWithTraces.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/SessionWithTraces.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/SessionWithTraces.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Trace.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Trace.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Trace.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Trace.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/TraceWithDetails.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/TraceWithDetails.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/TraceWithDetails.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/TraceWithDetails.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/TraceWithFullDetails.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/TraceWithFullDetails.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/TraceWithFullDetails.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/TraceWithFullDetails.java diff --git a/src/main/java/com/langfuse/client/resources/commons/types/Usage.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Usage.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/commons/types/Usage.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/commons/types/Usage.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/AsyncDatasetItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/AsyncDatasetItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/AsyncDatasetItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/AsyncDatasetItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/AsyncRawDatasetItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/AsyncRawDatasetItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/AsyncRawDatasetItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/AsyncRawDatasetItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/DatasetItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/DatasetItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/DatasetItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/DatasetItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/RawDatasetItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/RawDatasetItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/RawDatasetItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/RawDatasetItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/requests/GetDatasetItemsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/requests/GetDatasetItemsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/requests/GetDatasetItemsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/requests/GetDatasetItemsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/types/CreateDatasetItemRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/CreateDatasetItemRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/types/CreateDatasetItemRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/CreateDatasetItemRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/types/DeleteDatasetItemResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/DeleteDatasetItemResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/types/DeleteDatasetItemResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/DeleteDatasetItemResponse.java diff --git a/src/main/java/com/langfuse/client/resources/datasetitems/types/PaginatedDatasetItems.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/PaginatedDatasetItems.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetitems/types/PaginatedDatasetItems.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetitems/types/PaginatedDatasetItems.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncDatasetRunItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncDatasetRunItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncDatasetRunItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncDatasetRunItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncRawDatasetRunItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncRawDatasetRunItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncRawDatasetRunItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/AsyncRawDatasetRunItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/DatasetRunItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/DatasetRunItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/DatasetRunItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/DatasetRunItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/RawDatasetRunItemsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/RawDatasetRunItemsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/RawDatasetRunItemsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/RawDatasetRunItemsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/requests/ListDatasetRunItemsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/requests/ListDatasetRunItemsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/requests/ListDatasetRunItemsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/requests/ListDatasetRunItemsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/types/CreateDatasetRunItemRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/types/CreateDatasetRunItemRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/types/CreateDatasetRunItemRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/types/CreateDatasetRunItemRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasetrunitems/types/PaginatedDatasetRunItems.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/types/PaginatedDatasetRunItems.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasetrunitems/types/PaginatedDatasetRunItems.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasetrunitems/types/PaginatedDatasetRunItems.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/AsyncDatasetsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/AsyncDatasetsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/AsyncDatasetsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/AsyncDatasetsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/AsyncRawDatasetsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/AsyncRawDatasetsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/AsyncRawDatasetsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/AsyncRawDatasetsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/DatasetsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/DatasetsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/DatasetsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/DatasetsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/RawDatasetsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/RawDatasetsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/RawDatasetsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/RawDatasetsClient.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetRunsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetRunsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetRunsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetRunsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/requests/GetDatasetsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/types/CreateDatasetRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/CreateDatasetRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/types/CreateDatasetRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/CreateDatasetRequest.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/types/DeleteDatasetRunResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/DeleteDatasetRunResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/types/DeleteDatasetRunResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/DeleteDatasetRunResponse.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasetRuns.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasetRuns.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasetRuns.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasetRuns.java diff --git a/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasets.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasets.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasets.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/datasets/types/PaginatedDatasets.java diff --git a/src/main/java/com/langfuse/client/resources/health/AsyncHealthClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/AsyncHealthClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/AsyncHealthClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/AsyncHealthClient.java diff --git a/src/main/java/com/langfuse/client/resources/health/AsyncRawHealthClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/AsyncRawHealthClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/AsyncRawHealthClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/AsyncRawHealthClient.java diff --git a/src/main/java/com/langfuse/client/resources/health/HealthClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/HealthClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/HealthClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/HealthClient.java diff --git a/src/main/java/com/langfuse/client/resources/health/RawHealthClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/RawHealthClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/RawHealthClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/RawHealthClient.java diff --git a/src/main/java/com/langfuse/client/resources/health/errors/ServiceUnavailableError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/errors/ServiceUnavailableError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/errors/ServiceUnavailableError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/errors/ServiceUnavailableError.java diff --git a/src/main/java/com/langfuse/client/resources/health/types/HealthResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/types/HealthResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/health/types/HealthResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/health/types/HealthResponse.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/AsyncIngestionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/AsyncIngestionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/AsyncIngestionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/AsyncIngestionClient.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/AsyncRawIngestionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/AsyncRawIngestionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/AsyncRawIngestionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/AsyncRawIngestionClient.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/IngestionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/IngestionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/IngestionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/IngestionClient.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/RawIngestionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/RawIngestionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/RawIngestionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/RawIngestionClient.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/requests/IngestionRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/requests/IngestionRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/requests/IngestionRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/requests/IngestionRequest.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/BaseEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/BaseEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/BaseEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/BaseEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateEventEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateGenerationEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateObservationEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateObservationEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateObservationEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateObservationEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/CreateSpanEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IBaseEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IBaseEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IBaseEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IBaseEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateEventBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateEventBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ICreateEventBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateEventBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateSpanBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateSpanBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ICreateSpanBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ICreateSpanBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IOptionalObservationBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IOptionalObservationBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IOptionalObservationBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IOptionalObservationBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateEventBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateEventBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateEventBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateEventBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateSpanBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateSpanBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateSpanBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IUpdateSpanBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionError.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionError.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IngestionError.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionError.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IngestionEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IngestionResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionResponse.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionSuccess.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionSuccess.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IngestionSuccess.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionSuccess.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionUsage.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionUsage.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/IngestionUsage.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/IngestionUsage.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ObservationBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ObservationType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ObservationType.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiCompletionUsageSchema.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiCompletionUsageSchema.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiCompletionUsageSchema.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiCompletionUsageSchema.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiResponseUsageSchema.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiResponseUsageSchema.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiResponseUsageSchema.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiResponseUsageSchema.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiUsage.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiUsage.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiUsage.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OpenAiUsage.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/OptionalObservationBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OptionalObservationBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/OptionalObservationBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/OptionalObservationBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ScoreBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/ScoreEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/ScoreEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/SdkLogEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/TraceBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/TraceBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/TraceBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/TraceBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/TraceEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/TraceEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/TraceEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/TraceEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateEventBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateEventBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateEventBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateEventBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateGenerationEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateObservationEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateObservationEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateObservationEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateObservationEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanBody.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanEvent.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanEvent.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanEvent.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UpdateSpanEvent.java diff --git a/src/main/java/com/langfuse/client/resources/ingestion/types/UsageDetails.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UsageDetails.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/ingestion/types/UsageDetails.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/ingestion/types/UsageDetails.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/AsyncLlmConnectionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/AsyncLlmConnectionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/AsyncLlmConnectionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/AsyncLlmConnectionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/AsyncRawLlmConnectionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/AsyncRawLlmConnectionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/AsyncRawLlmConnectionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/AsyncRawLlmConnectionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/LlmConnectionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/LlmConnectionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/LlmConnectionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/LlmConnectionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/RawLlmConnectionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/RawLlmConnectionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/RawLlmConnectionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/RawLlmConnectionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/requests/GetLlmConnectionsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/requests/GetLlmConnectionsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/requests/GetLlmConnectionsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/requests/GetLlmConnectionsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmAdapter.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmAdapter.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/types/LlmAdapter.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmAdapter.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmConnection.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmConnection.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/types/LlmConnection.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/LlmConnection.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/types/PaginatedLlmConnections.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/PaginatedLlmConnections.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/types/PaginatedLlmConnections.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/PaginatedLlmConnections.java diff --git a/src/main/java/com/langfuse/client/resources/llmconnections/types/UpsertLlmConnectionRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/UpsertLlmConnectionRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/llmconnections/types/UpsertLlmConnectionRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/llmconnections/types/UpsertLlmConnectionRequest.java diff --git a/src/main/java/com/langfuse/client/resources/media/AsyncMediaClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/AsyncMediaClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/AsyncMediaClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/AsyncMediaClient.java diff --git a/src/main/java/com/langfuse/client/resources/media/AsyncRawMediaClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/AsyncRawMediaClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/AsyncRawMediaClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/AsyncRawMediaClient.java diff --git a/src/main/java/com/langfuse/client/resources/media/MediaClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/MediaClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/MediaClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/MediaClient.java diff --git a/src/main/java/com/langfuse/client/resources/media/RawMediaClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/RawMediaClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/RawMediaClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/RawMediaClient.java diff --git a/src/main/java/com/langfuse/client/resources/media/types/GetMediaResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/types/GetMediaResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaResponse.java diff --git a/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlRequest.java diff --git a/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/GetMediaUploadUrlResponse.java diff --git a/src/main/java/com/langfuse/client/resources/media/types/MediaContentType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/MediaContentType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/types/MediaContentType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/MediaContentType.java diff --git a/src/main/java/com/langfuse/client/resources/media/types/PatchMediaBody.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/PatchMediaBody.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/media/types/PatchMediaBody.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/media/types/PatchMediaBody.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/AsyncMetricsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/AsyncMetricsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/AsyncMetricsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/AsyncMetricsClient.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/AsyncRawMetricsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/AsyncRawMetricsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/AsyncRawMetricsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/AsyncRawMetricsClient.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/MetricsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/MetricsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/MetricsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/MetricsClient.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/RawMetricsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/RawMetricsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/RawMetricsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/RawMetricsClient.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/requests/GetMetricsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/requests/GetMetricsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/requests/GetMetricsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/requests/GetMetricsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/metrics/types/MetricsResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/types/MetricsResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metrics/types/MetricsResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metrics/types/MetricsResponse.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/AsyncMetricsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/AsyncMetricsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/AsyncMetricsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/AsyncMetricsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/AsyncRawMetricsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/AsyncRawMetricsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/AsyncRawMetricsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/AsyncRawMetricsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/MetricsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/MetricsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/MetricsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/MetricsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/RawMetricsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/RawMetricsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/RawMetricsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/RawMetricsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/requests/GetMetricsV2Request.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/requests/GetMetricsV2Request.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/requests/GetMetricsV2Request.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/requests/GetMetricsV2Request.java diff --git a/src/main/java/com/langfuse/client/resources/metricsv2/types/MetricsV2Response.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/types/MetricsV2Response.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/metricsv2/types/MetricsV2Response.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/metricsv2/types/MetricsV2Response.java diff --git a/src/main/java/com/langfuse/client/resources/models/AsyncModelsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/AsyncModelsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/AsyncModelsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/AsyncModelsClient.java diff --git a/src/main/java/com/langfuse/client/resources/models/AsyncRawModelsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/AsyncRawModelsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/AsyncRawModelsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/AsyncRawModelsClient.java diff --git a/src/main/java/com/langfuse/client/resources/models/ModelsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/ModelsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/ModelsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/ModelsClient.java diff --git a/src/main/java/com/langfuse/client/resources/models/RawModelsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/RawModelsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/RawModelsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/RawModelsClient.java diff --git a/src/main/java/com/langfuse/client/resources/models/requests/GetModelsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/requests/GetModelsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/requests/GetModelsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/requests/GetModelsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/models/types/CreateModelRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/types/CreateModelRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/types/CreateModelRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/types/CreateModelRequest.java diff --git a/src/main/java/com/langfuse/client/resources/models/types/PaginatedModels.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/types/PaginatedModels.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/models/types/PaginatedModels.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/models/types/PaginatedModels.java diff --git a/src/main/java/com/langfuse/client/resources/observations/AsyncObservationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/AsyncObservationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/AsyncObservationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/AsyncObservationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/observations/AsyncRawObservationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/AsyncRawObservationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/AsyncRawObservationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/AsyncRawObservationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/observations/ObservationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/ObservationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/ObservationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/ObservationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/observations/RawObservationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/RawObservationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/RawObservationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/RawObservationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/observations/requests/GetObservationsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/requests/GetObservationsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/requests/GetObservationsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/requests/GetObservationsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/observations/types/Observations.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/types/Observations.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/types/Observations.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/types/Observations.java diff --git a/src/main/java/com/langfuse/client/resources/observations/types/ObservationsViews.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/types/ObservationsViews.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observations/types/ObservationsViews.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observations/types/ObservationsViews.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/AsyncObservationsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/AsyncObservationsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/AsyncObservationsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/AsyncObservationsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/AsyncRawObservationsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/AsyncRawObservationsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/AsyncRawObservationsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/AsyncRawObservationsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/ObservationsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/ObservationsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/ObservationsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/ObservationsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/RawObservationsV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/RawObservationsV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/RawObservationsV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/RawObservationsV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/requests/GetObservationsV2Request.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/requests/GetObservationsV2Request.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/requests/GetObservationsV2Request.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/requests/GetObservationsV2Request.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Meta.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Meta.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Meta.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Meta.java diff --git a/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Response.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Response.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Response.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/observationsv2/types/ObservationsV2Response.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncOpentelemetryClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncOpentelemetryClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/AsyncOpentelemetryClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncOpentelemetryClient.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncRawOpentelemetryClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncRawOpentelemetryClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/AsyncRawOpentelemetryClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/AsyncRawOpentelemetryClient.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/OpentelemetryClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/OpentelemetryClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/OpentelemetryClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/OpentelemetryClient.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/RawOpentelemetryClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/RawOpentelemetryClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/RawOpentelemetryClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/RawOpentelemetryClient.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/requests/OtelTraceRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/requests/OtelTraceRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/requests/OtelTraceRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/requests/OtelTraceRequest.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttribute.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttribute.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttribute.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttribute.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttributeValue.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttributeValue.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttributeValue.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelAttributeValue.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResource.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResource.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResource.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResource.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResourceSpan.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResourceSpan.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResourceSpan.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelResourceSpan.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScope.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScope.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScope.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScope.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScopeSpan.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScopeSpan.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScopeSpan.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelScopeSpan.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelSpan.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelSpan.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelSpan.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelSpan.java diff --git a/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelTraceResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelTraceResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelTraceResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/opentelemetry/types/OtelTraceResponse.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/AsyncOrganizationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/AsyncOrganizationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/AsyncOrganizationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/AsyncOrganizationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/AsyncRawOrganizationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/AsyncRawOrganizationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/AsyncRawOrganizationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/AsyncRawOrganizationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/OrganizationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/OrganizationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/OrganizationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/OrganizationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/RawOrganizationsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/RawOrganizationsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/RawOrganizationsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/RawOrganizationsClient.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/DeleteMembershipRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/DeleteMembershipRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/DeleteMembershipRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/DeleteMembershipRequest.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/MembershipDeletionResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipDeletionResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/MembershipDeletionResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipDeletionResponse.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/MembershipRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRequest.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/MembershipResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/MembershipResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipResponse.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRole.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRole.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/MembershipRole.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipRole.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/MembershipsResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipsResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/MembershipsResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/MembershipsResponse.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKey.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKey.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKey.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKey.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKeysResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKeysResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKeysResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationApiKeysResponse.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProject.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProject.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProject.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProject.java diff --git a/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProjectsResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProjectsResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProjectsResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/organizations/types/OrganizationProjectsResponse.java diff --git a/src/main/java/com/langfuse/client/resources/projects/AsyncProjectsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/AsyncProjectsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/AsyncProjectsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/AsyncProjectsClient.java diff --git a/src/main/java/com/langfuse/client/resources/projects/AsyncRawProjectsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/AsyncRawProjectsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/AsyncRawProjectsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/AsyncRawProjectsClient.java diff --git a/src/main/java/com/langfuse/client/resources/projects/ProjectsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/ProjectsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/ProjectsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/ProjectsClient.java diff --git a/src/main/java/com/langfuse/client/resources/projects/RawProjectsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/RawProjectsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/RawProjectsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/RawProjectsClient.java diff --git a/src/main/java/com/langfuse/client/resources/projects/requests/CreateApiKeyRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/CreateApiKeyRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/requests/CreateApiKeyRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/CreateApiKeyRequest.java diff --git a/src/main/java/com/langfuse/client/resources/projects/requests/CreateProjectRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/CreateProjectRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/requests/CreateProjectRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/CreateProjectRequest.java diff --git a/src/main/java/com/langfuse/client/resources/projects/requests/UpdateProjectRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/UpdateProjectRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/requests/UpdateProjectRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/requests/UpdateProjectRequest.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyDeletionResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyDeletionResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/ApiKeyDeletionResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyDeletionResponse.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyList.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyList.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/ApiKeyList.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyList.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/ApiKeyResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeyResponse.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/ApiKeySummary.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeySummary.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/ApiKeySummary.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ApiKeySummary.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/Organization.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Organization.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/Organization.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Organization.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/Project.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Project.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/Project.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Project.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/ProjectDeletionResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ProjectDeletionResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/ProjectDeletionResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/ProjectDeletionResponse.java diff --git a/src/main/java/com/langfuse/client/resources/projects/types/Projects.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Projects.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/projects/types/Projects.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/projects/types/Projects.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/AsyncPromptsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/AsyncPromptsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/AsyncPromptsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/AsyncPromptsClient.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/AsyncRawPromptsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/AsyncRawPromptsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/AsyncRawPromptsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/AsyncRawPromptsClient.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/PromptsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/PromptsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/PromptsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/PromptsClient.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/RawPromptsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/RawPromptsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/RawPromptsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/RawPromptsClient.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/requests/DeletePromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/DeletePromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/requests/DeletePromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/DeletePromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/requests/GetPromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/GetPromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/requests/GetPromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/GetPromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/requests/ListPromptsMetaRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/ListPromptsMetaRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/requests/ListPromptsMetaRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/requests/ListPromptsMetaRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/BasePrompt.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/BasePrompt.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/BasePrompt.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/BasePrompt.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessage.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessage.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/ChatMessage.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessage.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageType.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageWithPlaceholders.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageWithPlaceholders.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageWithPlaceholders.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatMessageWithPlaceholders.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/ChatPrompt.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatPrompt.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/ChatPrompt.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/ChatPrompt.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateChatPromptType.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/CreatePromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreatePromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/CreatePromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreatePromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/CreateTextPromptType.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/IBasePrompt.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/IBasePrompt.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/IBasePrompt.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/IBasePrompt.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessage.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessage.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessage.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessage.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessageType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessageType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessageType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PlaceholderMessageType.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/Prompt.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/Prompt.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/Prompt.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/Prompt.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/PromptMeta.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptMeta.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/PromptMeta.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptMeta.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/PromptMetaListResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptMetaListResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/PromptMetaListResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptMetaListResponse.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/PromptType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/PromptType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/PromptType.java diff --git a/src/main/java/com/langfuse/client/resources/prompts/types/TextPrompt.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/TextPrompt.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/prompts/types/TextPrompt.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/prompts/types/TextPrompt.java diff --git a/src/main/java/com/langfuse/client/resources/promptversion/AsyncPromptVersionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/AsyncPromptVersionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/promptversion/AsyncPromptVersionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/AsyncPromptVersionClient.java diff --git a/src/main/java/com/langfuse/client/resources/promptversion/AsyncRawPromptVersionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/AsyncRawPromptVersionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/promptversion/AsyncRawPromptVersionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/AsyncRawPromptVersionClient.java diff --git a/src/main/java/com/langfuse/client/resources/promptversion/PromptVersionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/PromptVersionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/promptversion/PromptVersionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/PromptVersionClient.java diff --git a/src/main/java/com/langfuse/client/resources/promptversion/RawPromptVersionClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/RawPromptVersionClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/promptversion/RawPromptVersionClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/RawPromptVersionClient.java diff --git a/src/main/java/com/langfuse/client/resources/promptversion/requests/UpdatePromptRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/requests/UpdatePromptRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/promptversion/requests/UpdatePromptRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/promptversion/requests/UpdatePromptRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scim/AsyncRawScimClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/AsyncRawScimClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/AsyncRawScimClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/AsyncRawScimClient.java diff --git a/src/main/java/com/langfuse/client/resources/scim/AsyncScimClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/AsyncScimClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/AsyncScimClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/AsyncScimClient.java diff --git a/src/main/java/com/langfuse/client/resources/scim/RawScimClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/RawScimClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/RawScimClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/RawScimClient.java diff --git a/src/main/java/com/langfuse/client/resources/scim/ScimClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/ScimClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/ScimClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/ScimClient.java diff --git a/src/main/java/com/langfuse/client/resources/scim/requests/CreateUserRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/requests/CreateUserRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/requests/CreateUserRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/requests/CreateUserRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scim/requests/ListUsersRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/requests/ListUsersRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/requests/ListUsersRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/requests/ListUsersRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/AuthenticationScheme.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/AuthenticationScheme.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/AuthenticationScheme.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/AuthenticationScheme.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/BulkConfig.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/BulkConfig.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/BulkConfig.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/BulkConfig.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/EmptyResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/EmptyResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/EmptyResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/EmptyResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/FilterConfig.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/FilterConfig.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/FilterConfig.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/FilterConfig.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ResourceMeta.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceMeta.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ResourceMeta.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceMeta.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ResourceType.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceType.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ResourceType.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceType.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ResourceTypesResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceTypesResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ResourceTypesResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ResourceTypesResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/SchemaExtension.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemaExtension.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/SchemaExtension.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemaExtension.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/SchemaResource.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemaResource.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/SchemaResource.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemaResource.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/SchemasResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemasResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/SchemasResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/SchemasResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ScimEmail.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimEmail.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ScimEmail.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimEmail.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ScimFeatureSupport.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimFeatureSupport.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ScimFeatureSupport.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimFeatureSupport.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ScimName.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimName.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ScimName.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimName.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ScimUser.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimUser.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ScimUser.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimUser.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ScimUsersListResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimUsersListResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ScimUsersListResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ScimUsersListResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/ServiceProviderConfig.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ServiceProviderConfig.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/ServiceProviderConfig.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/ServiceProviderConfig.java diff --git a/src/main/java/com/langfuse/client/resources/scim/types/UserMeta.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/UserMeta.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scim/types/UserMeta.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scim/types/UserMeta.java diff --git a/src/main/java/com/langfuse/client/resources/score/AsyncRawScoreClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/AsyncRawScoreClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/AsyncRawScoreClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/AsyncRawScoreClient.java diff --git a/src/main/java/com/langfuse/client/resources/score/AsyncScoreClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/AsyncScoreClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/AsyncScoreClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/AsyncScoreClient.java diff --git a/src/main/java/com/langfuse/client/resources/score/RawScoreClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/RawScoreClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/RawScoreClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/RawScoreClient.java diff --git a/src/main/java/com/langfuse/client/resources/score/ScoreClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/ScoreClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/ScoreClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/ScoreClient.java diff --git a/src/main/java/com/langfuse/client/resources/score/types/CreateScoreRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/types/CreateScoreRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/types/CreateScoreRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/types/CreateScoreRequest.java diff --git a/src/main/java/com/langfuse/client/resources/score/types/CreateScoreResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/types/CreateScoreResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/score/types/CreateScoreResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/score/types/CreateScoreResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncRawScoreConfigsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncRawScoreConfigsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncRawScoreConfigsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncRawScoreConfigsClient.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncScoreConfigsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncScoreConfigsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncScoreConfigsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/AsyncScoreConfigsClient.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/RawScoreConfigsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/RawScoreConfigsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/RawScoreConfigsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/RawScoreConfigsClient.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/ScoreConfigsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/ScoreConfigsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/ScoreConfigsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/ScoreConfigsClient.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/requests/GetScoreConfigsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/requests/GetScoreConfigsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/requests/GetScoreConfigsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/requests/GetScoreConfigsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/types/CreateScoreConfigRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/CreateScoreConfigRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/types/CreateScoreConfigRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/CreateScoreConfigRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/types/ScoreConfigs.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/ScoreConfigs.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/types/ScoreConfigs.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/ScoreConfigs.java diff --git a/src/main/java/com/langfuse/client/resources/scoreconfigs/types/UpdateScoreConfigRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/UpdateScoreConfigRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scoreconfigs/types/UpdateScoreConfigRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scoreconfigs/types/UpdateScoreConfigRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/AsyncRawScoreV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/AsyncRawScoreV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/AsyncRawScoreV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/AsyncRawScoreV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/AsyncScoreV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/AsyncScoreV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/AsyncScoreV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/AsyncScoreV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/RawScoreV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/RawScoreV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/RawScoreV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/RawScoreV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/ScoreV2Client.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/ScoreV2Client.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/ScoreV2Client.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/ScoreV2Client.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/requests/GetScoresRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/requests/GetScoresRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/requests/GetScoresRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/requests/GetScoresRequest.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponse.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseData.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseData.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseData.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseData.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataBoolean.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataBoolean.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataBoolean.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataBoolean.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCategorical.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCategorical.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCategorical.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCategorical.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCorrection.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCorrection.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCorrection.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataCorrection.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataNumeric.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataNumeric.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataNumeric.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseDataNumeric.java diff --git a/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseTraceData.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseTraceData.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseTraceData.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/scorev2/types/GetScoresResponseTraceData.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/AsyncRawSessionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/AsyncRawSessionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/AsyncRawSessionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/AsyncRawSessionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/AsyncSessionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/AsyncSessionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/AsyncSessionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/AsyncSessionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/RawSessionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/RawSessionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/RawSessionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/RawSessionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/SessionsClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/SessionsClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/SessionsClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/SessionsClient.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/requests/GetSessionsRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/requests/GetSessionsRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/requests/GetSessionsRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/requests/GetSessionsRequest.java diff --git a/src/main/java/com/langfuse/client/resources/sessions/types/PaginatedSessions.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/types/PaginatedSessions.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/sessions/types/PaginatedSessions.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/sessions/types/PaginatedSessions.java diff --git a/src/main/java/com/langfuse/client/resources/trace/AsyncRawTraceClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/AsyncRawTraceClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/AsyncRawTraceClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/AsyncRawTraceClient.java diff --git a/src/main/java/com/langfuse/client/resources/trace/AsyncTraceClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/AsyncTraceClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/AsyncTraceClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/AsyncTraceClient.java diff --git a/src/main/java/com/langfuse/client/resources/trace/RawTraceClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/RawTraceClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/RawTraceClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/RawTraceClient.java diff --git a/src/main/java/com/langfuse/client/resources/trace/TraceClient.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/TraceClient.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/TraceClient.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/TraceClient.java diff --git a/src/main/java/com/langfuse/client/resources/trace/requests/DeleteTracesRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/requests/DeleteTracesRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/requests/DeleteTracesRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/requests/DeleteTracesRequest.java diff --git a/src/main/java/com/langfuse/client/resources/trace/requests/GetTracesRequest.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/requests/GetTracesRequest.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/requests/GetTracesRequest.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/requests/GetTracesRequest.java diff --git a/src/main/java/com/langfuse/client/resources/trace/types/DeleteTraceResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/DeleteTraceResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/types/DeleteTraceResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/DeleteTraceResponse.java diff --git a/src/main/java/com/langfuse/client/resources/trace/types/Sort.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/Sort.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/types/Sort.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/Sort.java diff --git a/src/main/java/com/langfuse/client/resources/trace/types/Traces.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/Traces.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/trace/types/Traces.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/trace/types/Traces.java diff --git a/src/main/java/com/langfuse/client/resources/utils/pagination/types/MetaResponse.java b/langfuse-java-legacy/src/main/java/com/langfuse/client/resources/utils/pagination/types/MetaResponse.java similarity index 100% rename from src/main/java/com/langfuse/client/resources/utils/pagination/types/MetaResponse.java rename to langfuse-java-legacy/src/main/java/com/langfuse/client/resources/utils/pagination/types/MetaResponse.java diff --git a/src/test/java/com/langfuse/client/TestClientFactory.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/TestClientFactory.java similarity index 100% rename from src/test/java/com/langfuse/client/TestClientFactory.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/TestClientFactory.java diff --git a/src/test/java/com/langfuse/client/core/QueryStringMapperTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/core/QueryStringMapperTest.java similarity index 100% rename from src/test/java/com/langfuse/client/core/QueryStringMapperTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/core/QueryStringMapperTest.java diff --git a/src/test/java/com/langfuse/client/deserialization/ChatPromptDeserializationTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/deserialization/ChatPromptDeserializationTest.java similarity index 100% rename from src/test/java/com/langfuse/client/deserialization/ChatPromptDeserializationTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/deserialization/ChatPromptDeserializationTest.java diff --git a/src/test/java/com/langfuse/client/deserialization/TextPromptDeserializationTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/deserialization/TextPromptDeserializationTest.java similarity index 100% rename from src/test/java/com/langfuse/client/deserialization/TextPromptDeserializationTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/deserialization/TextPromptDeserializationTest.java diff --git a/src/test/java/com/langfuse/client/integration/PromptIntegrationTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/integration/PromptIntegrationTest.java similarity index 100% rename from src/test/java/com/langfuse/client/integration/PromptIntegrationTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/integration/PromptIntegrationTest.java diff --git a/src/test/java/com/langfuse/client/prompt/PromptCompilerTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/prompt/PromptCompilerTest.java similarity index 100% rename from src/test/java/com/langfuse/client/prompt/PromptCompilerTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/prompt/PromptCompilerTest.java diff --git a/src/test/java/com/langfuse/client/prompt/PromptTemplateTest.java b/langfuse-java-legacy/src/test/java/com/langfuse/client/prompt/PromptTemplateTest.java similarity index 100% rename from src/test/java/com/langfuse/client/prompt/PromptTemplateTest.java rename to langfuse-java-legacy/src/test/java/com/langfuse/client/prompt/PromptTemplateTest.java diff --git a/src/test/resources/fixtures/chat_prompt_response.json b/langfuse-java-legacy/src/test/resources/fixtures/chat_prompt_response.json similarity index 100% rename from src/test/resources/fixtures/chat_prompt_response.json rename to langfuse-java-legacy/src/test/resources/fixtures/chat_prompt_response.json diff --git a/src/test/resources/fixtures/text_prompt_response.json b/langfuse-java-legacy/src/test/resources/fixtures/text_prompt_response.json similarity index 100% rename from src/test/resources/fixtures/text_prompt_response.json rename to langfuse-java-legacy/src/test/resources/fixtures/text_prompt_response.json diff --git a/langfuse-java-testcontainers/.mvn/wrapper/maven-wrapper.properties b/langfuse-java-testcontainers/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5291372 --- /dev/null +++ b/langfuse-java-testcontainers/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/langfuse-java-testcontainers/README.md b/langfuse-java-testcontainers/README.md new file mode 100644 index 0000000..fc70acc --- /dev/null +++ b/langfuse-java-testcontainers/README.md @@ -0,0 +1,150 @@ +# langfuse-java-testcontainers + +[Testcontainers](https://testcontainers.com) support for the Langfuse Java SDK. Starts a complete Langfuse environment for integration testing, including all required infrastructure services. + +## Maven Coordinates + +```xml + + com.langfuse + langfuse-java-testcontainers + 0.2.1-SNAPSHOT + test + +``` + +## What Gets Started + +`LangfuseContainer` orchestrates 6 Docker containers: + +| Service | Default Image | Purpose | +|---------|--------------|---------| +| PostgreSQL | `postgres:17` | Primary database | +| ClickHouse | `clickhouse/clickhouse-server` | Analytics data store | +| Redis | `redis:7` | Queue and cache | +| MinIO | `cgr.dev/chainguard/minio` | S3-compatible object storage | +| Langfuse Web | `langfuse/langfuse:3` | API server | +| Langfuse Worker | `langfuse/langfuse-worker:3` | Async event processing | + +Infrastructure containers start in parallel, then the web server and worker start in parallel after infrastructure is ready. + +## Quick Start + +```java +var langfuse = new LangfuseContainer(); +langfuse.start(); + +String url = langfuse.getLangfuseUrl(); // http://localhost: +String publicKey = langfuse.getPublicKey(); // pk-lf-dev +String secretKey = langfuse.getSecretKey(); // sk-lf-dev +``` + +### With the client + +```java +var client = LangfuseApi.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .build(); + +var health = client.health().healthHealth(); +``` + +## Singleton Pattern + +For test suites with multiple test classes, use the [Testcontainers singleton pattern](https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers) to start the container once per JVM: + +```java +abstract class AbstractIntegrationTest { + static LangfuseContainer langfuse = new LangfuseContainer(); + static LangfuseApi client; + + static { + langfuse.start(); + client = LangfuseApi.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .build(); + } +} +``` + +Do **not** use `@Testcontainers` or `@Container` annotations with the singleton pattern -- they would stop the container after each test class. + +## Configuration + +Customize the environment using the builder API: + +```java +var langfuse = new LangfuseContainer( + LangfuseContainerConfig.builder() + .langfuse() + .image("docker.io/langfuse/langfuse:3") + .port(3000) + .startupTimeout(Duration.ofMinutes(5)) + .initProjectPublicKey("my-public-key") + .initProjectSecretKey("my-secret-key") + .initOrgName("My Org") + .initProjectName("My Project") + .ingestionQueueDelay(Duration.ofMillis(100)) + .ingestionClickhouseWriteInterval(Duration.ofMillis(500)) + .and() + .postgres() + .image("postgres:16") + .username("myuser") + .password("mypass") + .databaseName("langfuse") + .and() + .clickhouse() + .username("default") + .password("secret") + .and() + .redis() + .password("redis-secret") + .and() + .minio() + .rootUser("minio-user") + .rootPassword("minio-secret") + .bucketName("my-bucket") + .and() + .build()); +``` + +### Defaults + +All defaults are aligned with the project's `docker-compose.yml`: + +| Setting | Default | +|---------|---------| +| Langfuse image | `docker.io/langfuse/langfuse:3` | +| Langfuse port | `3000` | +| Startup timeout | 3 minutes | +| Public key | `pk-lf-dev` | +| Secret key | `sk-lf-dev` | +| PostgreSQL | `postgres:17`, user/pass `postgres/postgres` | +| ClickHouse | `clickhouse/clickhouse-server`, user/pass `clickhouse/clickhouse` | +| Redis | `redis:7`, password `myredissecret` | +| MinIO | `cgr.dev/chainguard/minio`, user/pass `minio/miniosecret` | + +## Diagnostics + +On test failure, retrieve logs from all containers: + +```java +Map logs = langfuse.getAllLogs(); +logs.forEach((container, log) -> + System.err.println("=== " + container + " ===\n" + log)); +``` + +Returns a map with keys: `langfuse-web`, `langfuse-worker`, `postgres`, `clickhouse`, `redis`, `minio`. + +## Podman + +If using Podman instead of Docker, set `service_timeout=0` in the Podman machine to avoid connection drops when starting multiple containers: + +```bash +podman machine ssh "sudo sh -c 'echo -e \"[engine]\nservice_timeout=0\" > /etc/containers/containers.conf'" +podman machine stop && podman machine start +``` diff --git a/langfuse-java-testcontainers/mvnw b/langfuse-java-testcontainers/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/langfuse-java-testcontainers/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/langfuse-java-testcontainers/mvnw.cmd b/langfuse-java-testcontainers/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/langfuse-java-testcontainers/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/langfuse-java-testcontainers/pom.xml b/langfuse-java-testcontainers/pom.xml new file mode 100644 index 0000000..2419745 --- /dev/null +++ b/langfuse-java-testcontainers/pom.xml @@ -0,0 +1,95 @@ + + 4.0.0 + + + com.langfuse + langfuse-java-parent + 0.2.1-SNAPSHOT + + + langfuse-java-testcontainers + jar + + langfuse-java-testcontainers + Testcontainers support for the Langfuse Java SDK + + + + + org.testcontainers + testcontainers + + + + + org.testcontainers + testcontainers-postgresql + + + org.testcontainers + testcontainers-clickhouse + + + org.testcontainers + testcontainers-minio + + + com.redis + testcontainers-redis + 2.2.4 + + + + + org.slf4j + slf4j-api + + + + org.testcontainers + testcontainers-junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/LangfuseContainer.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/LangfuseContainer.java new file mode 100644 index 0000000..7acb5e3 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/LangfuseContainer.java @@ -0,0 +1,351 @@ +package com.langfuse.testcontainers; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.testcontainers.clickhouse.ClickHouseContainer; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; + +import com.langfuse.testcontainers.config.LangfuseConfig; +import com.langfuse.testcontainers.config.LangfuseContainerConfig; +import com.redis.testcontainers.RedisContainer; + +/** + * Testcontainers-based Langfuse environment. + * + *

Orchestrates all services required by Langfuse (PostgreSQL, ClickHouse, Redis, MinIO) + * and starts the Langfuse web server. Provides the Langfuse URL and pre-configured + * API credentials for use in integration tests. + * + *

{@code
+ * var langfuse = new LangfuseContainer();
+ * langfuse.start();
+ *
+ * String url = langfuse.getLangfuseUrl();
+ * String publicKey = langfuse.getPublicKey();
+ * String secretKey = langfuse.getSecretKey();
+ * }
+ * + * @author Eric Deandrea + */ +public class LangfuseContainer extends GenericContainer { + private static final String SERVICE_LABEL = "com.langfuse.testcontainers.service"; + private static final String POSTGRES_ALIAS = "postgres"; + private static final String CLICKHOUSE_ALIAS = "clickhouse"; + private static final String REDIS_ALIAS = "redis"; + private static final String MINIO_ALIAS = "minio"; + + private final LangfuseContainerConfig config; + private final Network network; + private final PostgreSQLContainer postgres; + private final ClickHouseContainer clickhouse; + private final RedisContainer redis; + private final MinIOContainer minio; + private final GenericContainer worker; + + /** + * Creates a new Langfuse test environment with default configuration. + */ + public LangfuseContainer() { + this(LangfuseContainerConfig.builder().build()); + } + + /** + * Creates a new Langfuse test environment with the given configuration. + * + * @param config the container configuration + */ + public LangfuseContainer(LangfuseContainerConfig config) { + super(config.langfuse().image()); + this.config = config; + this.network = Network.newNetwork(); + + var pgConfig = config.postgres(); + this.postgres = new PostgreSQLContainer<>(pgConfig.image()) + .withNetwork(network) + .withNetworkAliases(POSTGRES_ALIAS) + .withLabel(SERVICE_LABEL, "langfuse-postgres") + .withUsername(pgConfig.username()) + .withPassword(pgConfig.password()) + .withDatabaseName(pgConfig.databaseName()); + pgConfig.containerEnv().forEach(postgres::withEnv); + + var chConfig = config.clickhouse(); + this.clickhouse = new ClickHouseContainer(chConfig.image()) + .withNetwork(network) + .withNetworkAliases(CLICKHOUSE_ALIAS) + .withLabel(SERVICE_LABEL, "langfuse-clickhouse") + .withUsername(chConfig.username()) + .withPassword(chConfig.password()) + .withDatabaseName(chConfig.databaseName()); + chConfig.containerEnv().forEach(clickhouse::withEnv); + + var redisConfig = config.redis(); + this.redis = new RedisContainer(redisConfig.image()) + .withNetwork(network) + .withNetworkAliases(REDIS_ALIAS) + .withLabel(SERVICE_LABEL, "langfuse-redis") + .withCommand("--requirepass", redisConfig.password(), "--maxmemory-policy", "noeviction"); + redisConfig.containerEnv().forEach(redis::withEnv); + + var minioConfig = config.minio(); + this.minio = new MinIOContainer( + DockerImageName.parse(minioConfig.image()) + .asCompatibleSubstituteFor("minio/minio")) + .withNetwork(network) + .withNetworkAliases(MINIO_ALIAS) + .withLabel(SERVICE_LABEL, "langfuse-minio") + .withEnv("MINIO_ROOT_USER", minioConfig.rootUser()) + .withEnv("MINIO_ROOT_PASSWORD", minioConfig.rootPassword()) + .withCommand("server", "--address", ":9000", "--console-address", ":9001", "/data"); + minioConfig.containerEnv().forEach(minio::withEnv); + + var workerConfig = config.worker(); + this.worker = new GenericContainer<>(workerConfig.image()) + .withNetwork(network) + .withLabel(SERVICE_LABEL, "langfuse-worker"); + workerConfig.containerEnv().forEach(worker::withEnv); + + configureWorker(); + } + + /** + * Retrieves the port number currently mapped for the service. + * + * @return the port number mapped for the service, or the default port if no mapping is found + */ + public int getPort() { + return getMappedPort(LangfuseConfig.DEFAULT_PORT); + } + + @Override + protected void configure() { + var langfuseConfig = config.langfuse(); + var pgConfig = config.postgres(); + var chConfig = config.clickhouse(); + var redisConfig = config.redis(); + var minioConfig = config.minio(); + + withNetwork(network); + withLabel(SERVICE_LABEL, "langfuse-web"); + withExposedPorts(langfuseConfig.port()); + + withEnv("DATABASE_URL", "postgresql://%s:%s@%s:5432/%s".formatted( + pgConfig.username(), pgConfig.password(), POSTGRES_ALIAS, pgConfig.databaseName())); + withEnv("CLICKHOUSE_MIGRATION_URL", "clickhouse://%s:9000".formatted(CLICKHOUSE_ALIAS)); + withEnv("CLICKHOUSE_URL", "http://%s:8123".formatted(CLICKHOUSE_ALIAS)); + withEnv("CLICKHOUSE_USER", chConfig.username()); + withEnv("CLICKHOUSE_PASSWORD", chConfig.password()); + withEnv("CLICKHOUSE_CLUSTER_ENABLED", "false"); + + withEnv("REDIS_HOST", REDIS_ALIAS); + withEnv("REDIS_PORT", "6379"); + withEnv("REDIS_AUTH", redisConfig.password()); + withEnv("REDIS_TLS_ENABLED", String.valueOf(redisConfig.tlsEnabled())); + + withEnv("LANGFUSE_S3_EVENT_UPLOAD_BUCKET", minioConfig.bucketName()); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_REGION", "auto"); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID", minioConfig.rootUser()); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE", "true"); + withEnv("LANGFUSE_S3_EVENT_UPLOAD_PREFIX", "events/"); + + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_BUCKET", minioConfig.bucketName()); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_REGION", "auto"); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID", minioConfig.rootUser()); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE", "true"); + withEnv("LANGFUSE_S3_MEDIA_UPLOAD_PREFIX", "media/"); + + withEnv("LANGFUSE_S3_BATCH_EXPORT_ENABLED", String.valueOf(langfuseConfig.batchExportEnabled())); + withEnv("LANGFUSE_S3_BATCH_EXPORT_BUCKET", minioConfig.bucketName()); + withEnv("LANGFUSE_S3_BATCH_EXPORT_PREFIX", "exports/"); + withEnv("LANGFUSE_S3_BATCH_EXPORT_REGION", "auto"); + withEnv("LANGFUSE_S3_BATCH_EXPORT_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + withEnv("LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT", "http://localhost:9090"); + withEnv("LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID", minioConfig.rootUser()); + withEnv("LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + withEnv("LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE", "true"); + + withEnv("LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES", String.valueOf(langfuseConfig.enableExperimentalFeatures())); + withEnv("LANGFUSE_USE_AZURE_BLOB", "false"); + withEnv("LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE", "false"); + withEnv("LANGFUSE_OCI_AUTH_TYPE", "workload_identity"); + langfuseConfig.ingestionQueueDelay() + .ifPresent(d -> withEnv("LANGFUSE_INGESTION_QUEUE_DELAY_MS", String.valueOf(d.toMillis()))); + langfuseConfig.ingestionClickhouseWriteInterval() + .ifPresent(d -> withEnv("LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS", String.valueOf(d.toMillis()))); + + withEnv("EMAIL_FROM_ADDRESS", langfuseConfig.emailFromAddress()); + withEnv("SMTP_CONNECTION_URL", langfuseConfig.smtpConnectionUrl()); + + withEnv("NEXTAUTH_URL", "http://localhost:3000"); + withEnv("NEXTAUTH_SECRET", "mysecret"); + withEnv("SALT", "mysalt"); + withEnv("ENCRYPTION_KEY", "0000000000000000000000000000000000000000000000000000000000000000"); + withEnv("TELEMETRY_ENABLED", "false"); + + withEnv("LANGFUSE_INIT_ORG_ID", langfuseConfig.initOrgId()); + withEnv("LANGFUSE_INIT_ORG_NAME", langfuseConfig.initOrgName()); + withEnv("LANGFUSE_INIT_PROJECT_ID", langfuseConfig.initProjectId()); + withEnv("LANGFUSE_INIT_PROJECT_NAME", langfuseConfig.initProjectName()); + withEnv("LANGFUSE_INIT_PROJECT_PUBLIC_KEY", langfuseConfig.initProjectPublicKey()); + withEnv("LANGFUSE_INIT_PROJECT_SECRET_KEY", langfuseConfig.initProjectSecretKey()); + withEnv("LANGFUSE_INIT_USER_EMAIL", langfuseConfig.initUserEmail()); + withEnv("LANGFUSE_INIT_USER_NAME", langfuseConfig.initUserName()); + withEnv("LANGFUSE_INIT_USER_PASSWORD", langfuseConfig.initUserPassword()); + + langfuseConfig.containerEnv().forEach(this::withEnv); + + waitingFor(new HttpWaitStrategy() + .forPort(langfuseConfig.port()) + .forPath("/api/public/health") + .forStatusCode(200) + .withStartupTimeout(langfuseConfig.startupTimeout())); + } + + @Override + public void start() { + Startables.deepStart(postgres, clickhouse, redis, minio) + .thenRun(this::createMinioBucket) + .thenCompose(v -> CompletableFuture.allOf( + CompletableFuture.runAsync(super::start), + CompletableFuture.runAsync(worker::start))) + .join(); + } + + @Override + public void stop() { + super.stop(); + worker.stop(); + postgres.stop(); + clickhouse.stop(); + redis.stop(); + minio.stop(); + network.close(); + } + + /** + * Returns the full Langfuse URL (e.g., {@code http://localhost:32768}). + * + * @return the Langfuse URL + */ + public String getLangfuseUrl() { + return "http://%s:%d".formatted(getHost(), getMappedPort(config.langfuse().port())); + } + + /** + * Returns the pre-configured public API key. + * + * @return the public key + */ + public String getPublicKey() { + return config.langfuse().initProjectPublicKey(); + } + + /** + * Returns the pre-configured secret API key. + * + * @return the secret key + */ + public String getSecretKey() { + return config.langfuse().initProjectSecretKey(); + } + + /** + * Returns the logs from all containers in the Langfuse environment. + * + * @return a map of container name to log output + */ + public Map getAllLogs() { + return Map.of( + "langfuse-web", getLogs(), + "langfuse-worker", worker.getLogs(), + "langfuse-postgres", postgres.getLogs(), + "langfuse-clickhouse", clickhouse.getLogs(), + "langfuse-redis", redis.getLogs(), + "langfuse-minio", minio.getLogs() + ); + } + + private void configureWorker() { + var langfuseConfig = config.langfuse(); + var pgConfig = config.postgres(); + var chConfig = config.clickhouse(); + var redisConfig = config.redis(); + var minioConfig = config.minio(); + + worker.withEnv("DATABASE_URL", "postgresql://%s:%s@%s:5432/%s".formatted( + pgConfig.username(), pgConfig.password(), POSTGRES_ALIAS, pgConfig.databaseName())); + worker.withEnv("CLICKHOUSE_MIGRATION_URL", "clickhouse://%s:9000".formatted(CLICKHOUSE_ALIAS)); + worker.withEnv("CLICKHOUSE_URL", "http://%s:8123".formatted(CLICKHOUSE_ALIAS)); + worker.withEnv("CLICKHOUSE_USER", chConfig.username()); + worker.withEnv("CLICKHOUSE_PASSWORD", chConfig.password()); + worker.withEnv("CLICKHOUSE_CLUSTER_ENABLED", "false"); + + worker.withEnv("REDIS_HOST", REDIS_ALIAS); + worker.withEnv("REDIS_PORT", "6379"); + worker.withEnv("REDIS_AUTH", redisConfig.password()); + worker.withEnv("REDIS_TLS_ENABLED", String.valueOf(redisConfig.tlsEnabled())); + + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_BUCKET", minioConfig.bucketName()); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_REGION", "auto"); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID", minioConfig.rootUser()); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE", "true"); + worker.withEnv("LANGFUSE_S3_EVENT_UPLOAD_PREFIX", "events/"); + + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_BUCKET", minioConfig.bucketName()); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_REGION", "auto"); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID", minioConfig.rootUser()); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE", "true"); + worker.withEnv("LANGFUSE_S3_MEDIA_UPLOAD_PREFIX", "media/"); + + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_ENABLED", String.valueOf(langfuseConfig.batchExportEnabled())); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_BUCKET", minioConfig.bucketName()); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_PREFIX", "exports/"); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_REGION", "auto"); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_ENDPOINT", "http://%s:9000".formatted(MINIO_ALIAS)); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT", "http://localhost:9090"); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID", minioConfig.rootUser()); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY", minioConfig.rootPassword()); + worker.withEnv("LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE", "true"); + + worker.withEnv("LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES", String.valueOf(langfuseConfig.enableExperimentalFeatures())); + worker.withEnv("LANGFUSE_USE_AZURE_BLOB", "false"); + worker.withEnv("LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE", "false"); + worker.withEnv("LANGFUSE_OCI_AUTH_TYPE", "workload_identity"); + langfuseConfig.ingestionQueueDelay() + .ifPresent(d -> worker.withEnv("LANGFUSE_INGESTION_QUEUE_DELAY_MS", String.valueOf(d.toMillis()))); + langfuseConfig.ingestionClickhouseWriteInterval() + .ifPresent(d -> worker.withEnv("LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS", String.valueOf(d.toMillis()))); + + worker.withEnv("EMAIL_FROM_ADDRESS", langfuseConfig.emailFromAddress()); + worker.withEnv("SMTP_CONNECTION_URL", langfuseConfig.smtpConnectionUrl()); + + worker.withEnv("NEXTAUTH_URL", "http://localhost:3000"); + worker.withEnv("SALT", "mysalt"); + worker.withEnv("ENCRYPTION_KEY", "0000000000000000000000000000000000000000000000000000000000000000"); + worker.withEnv("TELEMETRY_ENABLED", "false"); + } + + private void createMinioBucket() { + try { + minio.execInContainer("mkdir", "-p", "/data/" + config.minio().bucketName()); + } + catch (Exception e) { + throw new RuntimeException("Failed to create MinIO bucket", e); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/ClickHouseConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/ClickHouseConfig.java new file mode 100644 index 0000000..2c7e4a5 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/ClickHouseConfig.java @@ -0,0 +1,113 @@ +package com.langfuse.testcontainers.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * ClickHouse configuration. + * + * @author Eric Deandrea + */ +public interface ClickHouseConfig { + /** + * Default container image for ClickHouse. + */ + String DEFAULT_IMAGE = "clickhouse/clickhouse-server"; + + /** + * @return the container image + */ + String image(); + + /** + * @return the database username + */ + String username(); + + /** + * @return the database password + */ + String password(); + + /** + * @return the database name + */ + String databaseName(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link ClickHouseConfig}. + * + * @author Eric Deandrea + */ + class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + String username = "clickhouse"; + String password = "clickhouse"; + String databaseName = "default"; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value ClickHouseConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** @param username the username (default: {@code "clickhouse"}) + * @return this builder */ + public Builder username(String username) { + this.username = username; + return this; + } + + /** @param password the password (default: {@code "clickhouse"}) + * @return this builder */ + public Builder password(String password) { + this.password = password; + return this; + } + + /** @param databaseName the database name (default: {@code "default"}) + * @return this builder */ + public Builder databaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + ClickHouseConfig build() { + return new DefaultClickHouseConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultClickHouseConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultClickHouseConfig.java new file mode 100644 index 0000000..9e4df15 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultClickHouseConfig.java @@ -0,0 +1,45 @@ +package com.langfuse.testcontainers.config; + + +import java.util.Map; + +final class DefaultClickHouseConfig implements ClickHouseConfig { + private final String image; + private final String username; + private final String password; + private final String databaseName; + private final Map containerEnv; + + DefaultClickHouseConfig(Builder builder) { + this.image = builder.image; + this.username = builder.username; + this.password = builder.password; + this.databaseName = builder.databaseName; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public String databaseName() { + return databaseName; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseConfig.java new file mode 100644 index 0000000..aaf2596 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseConfig.java @@ -0,0 +1,144 @@ +package com.langfuse.testcontainers.config; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +final class DefaultLangfuseConfig implements LangfuseConfig { + private final String image; + private final int port; + private final Duration startupTimeout; + private final String initOrgId; + private final String initOrgName; + private final String initProjectId; + private final String initProjectName; + private final String initProjectPublicKey; + private final String initProjectSecretKey; + private final String initUserEmail; + private final String initUserName; + private final String initUserPassword; + private final boolean enableExperimentalFeatures; + private final boolean batchExportEnabled; + private final Duration ingestionQueueDelay; + private final Duration ingestionClickhouseWriteInterval; + private final String emailFromAddress; + private final String smtpConnectionUrl; + private final Map containerEnv; + + DefaultLangfuseConfig(Builder builder) { + this.image = builder.image; + this.port = builder.port; + this.startupTimeout = builder.startupTimeout; + this.initOrgId = builder.initOrgId; + this.initOrgName = builder.initOrgName; + this.initProjectId = builder.initProjectId; + this.initProjectName = builder.initProjectName; + this.initProjectPublicKey = builder.initProjectPublicKey; + this.initProjectSecretKey = builder.initProjectSecretKey; + this.initUserEmail = builder.initUserEmail; + this.initUserName = builder.initUserName; + this.initUserPassword = builder.initUserPassword; + this.enableExperimentalFeatures = builder.enableExperimentalFeatures; + this.batchExportEnabled = builder.batchExportEnabled; + this.ingestionQueueDelay = builder.ingestionQueueDelay; + this.ingestionClickhouseWriteInterval = builder.ingestionClickhouseWriteInterval; + this.emailFromAddress = builder.emailFromAddress; + this.smtpConnectionUrl = builder.smtpConnectionUrl; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public int port() { + return port; + } + + @Override + public Duration startupTimeout() { + return startupTimeout; + } + + @Override + public String initOrgId() { + return initOrgId; + } + + @Override + public String initOrgName() { + return initOrgName; + } + + @Override + public String initProjectId() { + return initProjectId; + } + + @Override + public String initProjectName() { + return initProjectName; + } + + @Override + public String initProjectPublicKey() { + return initProjectPublicKey; + } + + @Override + public String initProjectSecretKey() { + return initProjectSecretKey; + } + + @Override + public String initUserEmail() { + return initUserEmail; + } + + @Override + public String initUserName() { + return initUserName; + } + + @Override + public String initUserPassword() { + return initUserPassword; + } + + @Override + public boolean enableExperimentalFeatures() { + return enableExperimentalFeatures; + } + + @Override + public boolean batchExportEnabled() { + return batchExportEnabled; + } + + @Override + public Optional ingestionQueueDelay() { + return Optional.ofNullable(ingestionQueueDelay); + } + + @Override + public Optional ingestionClickhouseWriteInterval() { + return Optional.ofNullable(ingestionClickhouseWriteInterval); + } + + @Override + public String emailFromAddress() { + return emailFromAddress; + } + + @Override + public String smtpConnectionUrl() { + return smtpConnectionUrl; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseContainerConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseContainerConfig.java new file mode 100644 index 0000000..5f67eb4 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultLangfuseContainerConfig.java @@ -0,0 +1,56 @@ +package com.langfuse.testcontainers.config; + + +/** + * Default implementation of {@link LangfuseContainerConfig}. + * + * @author Eric Deandrea + */ +final class DefaultLangfuseContainerConfig implements LangfuseContainerConfig { + + private final LangfuseConfig langfuse; + private final PostgresConfig postgres; + private final ClickHouseConfig clickhouse; + private final RedisConfig redis; + private final MinIOConfig minio; + private final WorkerConfig worker; + + DefaultLangfuseContainerConfig(Builder builder) { + this.langfuse = builder.langfuse.build(); + this.postgres = builder.postgres.build(); + this.clickhouse = builder.clickhouse.build(); + this.redis = builder.redis.build(); + this.minio = builder.minio.build(); + this.worker = builder.worker.build(); + } + + @Override + public LangfuseConfig langfuse() { + return langfuse; + } + + @Override + public PostgresConfig postgres() { + return postgres; + } + + @Override + public ClickHouseConfig clickhouse() { + return clickhouse; + } + + @Override + public RedisConfig redis() { + return redis; + } + + @Override + public MinIOConfig minio() { + return minio; + } + + @Override + public WorkerConfig worker() { + return worker; + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultMinIOConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultMinIOConfig.java new file mode 100644 index 0000000..d7dcdc2 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultMinIOConfig.java @@ -0,0 +1,45 @@ +package com.langfuse.testcontainers.config; + + +import java.util.Map; + +final class DefaultMinIOConfig implements MinIOConfig { + private final String image; + private final String rootUser; + private final String rootPassword; + private final String bucketName; + private final Map containerEnv; + + DefaultMinIOConfig(Builder builder) { + this.image = builder.image; + this.rootUser = builder.rootUser; + this.rootPassword = builder.rootPassword; + this.bucketName = builder.bucketName; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public String rootUser() { + return rootUser; + } + + @Override + public String rootPassword() { + return rootPassword; + } + + @Override + public String bucketName() { + return bucketName; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultPostgresConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultPostgresConfig.java new file mode 100644 index 0000000..c4cc702 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultPostgresConfig.java @@ -0,0 +1,45 @@ +package com.langfuse.testcontainers.config; + + +import java.util.Map; + +final class DefaultPostgresConfig implements PostgresConfig { + private final String image; + private final String username; + private final String password; + private final String databaseName; + private final Map containerEnv; + + DefaultPostgresConfig(Builder builder) { + this.image = builder.image; + this.username = builder.username; + this.password = builder.password; + this.databaseName = builder.databaseName; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public String databaseName() { + return databaseName; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultRedisConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultRedisConfig.java new file mode 100644 index 0000000..a8c878c --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultRedisConfig.java @@ -0,0 +1,37 @@ +package com.langfuse.testcontainers.config; + +import java.util.Map; + +final class DefaultRedisConfig implements RedisConfig { + private final String image; + private final String password; + private final boolean tlsEnabled; + private final Map containerEnv; + + DefaultRedisConfig(Builder builder) { + this.image = builder.image; + this.password = builder.password; + this.tlsEnabled = builder.tlsEnabled; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public String password() { + return password; + } + + @Override + public boolean tlsEnabled() { + return tlsEnabled; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultWorkerConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultWorkerConfig.java new file mode 100644 index 0000000..71af275 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/DefaultWorkerConfig.java @@ -0,0 +1,23 @@ +package com.langfuse.testcontainers.config; + +import java.util.Map; + +final class DefaultWorkerConfig implements WorkerConfig { + private final String image; + private final Map containerEnv; + + DefaultWorkerConfig(Builder builder) { + this.image = builder.image; + this.containerEnv = Map.copyOf(builder.containerEnv); + } + + @Override + public String image() { + return image; + } + + @Override + public Map containerEnv() { + return containerEnv; + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseConfig.java new file mode 100644 index 0000000..7f2a735 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseConfig.java @@ -0,0 +1,321 @@ +package com.langfuse.testcontainers.config; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Langfuse web server configuration. + * + * @author Eric Deandrea + */ +public interface LangfuseConfig { + /** + * Default container image for Langfuse web server. + */ + String DEFAULT_IMAGE = "docker.io/langfuse/langfuse:3"; + + /** + * Default port for the Langfuse web server. + */ + int DEFAULT_PORT = 3000; + + /** + * Default startup timeout for the Langfuse web server container. + */ + Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofMinutes(3); + + /** + * @return the container image + */ + String image(); + + /** + * @return the port the Langfuse web server listens on + */ + int port(); + + /** + * @return the startup timeout + */ + Duration startupTimeout(); + + /** + * @return the init org ID + */ + String initOrgId(); + + /** + * @return the init org name + */ + String initOrgName(); + + /** + * @return the init project ID + */ + String initProjectId(); + + /** + * @return the init project name + */ + String initProjectName(); + + /** + * @return the init project public API key + */ + String initProjectPublicKey(); + + /** + * @return the init project secret API key + */ + String initProjectSecretKey(); + + /** + * @return the init user email + */ + String initUserEmail(); + + /** + * @return the init user name + */ + String initUserName(); + + /** + * @return the init user password + */ + String initUserPassword(); + + /** + * @return whether experimental features are enabled + */ + boolean enableExperimentalFeatures(); + + /** + * @return whether batch export is enabled + */ + boolean batchExportEnabled(); + + /** + * @return the ingestion queue delay, or empty if using the server default + */ + Optional ingestionQueueDelay(); + + /** + * @return the ingestion ClickHouse write interval, or empty if using the server default + */ + Optional ingestionClickhouseWriteInterval(); + + /** + * @return the email from address (empty string if unset) + */ + String emailFromAddress(); + + /** + * @return the SMTP connection URL (empty string if unset) + */ + String smtpConnectionUrl(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link LangfuseConfig}. + * + * @author Eric Deandrea + */ + final class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + int port = DEFAULT_PORT; + Duration startupTimeout = DEFAULT_STARTUP_TIMEOUT; + String initOrgId = "langfuse-dev-org"; + String initOrgName = "Langfuse Dev Org"; + String initProjectId = "langfuse-dev-project"; + String initProjectName = "langfuse-dev"; + String initProjectPublicKey = "pk-lf-dev"; + String initProjectSecretKey = "sk-lf-dev"; + String initUserEmail = "dev@langfuse.com"; + String initUserName = "Dev User"; + String initUserPassword = "password"; + boolean enableExperimentalFeatures = false; + boolean batchExportEnabled = false; + Duration ingestionQueueDelay; + Duration ingestionClickhouseWriteInterval; + String emailFromAddress = ""; + String smtpConnectionUrl = ""; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * Returns the parent builder. + * + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value LangfuseConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** + * Sets the port the Langfuse web server listens on. + * + * @param port the port (default: {@value LangfuseConfig#DEFAULT_PORT}) + * @return this builder + */ + public Builder port(int port) { + this.port = port; + return this; + } + + /** @param startupTimeout the maximum time to wait for the container to start + * @return this builder */ + public Builder startupTimeout(Duration startupTimeout) { + this.startupTimeout = startupTimeout; + return this; + } + + /** @param initOrgId the org ID (default: {@code "langfuse-dev-org"}) + * @return this builder */ + public Builder initOrgId(String initOrgId) { + this.initOrgId = initOrgId; + return this; + } + + /** @param initOrgName the org name (default: {@code "Langfuse Dev Org"}) + * @return this builder */ + public Builder initOrgName(String initOrgName) { + this.initOrgName = initOrgName; + return this; + } + + /** @param initProjectId the project ID (default: {@code "langfuse-dev-project"}) + * @return this builder */ + public Builder initProjectId(String initProjectId) { + this.initProjectId = initProjectId; + return this; + } + + /** @param initProjectName the project name (default: {@code "langfuse-dev"}) + * @return this builder */ + public Builder initProjectName(String initProjectName) { + this.initProjectName = initProjectName; + return this; + } + + /** @param initProjectPublicKey the public key (default: {@code "pk-lf-dev"}) + * @return this builder */ + public Builder initProjectPublicKey(String initProjectPublicKey) { + this.initProjectPublicKey = initProjectPublicKey; + return this; + } + + /** @param initProjectSecretKey the secret key (default: {@code "sk-lf-dev"}) + * @return this builder */ + public Builder initProjectSecretKey(String initProjectSecretKey) { + this.initProjectSecretKey = initProjectSecretKey; + return this; + } + + /** @param initUserEmail the user email (default: {@code "dev@langfuse.com"}) + * @return this builder */ + public Builder initUserEmail(String initUserEmail) { + this.initUserEmail = initUserEmail; + return this; + } + + /** @param initUserName the user name (default: {@code "Dev User"}) + * @return this builder */ + public Builder initUserName(String initUserName) { + this.initUserName = initUserName; + return this; + } + + /** @param initUserPassword the user password (default: {@code "password"}) + * @return this builder */ + public Builder initUserPassword(String initUserPassword) { + this.initUserPassword = initUserPassword; + return this; + } + + /** @param enableExperimentalFeatures whether to enable experimental features (default: {@code false}) + * @return this builder */ + public Builder enableExperimentalFeatures(boolean enableExperimentalFeatures) { + this.enableExperimentalFeatures = enableExperimentalFeatures; + return this; + } + + /** @param batchExportEnabled whether to enable batch export (default: {@code false}) + * @return this builder */ + public Builder batchExportEnabled(boolean batchExportEnabled) { + this.batchExportEnabled = batchExportEnabled; + return this; + } + + /** + * Sets the ingestion queue processing delay. + * + * @param ingestionQueueDelay the delay between queue processing cycles + * @return this builder + */ + public Builder ingestionQueueDelay(Duration ingestionQueueDelay) { + this.ingestionQueueDelay = ingestionQueueDelay; + return this; + } + + /** + * Sets the interval between ClickHouse write flushes for ingested events. + * + * @param ingestionClickhouseWriteInterval the write interval + * @return this builder + */ + public Builder ingestionClickhouseWriteInterval(Duration ingestionClickhouseWriteInterval) { + this.ingestionClickhouseWriteInterval = ingestionClickhouseWriteInterval; + return this; + } + + /** @param emailFromAddress the email from address (default: empty) + * @return this builder */ + public Builder emailFromAddress(String emailFromAddress) { + this.emailFromAddress = emailFromAddress; + return this; + } + + /** @param smtpConnectionUrl the SMTP connection URL (default: empty) + * @return this builder */ + public Builder smtpConnectionUrl(String smtpConnectionUrl) { + this.smtpConnectionUrl = smtpConnectionUrl; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + LangfuseConfig build() { + return new DefaultLangfuseConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseContainerConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseContainerConfig.java new file mode 100644 index 0000000..e251c58 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/LangfuseContainerConfig.java @@ -0,0 +1,144 @@ +package com.langfuse.testcontainers.config; + +/** + * Configuration for the Langfuse testcontainers environment. + * Contains nested configurations for each infrastructure service. + * + *
{@code
+ * var config = LangfuseContainerConfig.builder()
+ *     .langfuse()
+ *         .initProjectPublicKey("my-pk")
+ *         .initProjectSecretKey("my-sk")
+ *         .and()
+ *     .postgres()
+ *         .image("postgres:16")
+ *         .and()
+ *     .build();
+ * }
+ * + * @author Eric Deandrea + */ +public interface LangfuseContainerConfig { + + /** + * @return the Langfuse web server configuration + */ + LangfuseConfig langfuse(); + + /** + * @return the PostgreSQL configuration + */ + PostgresConfig postgres(); + + /** + * @return the ClickHouse configuration + */ + ClickHouseConfig clickhouse(); + + /** + * @return the Redis configuration + */ + RedisConfig redis(); + + /** + * @return the MinIO configuration + */ + MinIOConfig minio(); + + /** + * @return the Langfuse worker configuration + */ + WorkerConfig worker(); + + /** + * Creates a new builder with default values. + * + * @return a new {@link Builder} + */ + static Builder builder() { + return new Builder(); + } + + /** + * Top-level builder for {@link LangfuseContainerConfig}. + * + * @author Eric Deandrea + */ + class Builder { + final LangfuseConfig.Builder langfuse = new LangfuseConfig.Builder(this); + final PostgresConfig.Builder postgres = new PostgresConfig.Builder(this); + final ClickHouseConfig.Builder clickhouse = new ClickHouseConfig.Builder(this); + final RedisConfig.Builder redis = new RedisConfig.Builder(this); + final MinIOConfig.Builder minio = new MinIOConfig.Builder(this); + final WorkerConfig.Builder worker = new WorkerConfig.Builder(this); + + /** + * Returns the Langfuse web server config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the Langfuse config builder + */ + public LangfuseConfig.Builder langfuse() { + return langfuse; + } + + /** + * Returns the PostgreSQL config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the PostgreSQL config builder + */ + public PostgresConfig.Builder postgres() { + return postgres; + } + + /** + * Returns the ClickHouse config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the ClickHouse config builder + */ + public ClickHouseConfig.Builder clickhouse() { + return clickhouse; + } + + /** + * Returns the Redis config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the Redis config builder + */ + public RedisConfig.Builder redis() { + return redis; + } + + /** + * Returns the MinIO config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the MinIO config builder + */ + public MinIOConfig.Builder minio() { + return minio; + } + + /** + * Returns the Langfuse worker config builder for customization. + * Call {@code .and()} to return to this builder. + * + * @return the worker config builder + */ + public WorkerConfig.Builder worker() { + return worker; + } + + /** + * Builds the configuration. + * + * @return a new {@link LangfuseContainerConfig} + */ + public LangfuseContainerConfig build() { + return new DefaultLangfuseContainerConfig(this); + } + } +} \ No newline at end of file diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/MinIOConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/MinIOConfig.java new file mode 100644 index 0000000..dc6eea9 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/MinIOConfig.java @@ -0,0 +1,113 @@ +package com.langfuse.testcontainers.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * MinIO configuration. + * + * @author Eric Deandrea + */ +public interface MinIOConfig { + /** + * Default container image for MinIO. + */ + String DEFAULT_IMAGE = "cgr.dev/chainguard/minio"; + + /** + * @return the container image + */ + String image(); + + /** + * @return the MinIO root user + */ + String rootUser(); + + /** + * @return the MinIO root password + */ + String rootPassword(); + + /** + * @return the bucket name to create + */ + String bucketName(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link MinIOConfig}. + * + * @author Eric Deandrea + */ + final class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + String rootUser = "minio"; + String rootPassword = "miniosecret"; + String bucketName = "langfuse"; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value MinIOConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** @param rootUser the root user (default: {@code "minio"}) + * @return this builder */ + public Builder rootUser(String rootUser) { + this.rootUser = rootUser; + return this; + } + + /** @param rootPassword the root password (default: {@code "miniosecret"}) + * @return this builder */ + public Builder rootPassword(String rootPassword) { + this.rootPassword = rootPassword; + return this; + } + + /** @param bucketName the bucket name (default: {@code "langfuse"}) + * @return this builder */ + public Builder bucketName(String bucketName) { + this.bucketName = bucketName; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + MinIOConfig build() { + return new DefaultMinIOConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/PostgresConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/PostgresConfig.java new file mode 100644 index 0000000..3a7e6c6 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/PostgresConfig.java @@ -0,0 +1,113 @@ +package com.langfuse.testcontainers.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * PostgreSQL configuration. + * + * @author Eric Deandrea + */ +public interface PostgresConfig { + /** + * Default container image for PostgreSQL. + */ + String DEFAULT_IMAGE = "postgres:17"; + + /** + * @return the container image + */ + String image(); + + /** + * @return the database username + */ + String username(); + + /** + * @return the database password + */ + String password(); + + /** + * @return the database name + */ + String databaseName(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link PostgresConfig}. + * + * @author Eric Deandrea + */ + final class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + String username = "postgres"; + String password = "postgres"; + String databaseName = "postgres"; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value PostgresConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** @param username the username (default: {@code "postgres"}) + * @return this builder */ + public Builder username(String username) { + this.username = username; + return this; + } + + /** @param password the password (default: {@code "postgres"}) + * @return this builder */ + public Builder password(String password) { + this.password = password; + return this; + } + + /** @param databaseName the database name (default: {@code "postgres"}) + * @return this builder */ + public Builder databaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + PostgresConfig build() { + return new DefaultPostgresConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/RedisConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/RedisConfig.java new file mode 100644 index 0000000..769db3e --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/RedisConfig.java @@ -0,0 +1,100 @@ +package com.langfuse.testcontainers.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * Redis configuration. + * + * @author Eric Deandrea + */ +public interface RedisConfig { + /** + * Default container image for Redis. + */ + String DEFAULT_IMAGE = "redis:7"; + + /** + * @return the container image + */ + String image(); + + /** + * @return the Redis password + */ + String password(); + + /** + * @return whether TLS is enabled + */ + boolean tlsEnabled(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link RedisConfig}. + * + * @author Eric Deandrea + */ + final class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + String password = "myredissecret"; + boolean tlsEnabled = false; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value RedisConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** @param password the password (default: {@code "myredissecret"}) + * @return this builder */ + public Builder password(String password) { + this.password = password; + return this; + } + + /** @param tlsEnabled whether TLS is enabled (default: {@code false}) + * @return this builder */ + public Builder tlsEnabled(boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + RedisConfig build() { + return new DefaultRedisConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/WorkerConfig.java b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/WorkerConfig.java new file mode 100644 index 0000000..9c12309 --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/config/WorkerConfig.java @@ -0,0 +1,74 @@ +package com.langfuse.testcontainers.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * Langfuse worker configuration. + * + * @author Eric Deandrea + */ +public interface WorkerConfig { + /** + * Default container image for the Langfuse worker. + */ + String DEFAULT_IMAGE = "docker.io/langfuse/langfuse-worker:3"; + + /** + * @return the container image + */ + String image(); + + /** + * @return additional container environment variables + */ + Map containerEnv(); + + /** + * Builder for {@link WorkerConfig}. + * + * @author Eric Deandrea + */ + final class Builder { + private final LangfuseContainerConfig.Builder parent; + String image = DEFAULT_IMAGE; + final Map containerEnv = new HashMap<>(); + + Builder(LangfuseContainerConfig.Builder parent) { + this.parent = parent; + } + + /** + * @return the parent {@link LangfuseContainerConfig.Builder} + */ + public LangfuseContainerConfig.Builder and() { + return parent; + } + + /** @param image the container image (default: {@value WorkerConfig#DEFAULT_IMAGE}) + * @return this builder */ + public Builder image(String image) { + this.image = image; + return this; + } + + /** @param key the environment variable name + * @param value the environment variable value + * @return this builder */ + public Builder containerEnv(String key, String value) { + this.containerEnv.put(key, value); + return this; + } + + /** @param env a map of environment variable names to values + * @return this builder */ + public Builder containerEnv(Map env) { + this.containerEnv.putAll(env); + return this; + } + + WorkerConfig build() { + return new DefaultWorkerConfig(this); + } + } +} diff --git a/langfuse-java-testcontainers/src/test/java/com/langfuse/testcontainers/LangfuseContainerTest.java b/langfuse-java-testcontainers/src/test/java/com/langfuse/testcontainers/LangfuseContainerTest.java new file mode 100644 index 0000000..b4560f7 --- /dev/null +++ b/langfuse-java-testcontainers/src/test/java/com/langfuse/testcontainers/LangfuseContainerTest.java @@ -0,0 +1,71 @@ +package com.langfuse.testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Verifies that {@link LangfuseContainer} starts all services correctly + * and the Langfuse health endpoint is reachable. + * + * @author Eric Deandrea + */ +@Testcontainers +class LangfuseContainerTest { + + @Container + static final LangfuseContainer langfuse = new LangfuseContainer(); + + @Test + void containerIsRunning() { + assertThat(langfuse.isRunning()) + .isTrue(); + } + + @Test + void langfuseUrlIsAvailable() { + assertThat(langfuse.getLangfuseUrl()) + .isNotBlank() + .startsWith("http://"); + } + + @Test + void publicKeyMatchesDefault() { + assertThat(langfuse.getPublicKey()) + .isEqualTo("pk-lf-dev"); + } + + @Test + void secretKeyMatchesDefault() { + assertThat(langfuse.getSecretKey()) + .isEqualTo("sk-lf-dev"); + } + + @Test + void healthEndpointReturnsOk() throws Exception { + var client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + + var request = HttpRequest.newBuilder() + .uri(URI.create(langfuse.getLangfuseUrl() + "/api/public/health")) + .GET() + .build(); + + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + assertThat(response.statusCode()) + .isEqualTo(200); + + assertThat(response.body()) + .contains("\"status\"") + .contains("\"version\""); + } +} diff --git a/mvnw b/mvnw index 3a28521..bd8896b 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : -exec_maven "$@" \ No newline at end of file +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 5679db1..92450f9 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { @@ -146,4 +186,4 @@ try { catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" \ No newline at end of file +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..4a6470b --- /dev/null +++ b/openapi.yml @@ -0,0 +1,13731 @@ +openapi: 3.0.1 +info: + title: langfuse + version: '' + description: >- + ## Authentication + + + Authenticate with the API using [Basic + Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API + keys in the project settings: + + + - username: Langfuse Public Key + + - password: Langfuse Secret Key + + + ## Exports + + + - OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml +paths: + /api/public/annotation-queues: + get: + description: Get all annotation queues + operationId: annotationQueues_listQueues + tags: + - AnnotationQueues + parameters: + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedAnnotationQueues' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: Create an annotation queue + operationId: annotationQueues_createQueue + tags: + - AnnotationQueues + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueue' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAnnotationQueueRequest' + /api/public/annotation-queues/{queueId}: + get: + description: Get an annotation queue by ID + operationId: annotationQueues_getQueue + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueue' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/annotation-queues/{queueId}/items: + get: + description: Get items for a specific annotation queue + operationId: annotationQueues_listQueueItems + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + - name: status + in: query + description: Filter by status + required: false + schema: + $ref: '#/components/schemas/AnnotationQueueStatus' + nullable: true + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedAnnotationQueueItems' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: Add an item to an annotation queue + operationId: annotationQueues_createQueueItem + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueueItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAnnotationQueueItemRequest' + /api/public/annotation-queues/{queueId}/items/{itemId}: + get: + description: Get a specific item from an annotation queue + operationId: annotationQueues_getQueueItem + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + - name: itemId + in: path + description: The unique identifier of the annotation queue item + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueueItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + patch: + description: Update an annotation queue item + operationId: annotationQueues_updateQueueItem + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + - name: itemId + in: path + description: The unique identifier of the annotation queue item + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueueItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateAnnotationQueueItemRequest' + delete: + description: Remove an item from an annotation queue + operationId: annotationQueues_deleteQueueItem + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + - name: itemId + in: path + description: The unique identifier of the annotation queue item + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteAnnotationQueueItemResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/annotation-queues/{queueId}/assignments: + post: + description: Create an assignment for a user to an annotation queue + operationId: annotationQueues_createQueueAssignment + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAnnotationQueueAssignmentResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueueAssignmentRequest' + delete: + description: Delete an assignment for a user to an annotation queue + operationId: annotationQueues_deleteQueueAssignment + tags: + - AnnotationQueues + parameters: + - name: queueId + in: path + description: The unique identifier of the annotation queue + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteAnnotationQueueAssignmentResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AnnotationQueueAssignmentRequest' + /api/public/integrations/blob-storage: + get: + description: >- + Get all blob storage integrations for the organization (requires + organization-scoped API key) + operationId: blobStorageIntegrations_getBlobStorageIntegrations + tags: + - BlobStorageIntegrations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/BlobStorageIntegrationsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + put: + description: >- + Create or update a blob storage integration for a specific project + (requires organization-scoped API key). The configuration is validated + by performing a test upload to the bucket. + operationId: blobStorageIntegrations_upsertBlobStorageIntegration + tags: + - BlobStorageIntegrations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/BlobStorageIntegrationResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBlobStorageIntegrationRequest' + /api/public/integrations/blob-storage/{id}: + get: + description: >- + Get the sync status of a blob storage integration by integration ID + (requires organization-scoped API key) + operationId: blobStorageIntegrations_getBlobStorageIntegrationStatus + tags: + - BlobStorageIntegrations + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/BlobStorageIntegrationStatusResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: >- + Delete a blob storage integration by ID (requires organization-scoped + API key) + operationId: blobStorageIntegrations_deleteBlobStorageIntegration + tags: + - BlobStorageIntegrations + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/BlobStorageIntegrationDeletionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/comments: + post: + description: >- + Create a comment. Comments may be attached to different object types + (trace, observation, session, prompt). + operationId: comments_create + tags: + - Comments + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCommentResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCommentRequest' + get: + description: Get all comments + operationId: comments_get + tags: + - Comments + parameters: + - name: page + in: query + description: Page number, starts at 1. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. If you encounter api issues due to too + large page sizes, try to reduce the limit + required: false + schema: + type: integer + nullable: true + - name: objectType + in: query + description: >- + Filter comments by object type (trace, observation, session, + prompt). + required: false + schema: + type: string + nullable: true + - name: objectId + in: query + description: >- + Filter comments by object id. If objectType is not provided, an + error will be thrown. + required: false + schema: + type: string + nullable: true + - name: authorUserId + in: query + description: Filter comments by author user id. + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GetCommentsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/comments/{commentId}: + get: + description: Get a comment by id + operationId: comments_get-by-id + tags: + - Comments + parameters: + - name: commentId + in: path + description: The unique langfuse identifier of a comment + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/dataset-items: + post: + description: Create a dataset item + operationId: datasetItems_create + tags: + - DatasetItems + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDatasetItemRequest' + get: + description: >- + Get dataset items. Optionally specify a version to get the items as they + existed at that point in time. + + Note: If version parameter is provided, datasetName must also be + provided. + operationId: datasetItems_list + tags: + - DatasetItems + parameters: + - name: datasetName + in: query + required: false + schema: + type: string + nullable: true + - name: sourceTraceId + in: query + required: false + schema: + type: string + nullable: true + - name: sourceObservationId + in: query + required: false + schema: + type: string + nullable: true + - name: version + in: query + description: >- + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., + "2026-01-21T14:35:42Z"). + + If provided, returns state of dataset at this timestamp. + + If not provided, returns the latest version. Requires datasetName to + be specified. + required: false + schema: + type: string + format: date-time + nullable: true + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDatasetItems' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/dataset-items/{id}: + get: + description: Get a dataset item + operationId: datasetItems_get + tags: + - DatasetItems + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: >- + Delete a dataset item and all its run items. This action is + irreversible. + operationId: datasetItems_delete + tags: + - DatasetItems + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteDatasetItemResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/dataset-run-items: + post: + description: Create a dataset run item + operationId: datasetRunItems_create + tags: + - DatasetRunItems + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetRunItem' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDatasetRunItemRequest' + get: + description: List dataset run items + operationId: datasetRunItems_list + tags: + - DatasetRunItems + parameters: + - name: datasetId + in: query + required: true + schema: + type: string + - name: runName + in: query + required: true + schema: + type: string + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDatasetRunItems' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/v2/datasets: + get: + description: Get all datasets + operationId: datasets_list + tags: + - Datasets + parameters: + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDatasets' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: Create a dataset + operationId: datasets_create + tags: + - Datasets + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDatasetRequest' + /api/public/v2/datasets/{datasetName}: + get: + description: Get a dataset + operationId: datasets_get + tags: + - Datasets + parameters: + - name: datasetName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/datasets/{datasetName}/runs/{runName}: + get: + description: Get a dataset run and its items + operationId: datasets_getRun + tags: + - Datasets + parameters: + - name: datasetName + in: path + required: true + schema: + type: string + - name: runName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetRunWithItems' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: Delete a dataset run and all its run items. This action is irreversible. + operationId: datasets_deleteRun + tags: + - Datasets + parameters: + - name: datasetName + in: path + required: true + schema: + type: string + - name: runName + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteDatasetRunResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/datasets/{datasetName}/runs: + get: + description: Get dataset runs + operationId: datasets_getRuns + tags: + - Datasets + parameters: + - name: datasetName + in: path + required: true + schema: + type: string + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDatasetRuns' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/health: + get: + description: Check health of API and database + operationId: health_health + tags: + - Health + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '503': + description: '' + /api/public/ingestion: + post: + description: >- + **Legacy endpoint for batch ingestion for Langfuse Observability.** + + + -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). + Learn more: https://langfuse.com/integrations/native/opentelemetry + + + Within each batch, there can be multiple events. + + Each event has a type, an id, a timestamp, metadata and a body. + + Internally, we refer to this as the "event envelope" as it tells us + something about the event but not the trace. + + We use the event id within this envelope to deduplicate messages to + avoid processing the same event twice, i.e. the event id should be + unique per request. + + The event.body.id is the ID of the actual trace and will be used for + updates and will be visible within the Langfuse App. + + I.e. if you want to update a trace, you'd use the same body id, but + separate event IDs. + + + Notes: + + - Introduction to data model: + https://langfuse.com/docs/observability/data-model + + - Batch sizes are limited to 3.5 MB in total. You need to adjust the + number of events per batch accordingly. + + - The API does not return a 4xx status code for input errors. Instead, + it responds with a 207 status code, which includes a list of the + encountered errors. + operationId: ingestion_batch + tags: + - Ingestion + parameters: [] + responses: + '207': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/IngestionResponse' + examples: + Example1: + value: + successes: + - id: abcdef-1234-5678-90ab + status: 201 + errors: [] + Example2: + value: + successes: + - id: abcdef-1234-5678-90ab + status: 201 + errors: [] + Example3: + value: + successes: + - id: abcdef-1234-5678-90ab + status: 201 + errors: [] + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + batch: + type: array + items: + $ref: '#/components/schemas/IngestionEvent' + description: >- + Batch of tracing events to be ingested. Discriminated by + attribute `type`. + metadata: + nullable: true + description: >- + Optional. Metadata field used by the Langfuse SDKs for + debugging. + required: + - batch + examples: + Example1: + value: + batch: + - id: abcdef-1234-5678-90ab + timestamp: '2022-01-01T00:00:00.000Z' + type: trace-create + body: + id: abcdef-1234-5678-90ab + timestamp: '2022-01-01T00:00:00.000Z' + environment: production + name: My Trace + userId: 1234-5678-90ab-cdef + input: My input + output: My output + sessionId: 1234-5678-90ab-cdef + release: 1.0.0 + version: 1.0.0 + metadata: My metadata + tags: + - tag1 + - tag2 + public: true + Example2: + value: + batch: + - id: abcdef-1234-5678-90ab + timestamp: '2022-01-01T00:00:00.000Z' + type: span-create + body: + id: abcdef-1234-5678-90ab + traceId: 1234-5678-90ab-cdef + startTime: '2022-01-01T00:00:00.000Z' + environment: test + Example3: + value: + batch: + - id: abcdef-1234-5678-90ab + timestamp: '2022-01-01T00:00:00.000Z' + type: score-create + body: + id: abcdef-1234-5678-90ab + traceId: 1234-5678-90ab-cdef + name: My Score + value: 0.9 + environment: default + /api/public/metrics: + get: + description: >- + Get metrics from the Langfuse project using a query object. + + + Consider using the [v2 metrics + endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for + better performance. + + + For more details, see the [Metrics API + documentation](https://langfuse.com/docs/metrics/features/metrics-api). + operationId: legacy_metricsV1_metrics + tags: + - LegacyMetricsV1 + parameters: + - name: query + in: query + description: >- + JSON string containing the query parameters with the following + structure: + + ```json + + { + "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by, e.g. "name", "userId", "sessionId" + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure, e.g. "count", "latency", "value" + "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on + "operator": string, // Operator, e.g. "=", ">", "<", "contains" + "value": any, // Value to compare against + "type": string, // Data type, e.g. "string", "number", "stringObject" + "key": string // Required only when filtering on metadata + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 + "row_limit": number // Optional. Row limit for results (1-1000) + } + } + + ``` + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/legacyMetricsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/observations/{observationId}: + get: + description: Get a observation + operationId: legacy_observationsV1_get + tags: + - LegacyObservationsV1 + parameters: + - name: observationId + in: path + description: >- + The unique langfuse identifier of an observation, can be an event, + span or generation + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ObservationsView' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/observations: + get: + description: >- + Get a list of observations. + + + Consider using the [v2 observations + endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) + for cursor-based pagination and field selection. + operationId: legacy_observationsV1_getMany + tags: + - LegacyObservationsV1 + parameters: + - name: page + in: query + description: Page number, starts at 1. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. If you encounter api issues due to too + large page sizes, try to reduce the limit. + required: false + schema: + type: integer + nullable: true + - name: name + in: query + required: false + schema: + type: string + nullable: true + - name: userId + in: query + required: false + schema: + type: string + nullable: true + - name: type + in: query + required: false + schema: + type: string + nullable: true + - name: traceId + in: query + required: false + schema: + type: string + nullable: true + - name: level + in: query + description: >- + Optional filter for observations with a specific level (e.g. + "DEBUG", "DEFAULT", "WARNING", "ERROR"). + required: false + schema: + $ref: '#/components/schemas/ObservationLevel' + nullable: true + - name: parentObservationId + in: query + required: false + schema: + type: string + nullable: true + - name: environment + in: query + description: >- + Optional filter for observations where the environment is one of the + provided values. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: fromStartTime + in: query + description: >- + Retrieve only observations with a start_time on or after this + datetime (ISO 8601). + required: false + schema: + type: string + format: date-time + nullable: true + - name: toStartTime + in: query + description: >- + Retrieve only observations with a start_time before this datetime + (ISO 8601). + required: false + schema: + type: string + format: date-time + nullable: true + - name: version + in: query + description: Optional filter to only include observations with a certain version. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: >- + JSON string containing an array of filter conditions. When provided, + this takes precedence over query parameter filters (userId, name, + type, level, environment, fromStartTime, ...). + + + ## Filter Structure + + Each filter condition has the following structure: + + ```json + + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + + ``` + + + ## Available Columns + + + ### Core Observation Fields + + - `id` (string) - Observation ID + + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + + - `name` (string) - Observation name + + - `traceId` (string) - Associated trace ID + + - `startTime` (datetime) - Observation start time + + - `endTime` (datetime) - Observation end time + + - `environment` (string) - Environment tag + + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + + - `statusMessage` (string) - Status message + + - `version` (string) - Version tag + + + ### Performance Metrics + + - `latency` (number) - Latency in seconds (calculated: end_time - + start_time) + + - `timeToFirstToken` (number) - Time to first token in seconds + + - `tokensPerSecond` (number) - Output tokens per second + + + ### Token Usage + + - `inputTokens` (number) - Number of input tokens + + - `outputTokens` (number) - Number of output tokens + + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + + ### Cost Metrics + + - `inputCost` (number) - Input cost in USD + + - `outputCost` (number) - Output cost in USD + + - `totalCost` (number) - Total cost in USD + + + ### Model Information + + - `model` (string) - Provided model name + + - `promptName` (string) - Associated prompt name + + - `promptVersion` (number) - Associated prompt version + + + ### Structured Data + + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata + key-value pairs. Use `key` parameter to filter on specific metadata + keys. + + + ### Associated Trace Fields (requires join with traces table) + + - `userId` (string) - User ID from associated trace + + - `traceName` (string) - Name from associated trace + + - `traceEnvironment` (string) - Environment from associated trace + + - `traceTags` (arrayOptions) - Tags from associated trace + + + ## Filter Examples + + ```json + + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + + ``` + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/legacyObservationsViews' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/scores: + post: + description: Create a score (supports both trace and session scores) + operationId: legacy_scoreV1_create + tags: + - LegacyScoreV1 + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/legacyCreateScoreResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/legacyCreateScoreRequest' + /api/public/scores/{scoreId}: + delete: + description: Delete a score (supports both trace and session scores) + operationId: legacy_scoreV1_delete + tags: + - LegacyScoreV1 + parameters: + - name: scoreId + in: path + description: The unique langfuse identifier of a score + required: true + schema: + type: string + responses: + '204': + description: '' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/llm-connections: + get: + description: Get all LLM connections in a project + operationId: llmConnections_list + tags: + - LlmConnections + parameters: + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedLlmConnections' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + put: + description: >- + Create or update an LLM connection. The connection is upserted on + provider. + operationId: llmConnections_upsert + tags: + - LlmConnections + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/LlmConnection' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpsertLlmConnectionRequest' + /api/public/llm-connections/{id}: + delete: + description: >- + Delete an LLM connection by id. Evaluators that depend on the deleted + connection are automatically paused. + operationId: llmConnections_delete + tags: + - LlmConnections + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteLlmConnectionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/media/{mediaId}: + get: + description: Get a media record + operationId: media_get + tags: + - Media + parameters: + - name: mediaId + in: path + description: The unique langfuse identifier of a media record + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GetMediaResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + patch: + description: Patch a media record + operationId: media_patch + tags: + - Media + parameters: + - name: mediaId + in: path + description: The unique langfuse identifier of a media record + required: true + schema: + type: string + responses: + '204': + description: '' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PatchMediaBody' + /api/public/media: + post: + description: Get a presigned upload URL for a media record + operationId: media_getUploadUrl + tags: + - Media + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GetMediaUploadUrlResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetMediaUploadUrlRequest' + /api/public/v2/metrics: + get: + description: >- + Get metrics from the Langfuse project using a query object. V2 endpoint + with optimized performance. + + + ## V2 Differences + + - Supports `observations`, `scores-numeric`, and `scores-categorical` + views only (traces view not supported) + + - Direct access to tags and release fields on observations + + - Backwards-compatible: traceName, traceRelease, traceVersion dimensions + are still available on observations view + + - High cardinality dimensions are not supported and will return a 400 + error (see below) + + + For more details, see the [Metrics API + documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + + ## Available Views + + + ### observations + + Query observation-level data (spans, generations, events). + + + **Dimensions:** + + - `environment` - Deployment environment (e.g., production, staging) + + - `type` - Type of observation (SPAN, GENERATION, EVENT) + + - `name` - Name of the observation + + - `level` - Logging level of the observation + + - `version` - Version of the observation + + - `tags` - User-defined tags + + - `release` - Release version + + - `traceName` - Name of the parent trace (backwards-compatible) + + - `traceRelease` - Release version of the parent trace + (backwards-compatible, maps to release) + + - `traceVersion` - Version of the parent trace (backwards-compatible, + maps to version) + + - `providedModelName` - Name of the model used + + - `promptName` - Name of the prompt used + + - `promptVersion` - Version of the prompt used + + - `startTimeMonth` - Month of start_time in YYYY-MM format + + + **Measures:** + + - `count` - Total number of observations + + - `latency` - Observation latency (milliseconds) + + - `streamingLatency` - Generation latency from completion start to end + (milliseconds) + + - `inputTokens` - Sum of input tokens consumed + + - `outputTokens` - Sum of output tokens produced + + - `totalTokens` - Sum of all tokens consumed + + - `outputTokensPerSecond` - Output tokens per second + + - `tokensPerSecond` - Total tokens per second + + - `inputCost` - Input cost (USD) + + - `outputCost` - Output cost (USD) + + - `totalCost` - Total cost (USD) + + - `timeToFirstToken` - Time to first token (milliseconds) + + - `countScores` - Number of scores attached to the observation + + + ### scores-numeric + + Query numeric and boolean score data. + + + **Dimensions:** + + - `environment` - Deployment environment + + - `name` - Name of the score (e.g., accuracy, toxicity) + + - `source` - Origin of the score (API, ANNOTATION, EVAL) + + - `dataType` - Data type (NUMERIC, BOOLEAN) + + - `configId` - Identifier of the score config + + - `timestampMonth` - Month in YYYY-MM format + + - `timestampDay` - Day in YYYY-MM-DD format + + - `value` - Numeric value of the score + + - `traceName` - Name of the parent trace + + - `tags` - Tags + + - `traceRelease` - Release version + + - `traceVersion` - Version + + - `observationName` - Name of the associated observation + + - `observationModelName` - Model name of the associated observation + + - `observationPromptName` - Prompt name of the associated observation + + - `observationPromptVersion` - Prompt version of the associated + observation + + + **Measures:** + + - `count` - Total number of scores + + - `value` - Score value (for aggregations) + + + ### scores-categorical + + Query categorical score data. Same dimensions as scores-numeric except + uses `stringValue` instead of `value`. + + + **Measures:** + + - `count` - Total number of scores + + + ## High Cardinality Dimensions + + The following dimensions cannot be used as grouping dimensions in v2 + metrics API as they can cause performance issues. + + Use them in filters instead. + + + **observations view:** + + - `id` - Use traceId filter to narrow down results + + - `traceId` - Use traceId filter instead + + - `userId` - Use userId filter instead + + - `sessionId` - Use sessionId filter instead + + - `parentObservationId` - Use parentObservationId filter instead + + + **scores-numeric / scores-categorical views:** + + - `id` - Use specific filters to narrow down results + + - `traceId` - Use traceId filter instead + + - `userId` - Use userId filter instead + + - `sessionId` - Use sessionId filter instead + + - `observationId` - Use observationId filter instead + + + ## Aggregations + + Available aggregation functions: `sum`, `avg`, `count`, `max`, `min`, + `p50`, `p75`, `p90`, `p95`, `p99`, `histogram` + + + ## Time Granularities + + Available granularities for timeDimension: `auto`, `minute`, `hour`, + `day`, `week`, `month` + + - `auto` bins the data into approximately 50 buckets based on the time + range + operationId: metrics_metrics + tags: + - Metrics + parameters: + - name: query + in: query + description: >- + JSON string containing the query parameters with the following + structure: + + ```json + + { + "view": string, // Required. One of "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by (see available dimensions above) + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure (see available measures above) + "aggregation": string // How to aggregate: "sum", "avg", "count", "max", "min", "p50", "p75", "p90", "p95", "p99", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on (any dimension field) + "operator": string, // Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject/numberObject: same as string/number with required "key" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Value to compare against + "type": string, // Data type: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "key": string // Required only for stringObject/numberObject types (e.g., metadata filtering) + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "auto", "minute", "hour", "day", "week", "month" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range (must be after fromTimestamp) + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by (dimension or metric alias) + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram aggregation (1-100), default: 10 + "row_limit": number // Optional. Maximum number of rows to return (1-1000), default: 100 + } + } + + ``` + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MetricsV2Response' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/models: + post: + description: Create a model + operationId: models_create + tags: + - Models + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateModelRequest' + get: + description: Get all models + operationId: models_list + tags: + - Models + parameters: + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedModels' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/models/{id}: + get: + description: Get a model + operationId: models_get + tags: + - Models + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: >- + Delete a model. Cannot delete models managed by Langfuse. You can create + your own definition with the same modelName to override the definition + though. + operationId: models_delete + tags: + - Models + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '204': + description: '' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/v2/observations: + get: + description: >- + Get a list of observations with cursor-based pagination and flexible + field selection. + + + ## Cursor-based Pagination + + This endpoint uses cursor-based pagination for efficient traversal of + large datasets. + + The cursor is returned in the response metadata and should be passed in + subsequent requests + + to retrieve the next page of results. + + + ## Field Selection + + Use the `fields` parameter to control which observation fields are + returned: + + - `core` - Always included: id, traceId, startTime, endTime, projectId, + parentObservationId, type + + - `basic` - name, level, statusMessage, version, environment, + bookmarked, public, userId, sessionId + + - `time` - completionStartTime, createdAt, updatedAt + + - `io` - input, output + + - `metadata` - metadata (truncated to 200 chars by default, use + `expandMetadata` to get full values) + + - `model` - providedModelName, internalModelId, modelParameters + + - `usage` - usageDetails, costDetails, totalCost, usagePricingTierName + + - `prompt` - promptId, promptName, promptVersion + + - `metrics` - latency, timeToFirstToken + + - `trace_context` - tags, release, traceName + + + If not specified, `core` and `basic` field groups are returned. + + + ## Filters + + Multiple filtering options are available via query parameters or the + structured `filter` parameter. + + When using the `filter` parameter, it takes precedence over individual + query parameter filters. + operationId: observations_getMany + tags: + - Observations + parameters: + - name: fields + in: query + description: >- + Comma-separated list of field groups to include in the response. + + Available groups: core, basic, time, io, metadata, model, usage, + prompt, metrics, trace_context. + + If not specified, `core` and `basic` field groups are returned. + + Example: "basic,usage,model" + required: false + schema: + type: string + nullable: true + - name: expandMetadata + in: query + description: |- + Comma-separated list of metadata keys to return non-truncated. + By default, metadata values over 200 characters are truncated. + Use this parameter to retrieve full values for specific keys. + Example: "key1,key2" + required: false + schema: + type: string + nullable: true + - name: limit + in: query + description: Number of items to return per page. Maximum 1000, default 50. + required: false + schema: + type: integer + nullable: true + - name: cursor + in: query + description: >- + Base64-encoded cursor for pagination. Use the cursor from the + previous response to get the next page. + required: false + schema: + type: string + nullable: true + - name: parseIoAsJson + in: query + description: |- + **Deprecated.** Setting this to `true` will return a 400 error. + Input/output fields are always returned as raw strings. + Remove this parameter or set it to `false`. + required: false + schema: + type: boolean + nullable: true + - name: name + in: query + required: false + schema: + type: string + nullable: true + - name: userId + in: query + required: false + schema: + type: string + nullable: true + - name: type + in: query + description: >- + Filter by observation type (e.g., "GENERATION", "SPAN", "EVENT", + "AGENT", "TOOL", "CHAIN", "RETRIEVER", "EVALUATOR", "EMBEDDING", + "GUARDRAIL") + required: false + schema: + type: string + nullable: true + - name: traceId + in: query + required: false + schema: + type: string + nullable: true + - name: level + in: query + description: >- + Optional filter for observations with a specific level (e.g. + "DEBUG", "DEFAULT", "WARNING", "ERROR"). + required: false + schema: + $ref: '#/components/schemas/ObservationLevel' + nullable: true + - name: parentObservationId + in: query + required: false + schema: + type: string + nullable: true + - name: environment + in: query + description: >- + Optional filter for observations where the environment is one of the + provided values. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: fromStartTime + in: query + description: >- + Retrieve only observations with a start_time on or after this + datetime (ISO 8601). + required: false + schema: + type: string + format: date-time + nullable: true + - name: toStartTime + in: query + description: >- + Retrieve only observations with a start_time before this datetime + (ISO 8601). + required: false + schema: + type: string + format: date-time + nullable: true + - name: version + in: query + description: Optional filter to only include observations with a certain version. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: >- + JSON string containing an array of filter conditions. When provided, + this takes precedence over query parameter filters (userId, name, + type, level, environment, fromStartTime, ...). + + + ## Filter Structure + + Each filter condition has the following structure: + + ```json + + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + + ``` + + + ## Available Columns + + + ### Core Observation Fields + + - `id` (string) - Observation ID + + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + + - `name` (string) - Observation name + + - `traceId` (string) - Associated trace ID + + - `startTime` (datetime) - Observation start time + + - `endTime` (datetime) - Observation end time + + - `environment` (string) - Environment tag + + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + + - `statusMessage` (string) - Status message + + - `version` (string) - Version tag + + - `userId` (string) - User ID + + - `sessionId` (string) - Session ID + + + ### Trace-Related Fields + + - `traceName` (string) - Name of the parent trace + + - `traceTags` (arrayOptions) - Tags from the parent trace + + - `tags` (arrayOptions) - Alias for traceTags + + + ### Performance Metrics + + - `latency` (number) - Latency in seconds (calculated: end_time - + start_time) + + - `timeToFirstToken` (number) - Time to first token in seconds + + - `tokensPerSecond` (number) - Output tokens per second + + + ### Token Usage + + - `inputTokens` (number) - Number of input tokens + + - `outputTokens` (number) - Number of output tokens + + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + + ### Cost Metrics + + - `inputCost` (number) - Input cost in USD + + - `outputCost` (number) - Output cost in USD + + - `totalCost` (number) - Total cost in USD + + + ### Model Information + + - `model` (string) - Provided model name (alias: + `providedModelName`) + + - `promptName` (string) - Associated prompt name + + - `promptVersion` (number) - Associated prompt version + + + ### Structured Data + + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata + key-value pairs. Use `key` parameter to filter on specific metadata + keys. + + + ## Filter Examples + + ```json + + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + + ``` + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ObservationsV2Response' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/otel/v1/traces: + post: + description: >- + **OpenTelemetry Traces Ingestion Endpoint** + + + This endpoint implements the OTLP/HTTP specification for trace + ingestion, providing native OpenTelemetry integration for Langfuse + Observability. + + + **Supported Formats:** + + - Binary Protobuf: `Content-Type: application/x-protobuf` + + - JSON Protobuf: `Content-Type: application/json` + + - Supports gzip compression via `Content-Encoding: gzip` header + + + **Specification Compliance:** + + - Conforms to [OTLP/HTTP Trace + Export](https://opentelemetry.io/docs/specs/otlp/#otlphttp) + + - Implements `ExportTraceServiceRequest` message format + + + **Documentation:** + + - Integration guide: + https://langfuse.com/integrations/native/opentelemetry + + - Data model: https://langfuse.com/docs/observability/data-model + operationId: opentelemetry_exportTraces + tags: + - Opentelemetry + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/OtelTraceResponse' + examples: + BasicTraceExport: + value: {} + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + resourceSpans: + type: array + items: + $ref: '#/components/schemas/OtelResourceSpan' + description: >- + Array of resource spans containing trace data as defined in + the OTLP specification + required: + - resourceSpans + examples: + BasicTraceExport: + value: + resourceSpans: + - resource: + attributes: + - key: service.name + value: + stringValue: my-service + - key: service.version + value: + stringValue: 1.0.0 + scopeSpans: + - scope: + name: langfuse-sdk + version: 2.60.3 + spans: + - traceId: 0123456789abcdef0123456789abcdef + spanId: 0123456789abcdef + name: my-operation + kind: 1 + startTimeUnixNano: '1747872000000000000' + endTimeUnixNano: '1747872001000000000' + attributes: + - key: langfuse.observation.type + value: + stringValue: generation + status: {} + /api/public/organizations/memberships: + get: + description: >- + Get all memberships for the organization associated with the API key + (requires organization-scoped API key) + operationId: organizations_getOrganizationMemberships + tags: + - Organizations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + put: + description: >- + Create or update a membership for the organization associated with the + API key (requires organization-scoped API key) + operationId: organizations_updateOrganizationMembership + tags: + - Organizations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipRequest' + delete: + description: >- + Delete a membership from the organization associated with the API key + (requires organization-scoped API key) + operationId: organizations_deleteOrganizationMembership + tags: + - Organizations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipDeletionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteMembershipRequest' + /api/public/projects/{projectId}/memberships: + get: + description: >- + Get all memberships for a specific project (requires organization-scoped + API key) + operationId: organizations_getProjectMemberships + tags: + - Organizations + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + put: + description: >- + Create or update a membership for a specific project (requires + organization-scoped API key). The user must already be a member of the + organization. + operationId: organizations_updateProjectMembership + tags: + - Organizations + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipRequest' + delete: + description: >- + Delete a membership from a specific project (requires + organization-scoped API key). The user must be a member of the + organization. + operationId: organizations_deleteProjectMembership + tags: + - Organizations + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipDeletionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteMembershipRequest' + /api/public/organizations/projects: + get: + description: >- + Get all projects for the organization associated with the API key + (requires organization-scoped API key) + operationId: organizations_getOrganizationProjects + tags: + - Organizations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/OrganizationProjectsResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/organizations/apiKeys: + get: + description: >- + Get all API keys for the organization associated with the API key + (requires organization-scoped API key) + operationId: organizations_getOrganizationApiKeys + tags: + - Organizations + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/OrganizationApiKeysResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/projects: + get: + description: >- + Get Project associated with API key (requires project-scoped API key). + You can use GET /api/public/organizations/projects to get all projects + with an organization-scoped key. + operationId: projects_get + tags: + - Projects + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Projects' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: Create a new project (requires organization-scoped API key) + operationId: projects_create + tags: + - Projects + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + metadata: + type: object + additionalProperties: true + nullable: true + description: Optional metadata for the project + retention: + type: integer + description: >- + Number of days to retain data. Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. + required: + - name + - retention + /api/public/projects/{projectId}: + put: + description: Update a project by ID (requires organization-scoped API key). + operationId: projects_update + tags: + - Projects + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + metadata: + type: object + additionalProperties: true + nullable: true + description: Optional metadata for the project + retention: + type: integer + nullable: true + description: |- + Number of days to retain data. + Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. Will retain existing retention setting if omitted. + required: + - name + delete: + description: >- + Delete a project by ID (requires organization-scoped API key). Project + deletion is processed asynchronously. + operationId: projects_delete + tags: + - Projects + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '202': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectDeletionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/projects/{projectId}/apiKeys: + get: + description: Get all API keys for a project (requires organization-scoped API key) + operationId: projects_getApiKeys + tags: + - Projects + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyList' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: >- + Create a new API key for a project (requires organization-scoped API + key) + operationId: projects_createApiKey + tags: + - Projects + parameters: + - name: projectId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + note: + type: string + nullable: true + description: Optional note for the API key + publicKey: + type: string + nullable: true + description: >- + Optional predefined public key. Must start with 'pk-lf-'. If + provided, secretKey must also be provided. + secretKey: + type: string + nullable: true + description: >- + Optional predefined secret key. Must start with 'sk-lf-'. If + provided, publicKey must also be provided. + /api/public/projects/{projectId}/apiKeys/{apiKeyId}: + delete: + description: Delete an API key for a project (requires organization-scoped API key) + operationId: projects_deleteApiKey + tags: + - Projects + parameters: + - name: projectId + in: path + required: true + schema: + type: string + - name: apiKeyId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyDeletionResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/v2/prompts/{name}/versions/{version}: + patch: + description: Update labels for a specific prompt version + operationId: promptVersion_update + tags: + - PromptVersion + parameters: + - name: name + in: path + description: >- + The name of the prompt. If the prompt is in a folder (e.g., + "folder/subfolder/prompt-name"), + + the folder path must be URL encoded. + required: true + schema: + type: string + - name: version + in: path + description: Version of the prompt to update + required: true + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + newLabels: + type: array + items: + type: string + description: >- + New labels for the prompt version. Labels are unique across + versions. The "latest" label is reserved and managed by + Langfuse. + required: + - newLabels + /api/public/v2/prompts/{promptName}: + get: + description: Get a prompt + operationId: prompts_get + tags: + - Prompts + parameters: + - name: promptName + in: path + description: >- + The name of the prompt. If the prompt is in a folder (e.g., + "folder/subfolder/prompt-name"), + + the folder path must be URL encoded. + required: true + schema: + type: string + - name: version + in: query + description: Version of the prompt to be retrieved. + required: false + schema: + type: integer + nullable: true + - name: label + in: query + description: >- + Label of the prompt to be retrieved. Defaults to "production" if no + label or version is set. + required: false + schema: + type: string + nullable: true + - name: resolve + in: query + description: >- + Resolve prompt dependencies before returning the prompt. Defaults to + `true`. Set to `false` to return the raw stored prompt with + dependency tags intact. This bypasses prompt caching and is intended + for debugging or one-off jobs, not production runtime fetches. + required: false + schema: + type: boolean + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: >- + Delete prompt versions. If neither version nor label is specified, all + versions of the prompt are deleted. + operationId: prompts_delete + tags: + - Prompts + parameters: + - name: promptName + in: path + description: The name of the prompt + required: true + schema: + type: string + - name: label + in: query + description: >- + Optional label to filter deletion. If specified, deletes all prompt + versions that have this label. + required: false + schema: + type: string + nullable: true + - name: version + in: query + description: >- + Optional version to filter deletion. If specified, deletes only this + specific version of the prompt. + required: false + schema: + type: integer + nullable: true + responses: + '204': + description: '' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/v2/prompts: + get: + description: Get a list of prompt names with versions and labels + operationId: prompts_list + tags: + - Prompts + parameters: + - name: name + in: query + required: false + schema: + type: string + nullable: true + - name: label + in: query + required: false + schema: + type: string + nullable: true + - name: tag + in: query + required: false + schema: + type: string + nullable: true + - name: page + in: query + description: page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: limit of items per page + required: false + schema: + type: integer + nullable: true + - name: fromUpdatedAt + in: query + description: >- + Optional filter to only include prompt versions created/updated on + or after a certain datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: toUpdatedAt + in: query + description: >- + Optional filter to only include prompt versions created/updated + before a certain datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PromptMetaListResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: Create a new version for the prompt with the given `name` + operationId: prompts_create + tags: + - Prompts + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Prompt' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePromptRequest' + /api/public/scim/ServiceProviderConfig: + get: + description: >- + Get SCIM Service Provider Configuration (requires organization-scoped + API key) + operationId: scim_getServiceProviderConfig + tags: + - Scim + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ServiceProviderConfig' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/scim/ResourceTypes: + get: + description: Get SCIM Resource Types (requires organization-scoped API key) + operationId: scim_getResourceTypes + tags: + - Scim + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceTypesResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/scim/Schemas: + get: + description: Get SCIM Schemas (requires organization-scoped API key) + operationId: scim_getSchemas + tags: + - Scim + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/SchemasResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/scim/Users: + get: + description: List users in the organization (requires organization-scoped API key) + operationId: scim_listUsers + tags: + - Scim + parameters: + - name: filter + in: query + description: Filter expression (e.g. userName eq "value") + required: false + schema: + type: string + nullable: true + - name: startIndex + in: query + description: 1-based index of the first result to return (default 1) + required: false + schema: + type: integer + nullable: true + - name: count + in: query + description: Maximum number of results to return (default 100) + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScimUsersListResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + post: + description: >- + Create a new user in the organization (requires organization-scoped API + key) + operationId: scim_createUser + tags: + - Scim + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScimUser' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userName: + type: string + description: User's email address (required) + name: + $ref: '#/components/schemas/ScimName' + description: User's name information + emails: + type: array + items: + $ref: '#/components/schemas/ScimEmail' + nullable: true + description: User's email addresses + active: + type: boolean + nullable: true + description: Whether the user is active + password: + type: string + nullable: true + description: Initial password for the user + required: + - userName + - name + /api/public/scim/Users/{userId}: + get: + description: Get a specific user by ID (requires organization-scoped API key) + operationId: scim_getUser + tags: + - Scim + parameters: + - name: userId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScimUser' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: >- + Remove a user from the organization (requires organization-scoped API + key). Note that this only removes the user from the organization but + does not delete the user entity itself. + operationId: scim_deleteUser + tags: + - Scim + parameters: + - name: userId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/score-configs: + post: + description: >- + Create a score configuration (config). Score configs are used to define + the structure of scores + operationId: scoreConfigs_create + tags: + - ScoreConfigs + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScoreConfig' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScoreConfigRequest' + get: + description: Get all score configs + operationId: scoreConfigs_get + tags: + - ScoreConfigs + parameters: + - name: page + in: query + description: Page number, starts at 1. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. If you encounter api issues due to too + large page sizes, try to reduce the limit + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScoreConfigs' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/score-configs/{configId}: + get: + description: Get a score config + operationId: scoreConfigs_get-by-id + tags: + - ScoreConfigs + parameters: + - name: configId + in: path + description: The unique langfuse identifier of a score config + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScoreConfig' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + patch: + description: Update a score config + operationId: scoreConfigs_update + tags: + - ScoreConfigs + parameters: + - name: configId + in: path + description: The unique langfuse identifier of a score config + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ScoreConfig' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScoreConfigRequest' + /api/public/v2/scores: + get: + description: Get a list of scores (supports both trace and session scores) + operationId: scores_get-many + tags: + - Scores + parameters: + - name: page + in: query + description: Page number, starts at 1. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. Maximum 100. Defaults to 50. Requests with + a limit greater than 100 return HTTP 400. If you encounter api + issues due to too large page sizes, try to reduce the limit. + required: false + schema: + type: integer + nullable: true + - name: userId + in: query + description: Retrieve only scores with this userId associated to the trace. + required: false + schema: + type: string + nullable: true + - name: name + in: query + description: Retrieve only scores with this name. + required: false + schema: + type: string + nullable: true + - name: fromTimestamp + in: query + description: >- + Optional filter to only include scores created on or after a certain + datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: toTimestamp + in: query + description: >- + Optional filter to only include scores created before a certain + datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: environment + in: query + description: >- + Optional filter for scores where the environment is one of the + provided values. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: source + in: query + description: Retrieve only scores from a specific source. + required: false + schema: + $ref: '#/components/schemas/ScoreSource' + nullable: true + - name: operator + in: query + description: Retrieve only scores with value. + required: false + schema: + type: string + nullable: true + - name: value + in: query + description: Retrieve only scores with value. + required: false + schema: + type: number + format: double + nullable: true + - name: scoreIds + in: query + description: Comma-separated list of score IDs to limit the results to. + required: false + schema: + type: string + nullable: true + - name: configId + in: query + description: Retrieve only scores with a specific configId. + required: false + schema: + type: string + nullable: true + - name: sessionId + in: query + description: Retrieve only scores with a specific sessionId. + required: false + schema: + type: string + nullable: true + - name: datasetRunId + in: query + description: Retrieve only scores with a specific datasetRunId. + required: false + schema: + type: string + nullable: true + - name: traceId + in: query + description: Retrieve only scores with a specific traceId. + required: false + schema: + type: string + nullable: true + - name: observationId + in: query + description: Comma-separated list of observation IDs to filter scores by. + required: false + schema: + type: string + nullable: true + - name: queueId + in: query + description: Retrieve only scores with a specific annotation queueId. + required: false + schema: + type: string + nullable: true + - name: dataType + in: query + description: Retrieve only scores with a specific dataType. + required: false + schema: + $ref: '#/components/schemas/ScoreDataType' + nullable: true + - name: traceTags + in: query + description: >- + Only scores linked to traces that include all of these tags will be + returned. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: fields + in: query + description: >- + Comma-separated list of field groups to include in the response. + Available field groups: 'score' (core score fields), 'trace' (trace + properties: userId, tags, environment, sessionId). If not specified, + both 'score' and 'trace' are returned by default. Example: 'score' + to exclude trace data, 'score,trace' to include both. Note: When + filtering by trace properties (using userId or traceTags + parameters), the 'trace' field group must be included, otherwise a + 400 error will be returned. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: >- + A JSON stringified array of filter objects. Each object requires + type, column, operator, and value. Supports filtering by score + metadata using the stringObject type. Example: + [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. + Supported types: stringObject (metadata key-value filtering), + string, number, datetime, stringOptions, arrayOptions. Supported + operators for stringObject: =, contains, does not contain, starts + with, ends with. + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GetScoresResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/v2/scores/{scoreId}: + get: + description: Get a score (supports both trace and session scores) + operationId: scores_get-by-id + tags: + - Scores + parameters: + - name: scoreId + in: path + description: The unique langfuse identifier of a score + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Score' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/sessions: + get: + description: Get sessions + operationId: sessions_list + tags: + - Sessions + parameters: + - name: page + in: query + description: Page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. If you encounter api issues due to too + large page sizes, try to reduce the limit. + required: false + schema: + type: integer + nullable: true + - name: fromTimestamp + in: query + description: >- + Optional filter to only include sessions created on or after a + certain datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: toTimestamp + in: query + description: >- + Optional filter to only include sessions created before a certain + datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: environment + in: query + description: >- + Optional filter for sessions where the environment is one of the + provided values. + required: false + schema: + type: array + items: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSessions' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/sessions/{sessionId}: + get: + description: >- + Get a session. Please note that `traces` on this endpoint are not + paginated, if you plan to fetch large sessions, consider `GET + /api/public/traces?sessionId=` + operationId: sessions_get + tags: + - Sessions + parameters: + - name: sessionId + in: path + description: The unique id of a session + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/SessionWithTraces' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/traces/{traceId}: + get: + description: Get a specific trace + operationId: trace_get + tags: + - Trace + parameters: + - name: traceId + in: path + description: The unique langfuse identifier of a trace + required: true + schema: + type: string + - name: fields + in: query + description: >- + Comma-separated list of fields to include in the response. Available + field groups: 'core' (always included), 'io' (input, output, + metadata), 'scores', 'observations', 'metrics'. If not specified, + all fields are returned. Example: 'core,scores,metrics'. Note: + Excluded 'observations' or 'scores' fields return empty arrays; + excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/TraceWithFullDetails' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: Delete a specific trace + operationId: trace_delete + tags: + - Trace + parameters: + - name: traceId + in: path + description: The unique langfuse identifier of the trace to delete + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteTraceResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + /api/public/traces: + get: + description: Get list of traces + operationId: trace_list + tags: + - Trace + parameters: + - name: page + in: query + description: Page number, starts at 1 + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: >- + Limit of items per page. If you encounter api issues due to too + large page sizes, try to reduce the limit. + required: false + schema: + type: integer + nullable: true + - name: userId + in: query + required: false + schema: + type: string + nullable: true + - name: name + in: query + required: false + schema: + type: string + nullable: true + - name: sessionId + in: query + required: false + schema: + type: string + nullable: true + - name: fromTimestamp + in: query + description: >- + Optional filter to only include traces with a trace.timestamp on or + after a certain datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: toTimestamp + in: query + description: >- + Optional filter to only include traces with a trace.timestamp before + a certain datetime (ISO 8601) + required: false + schema: + type: string + format: date-time + nullable: true + - name: orderBy + in: query + description: >- + Format of the string [field].[asc/desc]. Fields: id, timestamp, + name, userId, release, version, public, bookmarked, sessionId. + Example: timestamp.asc + required: false + schema: + type: string + nullable: true + - name: tags + in: query + description: Only traces that include all of these tags will be returned. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: version + in: query + description: Optional filter to only include traces with a certain version. + required: false + schema: + type: string + nullable: true + - name: release + in: query + description: Optional filter to only include traces with a certain release. + required: false + schema: + type: string + nullable: true + - name: environment + in: query + description: >- + Optional filter for traces where the environment is one of the + provided values. + required: false + schema: + type: array + items: + type: string + nullable: true + - name: fields + in: query + description: >- + Comma-separated list of fields to include in the response. Available + field groups: 'core' (always included), 'io' (input, output, + metadata), 'scores', 'observations', 'metrics'. If not specified, + all fields are returned. Example: 'core,scores,metrics'. Note: + Excluded 'observations' or 'scores' fields return empty arrays; + excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: >- + JSON string containing an array of filter conditions. When provided, + this takes precedence over query parameter filters (userId, name, + sessionId, tags, version, release, environment, fromTimestamp, + toTimestamp). + + + ## Filter Structure + + Each filter condition has the following structure: + + ```json + + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + + ``` + + + ## Available Columns + + + ### Core Trace Fields + + - `id` (string) - Trace ID + + - `name` (string) - Trace name + + - `timestamp` (datetime) - Trace timestamp + + - `userId` (string) - User ID + + - `sessionId` (string) - Session ID + + - `environment` (string) - Environment tag + + - `version` (string) - Version tag + + - `release` (string) - Release tag + + - `tags` (arrayOptions) - Array of tags + + - `bookmarked` (boolean) - Bookmark status + + + ### Structured Data + + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata + key-value pairs. Use `key` parameter to filter on specific metadata + keys. + + + ### Aggregated Metrics (from observations) + + These metrics are aggregated from all observations within the trace: + + - `latency` (number) - Latency in seconds (time from first + observation start to last observation end) + + - `inputTokens` (number) - Total input tokens across all + observations + + - `outputTokens` (number) - Total output tokens across all + observations + + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + - `inputCost` (number) - Total input cost in USD + + - `outputCost` (number) - Total output cost in USD + + - `totalCost` (number) - Total cost in USD + + + ### Observation Level Aggregations + + These fields aggregate observation levels within the trace: + + - `level` (string) - Highest severity level (ERROR > WARNING > + DEFAULT > DEBUG) + + - `warningCount` (number) - Count of WARNING level observations + + - `errorCount` (number) - Count of ERROR level observations + + - `defaultCount` (number) - Count of DEFAULT level observations + + - `debugCount` (number) - Count of DEBUG level observations + + + ### Scores (requires join with scores table) + + - `scores_avg` (number) - Average of numeric scores (alias: + `scores`) + + - `score_categories` (categoryOptions) - Categorical score values + + + ## Filter Examples + + ```json + + [ + { + "type": "datetime", + "column": "timestamp", + "operator": ">=", + "value": "2024-01-01T00:00:00Z" + }, + { + "type": "string", + "column": "userId", + "operator": "=", + "value": "user-123" + }, + { + "type": "number", + "column": "totalCost", + "operator": ">=", + "value": 0.01 + }, + { + "type": "arrayOptions", + "column": "tags", + "operator": "all of", + "value": ["production", "critical"] + }, + { + "type": "stringObject", + "column": "metadata", + "key": "customer_tier", + "operator": "=", + "value": "enterprise" + } + ] + + ``` + + + ## Performance Notes + + - Filtering on `userId`, `sessionId`, or `metadata` may enable skip + indexes for better query performance + + - Score filters require a join with the scores table and may impact + query performance + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Traces' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + delete: + description: Delete multiple traces + operationId: trace_deleteMultiple + tags: + - Trace + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteTraceResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + traceIds: + type: array + items: + type: string + description: List of trace IDs to delete + required: + - traceIds + /api/public/unstable/evaluation-rules: + post: + description: >- + Create an evaluation rule. + + + An evaluation rule defines **what** incoming data should be evaluated + and **how prompt variables should be populated** from that data. + + + Use this resource after choosing an evaluator from the evaluator + endpoints. + + + Key rules: + + - `name` must be unique within the project for public evaluation rules + + - `target` must be `observation` or `experiment` + + - `evaluator.name` + `evaluator.scope` must identify an existing + evaluator family returned by the evaluator endpoints + + - Langfuse resolves that family to its latest version before saving the + evaluation rule + + - for `target=experiment`, use dataset `id` values from `GET + /api/public/v2/datasets` when filtering by `datasetId` + + - every evaluator prompt variable must be mapped exactly once + + - `expected_output` and `experiment_item_metadata` mappings are only + valid for `target=experiment` + + - if `enabled=true`, Langfuse validates that the referenced evaluator + can currently run + + - at most 50 evaluation rules can be effectively active in one project + at the same time + + + If an evaluation rule with the same `name` already exists in the + project, the API returns `409`. + + In that case, update the existing resource with `PATCH + /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of + creating a second one. + + + If enabling this resource would exceed the 50-active limit, the API also + returns `409`. + + In that case, disable or pause another active evaluation rule before + enabling a new one. + + + Current scope: + + - evaluation rules are live-ingestion rules only + + - they do not trigger historical backfills + + + Recovery guidance: + + - `400 invalid_filter_value`: fix the filter `column` or `value` using + `details.column`, `details.invalidValues`, and `details.allowedValues` + + - `400 invalid_filter_value` with `details.column=datasetId`: call `GET + /api/public/v2/datasets`, then retry with dataset `id` values from that + response + + - `400 missing_variable_mapping`: fetch the evaluator again and make + sure every variable in `variables` appears exactly once in `mapping` + + - `400 duplicate_variable_mapping`: remove repeated mappings for the + same variable + + - `400 invalid_variable_mapping`: switch to a valid `source` for the + selected `target`, or fix the variable name + + - `400 invalid_json_path`: remove or correct the `jsonPath` + + - `422 evaluator_preflight_failed`: the selected evaluator cannot run + with the resolved model configuration. Fix the evaluator/default model + setup, then retry the create request. + operationId: unstable_evaluationRules_create + tags: + - UnstableEvaluationRules + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluationRule' + examples: + CreateObservationEvaluationRule: + value: + id: erule_123 + name: answer-correctness-live + evaluator: + id: evaltmpl_123 + name: answer-correctness + scope: project + target: observation + enabled: true + status: active + sampling: 1 + filter: + - type: stringOptions + column: type + operator: any of + value: + - GENERATION + mapping: + - variable: input + source: input + - variable: output + source: output + createdAt: '2026-03-30T09:20:00.000Z' + updatedAt: '2026-03-30T09:20:00.000Z' + CreateExperimentEvaluationRule: + value: + id: erule_456 + name: experiment-expected-output-match + evaluator: + id: evaltmpl_456 + name: expected-output-match + scope: project + target: experiment + enabled: true + status: active + sampling: 0.5 + filter: + - type: stringOptions + column: datasetId + operator: any of + value: + - 550e8400-e29b-41d4-a716-446655440000 + mapping: + - variable: output + source: output + - variable: expected_output + source: expected_output + createdAt: '2026-03-30T09:30:00.000Z' + updatedAt: '2026-03-30T09:30:00.000Z' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '409': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateEvaluationRuleRequest' + examples: + CreateObservationEvaluationRule: + value: + name: answer-correctness-live + evaluator: + name: answer-correctness + scope: project + target: observation + enabled: true + sampling: 1 + filter: + - type: stringOptions + column: type + operator: any of + value: + - GENERATION + mapping: + - variable: input + source: input + - variable: output + source: output + CreateExperimentEvaluationRule: + value: + name: experiment-expected-output-match + evaluator: + name: expected-output-match + scope: project + target: experiment + enabled: true + sampling: 0.5 + filter: + - type: stringOptions + column: datasetId + operator: any of + value: + - 550e8400-e29b-41d4-a716-446655440000 + mapping: + - variable: output + source: output + - variable: expected_output + source: expected_output + get: + description: >- + List evaluation rules in the authenticated project. + + + Each item describes one live evaluation rule and its effective runtime + status. + operationId: unstable_evaluationRules_list + tags: + - UnstableEvaluationRules + parameters: + - name: page + in: query + description: 1-based page number. Defaults to `1`. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: Maximum number of items per page. Defaults to `50`. + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluationRules' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + /api/public/unstable/evaluation-rules/{evaluationRuleId}: + get: + description: >- + Get one evaluation rule by its identifier. + + + Use this endpoint to inspect the current evaluator, target, mapping, + filters, and effective runtime status. + operationId: unstable_evaluationRules_get + tags: + - UnstableEvaluationRules + parameters: + - name: evaluationRuleId + in: path + description: >- + Evaluation rule identifier returned by the evaluation rule + endpoints. + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluationRule' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + patch: + description: >- + Update an evaluation rule. + + + Typical uses: + + - enable or disable live execution + + - switch to another evaluator + + - adjust sampling + + - change filters + + - update variable mappings + + + Important behavior: + + - provide only the fields you want to change + + - if you provide `evaluator`, Langfuse resolves that evaluator family to + its latest version before saving + + - changing `target`, `filter`, or `mapping` must still produce a valid + target-specific configuration + + - if you change `target`, also send a compatible `filter` and `mapping` + in the same request unless the existing ones are still valid for the new + target + + - if the resulting config is enabled, Langfuse re-validates that the + selected evaluator can run + + - if the update would move a non-active evaluation rule into the active + state and the project already has 50 active evaluation rules, the API + returns `409` + + + Recovery guidance: + + - if the update fails with `missing_variable_mapping` or + `invalid_variable_mapping` after changing `evaluator` or `target`, + resend the request with a complete new `mapping` + + - if the update fails with `invalid_filter_value` after changing + `target`, resend the request with a target-compatible `filter` + operationId: unstable_evaluationRules_update + tags: + - UnstableEvaluationRules + parameters: + - name: evaluationRuleId + in: path + description: Evaluation rule identifier. + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluationRule' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableUpdateEvaluationRuleRequest' + delete: + description: >- + Delete an evaluation rule. + + + This removes the live-ingestion rule only. It does not delete the + referenced evaluator. + operationId: unstable_evaluationRules_delete + tags: + - UnstableEvaluationRules + parameters: + - name: evaluationRuleId + in: path + description: Evaluation rule identifier. + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDeleteEvaluationRuleResponse' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + /api/public/unstable/evaluators: + post: + description: >- + Create an evaluator in the authenticated project. + + + Use evaluators to define **how** Langfuse should score data: the prompt, + the expected structured output, and the optional model configuration. + + + Naming behavior: + + - If this is a new evaluator name in your project, Langfuse creates + version `1`. + + - If the name already exists in your project, Langfuse creates the next + version and returns it. + + - When a new project version is created, existing evaluation rules in + that project automatically move to the newest version for that evaluator + name. + + + Recommended workflow: + + 1. Create the evaluator. + + 2. Read the returned `variables` array. + + 3. Read the returned `outputDefinition.dataType` so the client knows + whether future scores will be numeric, boolean, or categorical. + + 4. Create one or more evaluation rules that reference the returned + evaluator family using `name` and `scope`. + + + Recovery guidance: + + - `422` with `code=evaluator_preflight_failed`: the evaluator cannot run + with the resolved model configuration. Add a valid explicit + `modelConfig`, or configure the project's default evaluation model, then + retry the same request. + + - `400` with `code=invalid_body`: the request shape is malformed. Use + the structured `details.issues` array to fix the specific fields and + retry. + + - `400` with `code=invalid_body` on `outputDefinition`: send `dataType`, + `reasoning.description`, and `score.description`. Do not send `version`; + it is not part of the public request shape. + + + Unstable API note: + + - This surface may evolve while the underlying evaluation data model is + being redesigned. + operationId: unstable_evaluators_create + tags: + - UnstableEvaluators + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluator' + examples: + CreateEvaluatorVersion: + value: + id: evaltmpl_123 + name: answer-correctness + version: 2 + scope: project + type: llm_as_judge + prompt: | + You are grading an answer. + + Input: + {{input}} + + Output: + {{output}} + + Return a score between 0 and 1. + variables: + - input + - output + outputDefinition: + dataType: NUMERIC + reasoning: + description: Explain why the score was assigned. + score: + description: Correctness score between 0 and 1. + modelConfig: + provider: openai + model: gpt-4.1-mini + evaluationRuleCount: 0 + createdAt: '2026-03-30T09:00:00.000Z' + updatedAt: '2026-03-30T09:00:00.000Z' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '409': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateEvaluatorRequest' + examples: + CreateEvaluatorVersion: + value: + name: answer-correctness + prompt: | + You are grading an answer. + + Input: + {{input}} + + Output: + {{output}} + + Return a score between 0 and 1. + outputDefinition: + dataType: NUMERIC + reasoning: + description: Explain why the score was assigned. + score: + description: Correctness score between 0 and 1. + modelConfig: + provider: openai + model: gpt-4.1-mini + get: + description: >- + List the evaluators available to the authenticated project. + + + Important behavior: + + - This endpoint returns the latest version of each available evaluator. + + - Results can include evaluators from your project and Langfuse-managed + evaluators. + + - If the same evaluator name exists in both places, both are returned as + separate items with different `scope` values. + operationId: unstable_evaluators_list + tags: + - UnstableEvaluators + parameters: + - name: page + in: query + description: 1-based page number. Defaults to `1`. + required: false + schema: + type: integer + nullable: true + - name: limit + in: query + description: Maximum number of items per page. Defaults to `50`. + required: false + schema: + type: integer + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluators' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] + /api/public/unstable/evaluators/{evaluatorId}: + get: + description: >- + Get one evaluator by `id`. + + + Use this endpoint when you want the prompt, output definition, model + configuration, and derived variables for the evaluator you plan to use + in an evaluation rule. + operationId: unstable_evaluators_get + tags: + - UnstableEvaluators + parameters: + - name: evaluatorId + in: path + description: Evaluator identifier returned by the evaluator endpoints. + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableEvaluator' + '400': + description: '' + content: + application/json: + schema: {} + '401': + description: '' + content: + application/json: + schema: {} + '403': + description: '' + content: + application/json: + schema: {} + '404': + description: '' + content: + application/json: + schema: {} + '405': + description: '' + content: + application/json: + schema: {} + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: + - BasicAuth: [] +components: + schemas: + AnnotationQueueStatus: + title: AnnotationQueueStatus + type: string + enum: + - PENDING + - COMPLETED + AnnotationQueueObjectType: + title: AnnotationQueueObjectType + type: string + enum: + - TRACE + - OBSERVATION + - SESSION + AnnotationQueue: + title: AnnotationQueue + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + nullable: true + scoreConfigIds: + type: array + items: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - name + - scoreConfigIds + - createdAt + - updatedAt + AnnotationQueueItem: + title: AnnotationQueueItem + type: object + properties: + id: + type: string + queueId: + type: string + objectId: + type: string + objectType: + $ref: '#/components/schemas/AnnotationQueueObjectType' + status: + $ref: '#/components/schemas/AnnotationQueueStatus' + completedAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - queueId + - objectId + - objectType + - status + - createdAt + - updatedAt + PaginatedAnnotationQueues: + title: PaginatedAnnotationQueues + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AnnotationQueue' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + PaginatedAnnotationQueueItems: + title: PaginatedAnnotationQueueItems + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/AnnotationQueueItem' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + CreateAnnotationQueueRequest: + title: CreateAnnotationQueueRequest + type: object + properties: + name: + type: string + description: + type: string + nullable: true + scoreConfigIds: + type: array + items: + type: string + required: + - name + - scoreConfigIds + CreateAnnotationQueueItemRequest: + title: CreateAnnotationQueueItemRequest + type: object + properties: + objectId: + type: string + objectType: + $ref: '#/components/schemas/AnnotationQueueObjectType' + status: + $ref: '#/components/schemas/AnnotationQueueStatus' + nullable: true + description: Defaults to PENDING for new queue items + required: + - objectId + - objectType + UpdateAnnotationQueueItemRequest: + title: UpdateAnnotationQueueItemRequest + type: object + properties: + status: + $ref: '#/components/schemas/AnnotationQueueStatus' + nullable: true + DeleteAnnotationQueueItemResponse: + title: DeleteAnnotationQueueItemResponse + type: object + properties: + success: + type: boolean + message: + type: string + required: + - success + - message + AnnotationQueueAssignmentRequest: + title: AnnotationQueueAssignmentRequest + type: object + properties: + userId: + type: string + required: + - userId + DeleteAnnotationQueueAssignmentResponse: + title: DeleteAnnotationQueueAssignmentResponse + type: object + properties: + success: + type: boolean + required: + - success + CreateAnnotationQueueAssignmentResponse: + title: CreateAnnotationQueueAssignmentResponse + type: object + properties: + userId: + type: string + queueId: + type: string + projectId: + type: string + required: + - userId + - queueId + - projectId + BlobStorageIntegrationType: + title: BlobStorageIntegrationType + type: string + enum: + - S3 + - S3_COMPATIBLE + - AZURE_BLOB_STORAGE + BlobStorageIntegrationFileType: + title: BlobStorageIntegrationFileType + type: string + enum: + - JSON + - CSV + - JSONL + BlobStorageExportMode: + title: BlobStorageExportMode + type: string + enum: + - FULL_HISTORY + - FROM_TODAY + - FROM_CUSTOM_DATE + BlobStorageExportFrequency: + title: BlobStorageExportFrequency + type: string + enum: + - every_20_minutes + - hourly + - daily + - weekly + BlobStorageExportSource: + title: BlobStorageExportSource + type: string + enum: + - LEGACY_TRACES_OBSERVATIONS + - OBSERVATIONS_V2 + - LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS + description: >- + What data the integration exports. + + - `LEGACY_TRACES_OBSERVATIONS`: traces, observations, and scores tables + with a fixed column set. The `exportFieldGroups` field is not + applicable. + + - `OBSERVATIONS_V2`: same data model as the + `/api/public/v2/observations` endpoint, plus scores. Columns are + controlled by `exportFieldGroups`. + + - `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: both sets. For the + `OBSERVATIONS_V2` portion, columns are controlled by + `exportFieldGroups`. + + + **Note:** `OBSERVATIONS_V2` and the enriched-observations portion of + `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` rely on the enriched + observations table (Langfuse Fast Preview / v4), which is currently + available on Langfuse Cloud only. See https://langfuse.com/docs/v4. + BlobStorageExportFieldGroup: + title: BlobStorageExportFieldGroup + type: string + enum: + - core + - basic + - time + - io + - metadata + - model + - usage + - prompt + - metrics + - tools + - trace_context + description: >- + Field group for the OBSERVATIONS_V2 and + LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS export. + CreateBlobStorageIntegrationRequest: + title: CreateBlobStorageIntegrationRequest + type: object + properties: + projectId: + type: string + description: ID of the project in which to configure the blob storage integration + type: + $ref: '#/components/schemas/BlobStorageIntegrationType' + bucketName: + type: string + description: >- + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid + Azure container name (3-63 chars, lowercase letters, numbers, and + hyphens only, must start and end with a letter or number, no + consecutive hyphens). + endpoint: + type: string + nullable: true + description: Custom endpoint URL (required for S3_COMPATIBLE type) + region: + type: string + description: Storage region + accessKeyId: + type: string + nullable: true + description: Access key ID for authentication + secretAccessKey: + type: string + nullable: true + description: Secret access key for authentication (will be encrypted when stored) + prefix: + type: string + nullable: true + description: >- + Path prefix for exported files (must end with forward slash if + provided) + exportFrequency: + $ref: '#/components/schemas/BlobStorageExportFrequency' + enabled: + type: boolean + description: Whether the integration is active + forcePathStyle: + type: boolean + description: Use path-style URLs for S3 requests + fileType: + $ref: '#/components/schemas/BlobStorageIntegrationFileType' + exportMode: + $ref: '#/components/schemas/BlobStorageExportMode' + exportStartDate: + type: string + format: date-time + nullable: true + description: >- + Custom start date for exports (required when exportMode is + FROM_CUSTOM_DATE) + compressed: + type: boolean + nullable: true + description: >- + Enable gzip compression for exported files (.csv.gz, .json.gz, + .jsonl.gz). Defaults to true. + exportSource: + $ref: '#/components/schemas/BlobStorageExportSource' + nullable: true + description: >- + Data to export. When omitted on update, the existing value is + preserved. When omitted on create: pre-cutoff Cloud projects and + self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; + post-cutoff Cloud projects (created on or after 2026-05-20) + auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` + is provided. + + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects + created on or after 2026-05-20 on Langfuse Cloud, + `LEGACY_TRACES_OBSERVATIONS` and + `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP + 400. Omitting `exportSource` on these projects silently defaults to + `OBSERVATIONS_V2` rather than the schema column default. Use + `OBSERVATIONS_V2` for all new integrations. Projects created before + 2026-05-20 and self-hosted deployments are unaffected. + exportFieldGroups: + type: array + items: + $ref: '#/components/schemas/BlobStorageExportFieldGroup' + nullable: true + description: >- + Field groups to include in each exported row. + + + For exportSource `OBSERVATIONS_V2` or + `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if + provided. When omitted on create, the column default (all groups) + applies. When omitted on update, the existing value is preserved. + + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be + omitted or null. Sending an array (including an empty array) returns + 400, because that source uses a fixed column set and does not honor + field groups. + + + `exportFieldGroups` requires `exportSource` to be provided in the + same request. + required: + - projectId + - type + - bucketName + - region + - exportFrequency + - enabled + - forcePathStyle + - fileType + - exportMode + BlobStorageIntegrationResponse: + title: BlobStorageIntegrationResponse + type: object + properties: + id: + type: string + projectId: + type: string + type: + $ref: '#/components/schemas/BlobStorageIntegrationType' + bucketName: + type: string + endpoint: + type: string + nullable: true + region: + type: string + accessKeyId: + type: string + nullable: true + prefix: + type: string + exportFrequency: + $ref: '#/components/schemas/BlobStorageExportFrequency' + enabled: + type: boolean + forcePathStyle: + type: boolean + fileType: + $ref: '#/components/schemas/BlobStorageIntegrationFileType' + exportMode: + $ref: '#/components/schemas/BlobStorageExportMode' + exportStartDate: + type: string + format: date-time + nullable: true + compressed: + type: boolean + exportSource: + $ref: '#/components/schemas/BlobStorageExportSource' + exportFieldGroups: + type: array + items: + $ref: '#/components/schemas/BlobStorageExportFieldGroup' + nullable: true + description: >- + Field groups included in each exported row for `OBSERVATIONS_V2` / + `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` sources. Always `null` + when exportSource is `LEGACY_TRACES_OBSERVATIONS` (the field does + not apply to that source; any legacy DB value is hidden from the + public surface). + nextSyncAt: + type: string + format: date-time + nullable: true + lastSyncAt: + type: string + format: date-time + nullable: true + lastError: + type: string + nullable: true + lastErrorAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - projectId + - type + - bucketName + - region + - prefix + - exportFrequency + - enabled + - forcePathStyle + - fileType + - exportMode + - compressed + - exportSource + - createdAt + - updatedAt + BlobStorageIntegrationsResponse: + title: BlobStorageIntegrationsResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/BlobStorageIntegrationResponse' + required: + - data + BlobStorageSyncStatus: + title: BlobStorageSyncStatus + type: string + enum: + - idle + - queued + - up_to_date + - disabled + - error + description: >- + Sync status of the blob storage integration: + + - `disabled` β€” integration is not enabled + + - `error` β€” last export failed (see `lastError` for details) + + - `idle` β€” enabled but has never exported yet + + - `queued` β€” next export is overdue (`nextSyncAt` is in the past) and + waiting to be picked up by the worker + + - `up_to_date` β€” all available data has been exported; next export is + scheduled for the future + + + **ETL usage**: poll this endpoint and check for `up_to_date` status. + Compare `lastSyncAt` against your + + ETL bookmark to determine if new data is available. Note that exports + run with a 20-minute lag buffer, + + so `lastSyncAt` will always be at least 20 minutes behind real-time. + BlobStorageIntegrationStatusResponse: + title: BlobStorageIntegrationStatusResponse + type: object + properties: + id: + type: string + projectId: + type: string + syncStatus: + $ref: '#/components/schemas/BlobStorageSyncStatus' + enabled: + type: boolean + lastSyncAt: + type: string + format: date-time + nullable: true + description: >- + End of the last successfully exported time window. Compare against + your ETL bookmark to determine if new data is available. Null if the + integration has never synced. + nextSyncAt: + type: string + format: date-time + nullable: true + description: When the next export is scheduled. Null if no sync has occurred yet. + lastError: + type: string + nullable: true + description: >- + Raw error message from the storage provider (S3/Azure/GCS) if the + last export failed. Cleared on successful export. + lastErrorAt: + type: string + format: date-time + nullable: true + description: When the last error occurred. Cleared on successful export. + required: + - id + - projectId + - syncStatus + - enabled + BlobStorageIntegrationDeletionResponse: + title: BlobStorageIntegrationDeletionResponse + type: object + properties: + message: + type: string + required: + - message + CreateCommentRequest: + title: CreateCommentRequest + type: object + properties: + projectId: + type: string + description: The id of the project to attach the comment to. + objectType: + type: string + description: >- + The type of the object to attach the comment to (trace, observation, + session, prompt). + objectId: + type: string + description: >- + The id of the object to attach the comment to. If this does not + reference a valid existing object, an error will be thrown. + content: + type: string + description: >- + The content of the comment. May include markdown. Currently limited + to 5000 characters. + authorUserId: + type: string + nullable: true + description: The id of the user who created the comment. + required: + - projectId + - objectType + - objectId + - content + CreateCommentResponse: + title: CreateCommentResponse + type: object + properties: + id: + type: string + description: The id of the created object in Langfuse + required: + - id + GetCommentsResponse: + title: GetCommentsResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Comment' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + Trace: + title: Trace + type: object + properties: + id: + type: string + description: The unique identifier of a trace + timestamp: + type: string + format: date-time + description: The timestamp when the trace was created + name: + type: string + nullable: true + description: The name of the trace + input: + nullable: true + description: The input data of the trace. Can be any JSON. + output: + nullable: true + description: The output data of the trace. Can be any JSON. + sessionId: + type: string + nullable: true + description: The session identifier associated with the trace + release: + type: string + nullable: true + description: The release version of the application when the trace was created + version: + type: string + nullable: true + description: The version of the trace + userId: + type: string + nullable: true + description: The user identifier associated with the trace + metadata: + nullable: true + description: The metadata associated with the trace. Can be any JSON. + tags: + type: array + items: + type: string + description: The tags associated with the trace. + public: + type: boolean + description: Public traces are accessible via url without login + environment: + type: string + description: >- + The environment from which this trace originated. Can be any + lowercase alphanumeric string with hyphens and underscores that does + not start with 'langfuse'. + required: + - id + - timestamp + - tags + - public + - environment + TraceWithDetails: + title: TraceWithDetails + type: object + properties: + htmlPath: + type: string + description: Path of trace in Langfuse UI + latency: + type: number + format: double + nullable: true + description: Latency of trace in seconds + totalCost: + type: number + format: double + nullable: true + description: Cost of trace in USD + observations: + type: array + items: + type: string + nullable: true + description: List of observation ids + scores: + type: array + items: + type: string + nullable: true + description: List of score ids + required: + - htmlPath + allOf: + - $ref: '#/components/schemas/Trace' + TraceWithFullDetails: + title: TraceWithFullDetails + type: object + properties: + htmlPath: + type: string + description: Path of trace in Langfuse UI + latency: + type: number + format: double + nullable: true + description: Latency of trace in seconds + totalCost: + type: number + format: double + nullable: true + description: Cost of trace in USD + observations: + type: array + items: + $ref: '#/components/schemas/ObservationsView' + description: List of observations + scores: + type: array + items: + $ref: '#/components/schemas/ScoreV1' + description: List of scores + required: + - htmlPath + - observations + - scores + allOf: + - $ref: '#/components/schemas/Trace' + Session: + title: Session + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + projectId: + type: string + environment: + type: string + description: The environment from which this session originated. + required: + - id + - createdAt + - projectId + - environment + SessionWithTraces: + title: SessionWithTraces + type: object + properties: + traces: + type: array + items: + $ref: '#/components/schemas/Trace' + required: + - traces + allOf: + - $ref: '#/components/schemas/Session' + Observation: + title: Observation + type: object + properties: + id: + type: string + description: The unique identifier of the observation + traceId: + type: string + nullable: true + description: The trace ID associated with the observation + type: + type: string + description: The type of the observation + name: + type: string + nullable: true + description: The name of the observation + startTime: + type: string + format: date-time + description: The start time of the observation + endTime: + type: string + format: date-time + nullable: true + description: The end time of the observation. + completionStartTime: + type: string + format: date-time + nullable: true + description: The completion start time of the observation + model: + type: string + nullable: true + description: The model used for the observation + modelParameters: + description: The parameters of the model used for the observation + input: + description: The input data of the observation + version: + type: string + nullable: true + description: The version of the observation + metadata: + description: Additional metadata of the observation + output: + description: The output data of the observation + usage: + $ref: '#/components/schemas/Usage' + description: >- + (Deprecated. Use usageDetails and costDetails instead.) The usage + data of the observation + level: + $ref: '#/components/schemas/ObservationLevel' + description: The level of the observation + statusMessage: + type: string + nullable: true + description: The status message of the observation + parentObservationId: + type: string + nullable: true + description: The parent observation ID + promptId: + type: string + nullable: true + description: The prompt ID associated with the observation + usageDetails: + type: object + additionalProperties: + type: integer + description: >- + The usage details of the observation. Key is the name of the usage + metric, value is the number of units consumed. The total key is the + sum of all (non-total) usage metrics or the total value ingested. + costDetails: + type: object + additionalProperties: + type: number + format: double + description: >- + The cost details of the observation. Key is the name of the cost + metric, value is the cost in USD. The total key is the sum of all + (non-total) cost metrics or the total value ingested. + environment: + type: string + description: >- + The environment from which this observation originated. Can be any + lowercase alphanumeric string with hyphens and underscores that does + not start with 'langfuse'. + required: + - id + - type + - startTime + - modelParameters + - input + - metadata + - output + - usage + - level + - usageDetails + - costDetails + - environment + ObservationsView: + title: ObservationsView + type: object + properties: + promptName: + type: string + nullable: true + description: The name of the prompt associated with the observation + promptVersion: + type: integer + nullable: true + description: The version of the prompt associated with the observation + modelId: + type: string + nullable: true + description: The unique identifier of the model + inputPrice: + type: number + format: double + nullable: true + description: The price of the input in USD + outputPrice: + type: number + format: double + nullable: true + description: The price of the output in USD. + totalPrice: + type: number + format: double + nullable: true + description: The total price in USD. + calculatedInputCost: + type: number + format: double + nullable: true + description: >- + (Deprecated. Use usageDetails and costDetails instead.) The + calculated cost of the input in USD + calculatedOutputCost: + type: number + format: double + nullable: true + description: >- + (Deprecated. Use usageDetails and costDetails instead.) The + calculated cost of the output in USD + calculatedTotalCost: + type: number + format: double + nullable: true + description: >- + (Deprecated. Use usageDetails and costDetails instead.) The + calculated total cost in USD + latency: + type: number + format: double + nullable: true + description: The latency in seconds. + timeToFirstToken: + type: number + format: double + nullable: true + description: The time to the first token in seconds + allOf: + - $ref: '#/components/schemas/Observation' + ObservationV2: + title: ObservationV2 + type: object + description: >- + An observation from the v2 API with field-group-based selection. + + Core fields are always present. Other fields are included only when + their field group is requested. + properties: + id: + type: string + description: The unique identifier of the observation + traceId: + type: string + nullable: true + description: The trace ID associated with the observation + startTime: + type: string + format: date-time + description: The start time of the observation + endTime: + type: string + format: date-time + nullable: true + description: The end time of the observation + projectId: + type: string + description: The project ID this observation belongs to + parentObservationId: + type: string + nullable: true + description: The parent observation ID + type: + type: string + description: The type of the observation (e.g. GENERATION, SPAN, EVENT) + name: + type: string + nullable: true + description: The name of the observation + level: + $ref: '#/components/schemas/ObservationLevel' + nullable: true + description: The level of the observation + statusMessage: + type: string + nullable: true + description: The status message of the observation + version: + type: string + nullable: true + description: The version of the observation + environment: + type: string + nullable: true + description: The environment from which this observation originated + bookmarked: + type: boolean + nullable: true + description: Whether the observation is bookmarked + public: + type: boolean + nullable: true + description: Whether the observation is public + userId: + type: string + nullable: true + description: The user ID associated with the observation + sessionId: + type: string + nullable: true + description: The session ID associated with the observation + completionStartTime: + type: string + format: date-time + nullable: true + description: The completion start time of the observation + createdAt: + type: string + format: date-time + nullable: true + description: The creation timestamp of the observation + updatedAt: + type: string + format: date-time + nullable: true + description: The last update timestamp of the observation + input: + nullable: true + description: The input data of the observation + output: + nullable: true + description: The output data of the observation + metadata: + nullable: true + description: Additional metadata of the observation + providedModelName: + type: string + nullable: true + description: The model name as provided by the user + internalModelId: + type: string + nullable: true + description: The internal model ID matched by Langfuse + modelParameters: + nullable: true + description: The parameters of the model used for the observation + usageDetails: + type: object + additionalProperties: + type: integer + nullable: true + description: >- + The usage details of the observation. Key is the usage metric name, + value is the number of units consumed. + costDetails: + type: object + additionalProperties: + type: number + format: double + nullable: true + description: >- + The cost details of the observation. Key is the cost metric name, + value is the cost in USD. + totalCost: + type: number + format: double + nullable: true + description: The total cost of the observation in USD + usagePricingTierName: + type: string + nullable: true + description: >- + The name of the pricing tier applied to this observation's usage + costs + promptId: + type: string + nullable: true + description: The prompt ID associated with the observation + promptName: + type: string + nullable: true + description: The prompt name associated with the observation + promptVersion: + type: integer + nullable: true + description: The prompt version associated with the observation + latency: + type: number + format: double + nullable: true + description: The latency in seconds + timeToFirstToken: + type: number + format: double + nullable: true + description: The time to first token in seconds + modelId: + type: string + nullable: true + description: >- + The matched model ID. Null when the `model` field group is not + requested. + inputPrice: + type: string + nullable: true + description: >- + The input token price (USD per unit) from the matched model, + serialized as a decimal string (e.g. "0.0001"). Null when the + `model` field group is not requested. + outputPrice: + type: string + nullable: true + description: >- + The output token price (USD per unit) from the matched model, + serialized as a decimal string (e.g. "0.0001"). Null when the + `model` field group is not requested. + totalPrice: + type: string + nullable: true + description: >- + The total token price (USD per unit) from the matched model, + serialized as a decimal string (e.g. "0.0001"). Null when the + `model` field group is not requested. + traceName: + type: string + nullable: true + description: The name of the parent trace + tags: + type: array + items: + type: string + nullable: true + description: Tags from the parent trace (denormalized onto the observation) + release: + type: string + nullable: true + description: The release version of the parent trace + required: + - id + - startTime + - projectId + - type + Usage: + title: Usage + type: object + description: >- + (Deprecated. Use usageDetails and costDetails instead.) Standard + interface for usage and cost + properties: + input: + type: integer + description: Number of input units (e.g. tokens) + output: + type: integer + description: Number of output units (e.g. tokens) + total: + type: integer + description: Defaults to input+output if not set + unit: + type: string + nullable: true + description: Unit of measurement + inputCost: + type: number + format: double + nullable: true + description: USD input cost + outputCost: + type: number + format: double + nullable: true + description: USD output cost + totalCost: + type: number + format: double + nullable: true + description: USD total cost, defaults to input+output + required: + - input + - output + - total + ScoreConfig: + title: ScoreConfig + type: object + description: Configuration for a score + properties: + id: + type: string + name: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + projectId: + type: string + dataType: + $ref: '#/components/schemas/ScoreConfigDataType' + isArchived: + type: boolean + description: Whether the score config is archived. Defaults to false + minValue: + type: number + format: double + nullable: true + description: >- + Sets minimum value for numerical scores. If not set, the minimum + value defaults to -∞ + maxValue: + type: number + format: double + nullable: true + description: >- + Sets maximum value for numerical scores. If not set, the maximum + value defaults to +∞ + categories: + type: array + items: + $ref: '#/components/schemas/ConfigCategory' + nullable: true + description: Configures custom categories for categorical scores + description: + type: string + nullable: true + description: Description of the score config + required: + - id + - name + - createdAt + - updatedAt + - projectId + - dataType + - isArchived + ConfigCategory: + title: ConfigCategory + type: object + properties: + value: + type: number + format: double + label: + type: string + required: + - value + - label + BaseScoreV1: + title: BaseScoreV1 + type: object + properties: + id: + type: string + traceId: + type: string + name: + type: string + source: + $ref: '#/components/schemas/ScoreSource' + observationId: + type: string + nullable: true + description: The observation ID associated with the score + timestamp: + type: string + format: date-time + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + authorUserId: + type: string + nullable: true + description: The user ID of the author + comment: + type: string + nullable: true + description: Comment on the score + metadata: + description: Metadata associated with the score + configId: + type: string + nullable: true + description: >- + Reference a score config on a score. When set, config and score name + must be equal and value must comply to optionally defined numerical + range + queueId: + type: string + nullable: true + description: >- + The annotation queue referenced by the score. Indicates if score was + initially created while processing annotation queue. + environment: + type: string + description: >- + The environment from which this score originated. Can be any + lowercase alphanumeric string with hyphens and underscores that does + not start with 'langfuse'. + required: + - id + - traceId + - name + - source + - timestamp + - createdAt + - updatedAt + - metadata + - environment + NumericScoreV1: + title: NumericScoreV1 + type: object + properties: + value: + type: number + format: double + description: The numeric value of the score + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV1' + BooleanScoreV1: + title: BooleanScoreV1 + type: object + properties: + value: + type: number + format: double + description: >- + The numeric value of the score. Equals 1 for "True" and 0 for + "False" + stringValue: + type: string + description: >- + The string representation of the score value. Is inferred from the + numeric value and equals "True" or "False" + required: + - value + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScoreV1' + CategoricalScoreV1: + title: CategoricalScoreV1 + type: object + properties: + value: + type: number + format: double + description: >- + Represents the numeric category mapping of the stringValue. If no + config is linked, defaults to 0. + stringValue: + type: string + description: >- + The string representation of the score value. If no config is + linked, can be any string. Otherwise, must map to a config category + required: + - value + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScoreV1' + TextScoreV1: + title: TextScoreV1 + type: object + properties: + stringValue: + type: string + description: The text content of the score (1-500 characters) + required: + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScoreV1' + ScoreV1: + title: ScoreV1 + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: '#/components/schemas/NumericScoreV1' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: '#/components/schemas/CategoricalScoreV1' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: '#/components/schemas/BooleanScoreV1' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - TEXT + - $ref: '#/components/schemas/TextScoreV1' + required: + - dataType + BaseScore: + title: BaseScore + type: object + properties: + id: + type: string + traceId: + type: string + nullable: true + description: The trace ID associated with the score + sessionId: + type: string + nullable: true + description: The session ID associated with the score + observationId: + type: string + nullable: true + description: The observation ID associated with the score + datasetRunId: + type: string + nullable: true + description: The dataset run ID associated with the score + name: + type: string + source: + $ref: '#/components/schemas/ScoreSource' + timestamp: + type: string + format: date-time + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + authorUserId: + type: string + nullable: true + description: The user ID of the author + comment: + type: string + nullable: true + description: Comment on the score + metadata: + description: Metadata associated with the score + configId: + type: string + nullable: true + description: >- + Reference a score config on a score. When set, config and score name + must be equal and value must comply to optionally defined numerical + range + queueId: + type: string + nullable: true + description: >- + The annotation queue referenced by the score. Indicates if score was + initially created while processing annotation queue. + environment: + type: string + description: >- + The environment from which this score originated. Can be any + lowercase alphanumeric string with hyphens and underscores that does + not start with 'langfuse'. + required: + - id + - name + - source + - timestamp + - createdAt + - updatedAt + - metadata + - environment + NumericScore: + title: NumericScore + type: object + properties: + value: + type: number + format: double + description: The numeric value of the score + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScore' + BooleanScore: + title: BooleanScore + type: object + properties: + value: + type: number + format: double + description: >- + The numeric value of the score. Equals 1 for "True" and 0 for + "False" + stringValue: + type: string + description: >- + The string representation of the score value. Is inferred from the + numeric value and equals "True" or "False" + required: + - value + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScore' + CategoricalScore: + title: CategoricalScore + type: object + properties: + value: + type: number + format: double + description: >- + Represents the numeric category mapping of the stringValue. If no + config is linked, defaults to 0. + stringValue: + type: string + description: >- + The string representation of the score value. If no config is + linked, can be any string. Otherwise, must map to a config category + required: + - value + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScore' + CorrectionScore: + title: CorrectionScore + type: object + properties: + value: + type: number + format: double + description: The numeric value of the score. Always 0 for correction scores. + stringValue: + type: string + description: The string representation of the correction content + required: + - value + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScore' + TextScore: + title: TextScore + type: object + properties: + stringValue: + type: string + description: The text content of the score (1-500 characters) + required: + - stringValue + allOf: + - $ref: '#/components/schemas/BaseScore' + Score: + title: Score + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: '#/components/schemas/NumericScore' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: '#/components/schemas/CategoricalScore' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: '#/components/schemas/BooleanScore' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CORRECTION + - $ref: '#/components/schemas/CorrectionScore' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - TEXT + - $ref: '#/components/schemas/TextScore' + required: + - dataType + CreateScoreValue: + title: CreateScoreValue + oneOf: + - type: number + format: double + - type: string + description: >- + The value of the score. Must be passed as string for categorical and + text scores, and numeric for boolean and numeric scores + Comment: + title: Comment + type: object + properties: + id: + type: string + projectId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + objectType: + $ref: '#/components/schemas/CommentObjectType' + objectId: + type: string + content: + type: string + authorUserId: + type: string + nullable: true + description: The user ID of the comment author + required: + - id + - projectId + - createdAt + - updatedAt + - objectType + - objectId + - content + Dataset: + title: Dataset + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + nullable: true + description: Description of the dataset + metadata: + description: Metadata associated with the dataset + inputSchema: + nullable: true + description: JSON Schema for validating dataset item inputs + expectedOutputSchema: + nullable: true + description: JSON Schema for validating dataset item expected outputs + projectId: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - name + - metadata + - projectId + - createdAt + - updatedAt + DatasetItem: + title: DatasetItem + type: object + properties: + id: + type: string + status: + $ref: '#/components/schemas/DatasetStatus' + input: + description: Input data for the dataset item + expectedOutput: + description: Expected output for the dataset item + metadata: + description: Metadata associated with the dataset item + sourceTraceId: + type: string + nullable: true + description: The trace ID that sourced this dataset item + sourceObservationId: + type: string + nullable: true + description: The observation ID that sourced this dataset item + datasetId: + type: string + datasetName: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - status + - input + - expectedOutput + - metadata + - datasetId + - datasetName + - createdAt + - updatedAt + DatasetRunItem: + title: DatasetRunItem + type: object + properties: + id: + type: string + datasetRunId: + type: string + datasetRunName: + type: string + datasetItemId: + type: string + traceId: + type: string + observationId: + type: string + nullable: true + description: The observation ID associated with this run item + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - datasetRunId + - datasetRunName + - datasetItemId + - traceId + - createdAt + - updatedAt + DatasetRun: + title: DatasetRun + type: object + properties: + id: + type: string + description: Unique identifier of the dataset run + name: + type: string + description: Name of the dataset run + description: + type: string + nullable: true + description: Description of the run + metadata: + description: Metadata of the dataset run + datasetId: + type: string + description: Id of the associated dataset + datasetName: + type: string + description: Name of the associated dataset + createdAt: + type: string + format: date-time + description: The date and time when the dataset run was created + updatedAt: + type: string + format: date-time + description: The date and time when the dataset run was last updated + required: + - id + - name + - metadata + - datasetId + - datasetName + - createdAt + - updatedAt + DatasetRunWithItems: + title: DatasetRunWithItems + type: object + properties: + datasetRunItems: + type: array + items: + $ref: '#/components/schemas/DatasetRunItem' + required: + - datasetRunItems + allOf: + - $ref: '#/components/schemas/DatasetRun' + Model: + title: Model + type: object + description: >- + Model definition used for transforming usage into USD cost and/or + tokenization. + + + Models can have either simple flat pricing or tiered pricing: + + - Flat pricing: Single price per usage type (legacy, but still + supported) + + - Tiered pricing: Multiple pricing tiers with conditional matching based + on usage patterns + + + The pricing tiers approach is recommended for models with usage-based + pricing variations. + + When using tiered pricing, the flat price fields (inputPrice, + outputPrice, prices) are populated + + from the default tier for backward compatibility. + properties: + id: + type: string + modelName: + type: string + description: >- + Name of the model definition. If multiple with the same name exist, + they are applied in the following order: (1) custom over built-in, + (2) newest according to startTime where + model.startTime- + Regex pattern which matches this model definition to + generation.model. Useful in case of fine-tuned models. If you want + to exact match, use `(?i)^modelname$` + startDate: + type: string + format: date-time + nullable: true + description: Apply only to generations which are newer than this ISO date. + unit: + $ref: '#/components/schemas/ModelUsageUnit' + nullable: true + description: Unit used by this model. + inputPrice: + type: number + format: double + nullable: true + description: Deprecated. See 'prices' instead. Price (USD) per input unit + outputPrice: + type: number + format: double + nullable: true + description: Deprecated. See 'prices' instead. Price (USD) per output unit + totalPrice: + type: number + format: double + nullable: true + description: >- + Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot + be set if input or output price is set. + tokenizerId: + type: string + nullable: true + description: >- + Optional. Tokenizer to be applied to observations which match to + this model. See docs for more details. + tokenizerConfig: + description: >- + Optional. Configuration for the selected tokenizer. Needs to be + JSON. See docs for more details. + isLangfuseManaged: + type: boolean + createdAt: + type: string + format: date-time + description: Timestamp when the model was created + prices: + type: object + additionalProperties: + $ref: '#/components/schemas/ModelPrice' + description: >- + Deprecated. Use 'pricingTiers' instead for models with usage-based + pricing variations. + + + This field shows prices by usage type from the default pricing tier. + Maintained for backward compatibility. + + If the model uses tiered pricing, this field will be populated from + the default tier's prices. + pricingTiers: + type: array + items: + $ref: '#/components/schemas/PricingTier' + description: >- + Array of pricing tiers with conditional pricing based on usage + thresholds. + + + Pricing tiers enable accurate cost tracking for models that charge + different rates based on usage patterns + + (e.g., different rates for high-volume usage, large context windows, + or cached tokens). + + + Each model must have exactly one default tier (isDefault=true, + priority=0) that serves as a fallback. + + Additional conditional tiers can be defined with specific matching + criteria. + + + If this array is empty, the model uses legacy flat pricing from the + inputPrice/outputPrice/totalPrice fields. + required: + - id + - modelName + - matchPattern + - tokenizerConfig + - isLangfuseManaged + - createdAt + - prices + - pricingTiers + ModelPrice: + title: ModelPrice + type: object + properties: + price: + type: number + format: double + required: + - price + PricingTierCondition: + title: PricingTierCondition + type: object + description: >- + Condition for matching a pricing tier based on usage details. Used to + implement tiered pricing models where costs vary based on usage + thresholds. + + + How it works: + + 1. The regex pattern matches against usage detail keys (e.g., + "input_tokens", "input_cached") + + 2. Values of all matching keys are summed together + + 3. The sum is compared against the threshold value using the specified + operator + + 4. All conditions in a tier must be met (AND logic) for the tier to + match + + + Common use cases: + + - Threshold-based pricing: Match when accumulated usage exceeds a + certain amount + + - Usage-type-specific pricing: Different rates for cached vs non-cached + tokens, or input vs output + + - Volume-based pricing: Different rates based on total request or token + count + properties: + usageDetailPattern: + type: string + description: >- + Regex pattern to match against usage detail keys. All matching keys' + values are summed for threshold comparison. + + + Examples: + + - "^input" matches "input", "input_tokens", "input_cached", etc. + + - "^(input|prompt)" matches both "input_tokens" and "prompt_tokens" + + - "_cache$" matches "input_cache", "output_cache", etc. + + + The pattern is case-insensitive by default. If no keys match, the + sum is treated as zero. + operator: + $ref: '#/components/schemas/PricingTierOperator' + description: >- + Comparison operator to apply between the summed value and the + threshold. + + + - gt: greater than (sum > threshold) + + - gte: greater than or equal (sum >= threshold) + + - lt: less than (sum < threshold) + + - lte: less than or equal (sum <= threshold) + + - eq: equal (sum == threshold) + + - neq: not equal (sum != threshold) + value: + type: number + format: double + description: >- + Threshold value for comparison. For token-based pricing, this is + typically the token count threshold (e.g., 200000 for a 200K token + threshold). + caseSensitive: + type: boolean + description: >- + Whether the regex pattern matching is case-sensitive. Default is + false (case-insensitive matching). + required: + - usageDetailPattern + - operator + - value + - caseSensitive + PricingTier: + title: PricingTier + type: object + description: >- + Pricing tier definition with conditional pricing based on usage + thresholds. + + + Pricing tiers enable accurate cost tracking for LLM providers that + charge different rates based on usage patterns. + + For example, some providers charge higher rates when context size + exceeds certain thresholds. + + + How tier matching works: + + 1. Tiers are evaluated in ascending priority order (priority 1 before + priority 2, etc.) + + 2. The first tier where ALL conditions match is selected + + 3. If no conditional tiers match, the default tier is used as a fallback + + 4. The default tier has priority 0 and no conditions + + + Why priorities matter: + + - Lower priority numbers are evaluated first, allowing you to define + specific cases before general ones + + - Example: Priority 1 for "high usage" (>200K tokens), Priority 2 for + "medium usage" (>100K tokens), Priority 0 for default + + - Without proper ordering, a less specific condition might match before + a more specific one + + + Every model must have exactly one default tier to ensure cost + calculation always succeeds. + properties: + id: + type: string + description: Unique identifier for the pricing tier + name: + type: string + description: >- + Name of the pricing tier for display and identification purposes. + + + Examples: "Standard", "High Volume Tier", "Large Context", "Extended + Context Tier" + isDefault: + type: boolean + description: >- + Whether this is the default tier. Every model must have exactly one + default tier with priority 0 and no conditions. + + + The default tier serves as a fallback when no conditional tiers + match, ensuring cost calculation always succeeds. + + It typically represents the base pricing for standard usage + patterns. + priority: + type: integer + description: >- + Priority for tier matching evaluation. Lower numbers = higher + priority (evaluated first). + + + The default tier must always have priority 0. Conditional tiers + should have priority 1, 2, 3, etc. + + + Example ordering: + + - Priority 0: Default tier (no conditions, always matches as + fallback) + + - Priority 1: High usage tier (e.g., >200K tokens) + + - Priority 2: Medium usage tier (e.g., >100K tokens) + + + This ensures more specific conditions are checked before general + ones. + conditions: + type: array + items: + $ref: '#/components/schemas/PricingTierCondition' + description: >- + Array of conditions that must ALL be met for this tier to match (AND + logic). + + + The default tier must have an empty conditions array. Conditional + tiers should have one or more conditions + + that define when this tier's pricing applies. + + + Multiple conditions enable complex matching scenarios (e.g., "high + input tokens AND low output tokens"). + prices: + type: object + additionalProperties: + type: number + format: double + description: >- + Prices (USD) by usage type for this tier. + + + Common usage types: "input", "output", "total", "request", "image" + + Prices are specified in USD per unit (e.g., per token, per request, + per second). + + + Example: {"input": 0.000003, "output": 0.000015} means $3 per + million input tokens and $15 per million output tokens. + required: + - id + - name + - isDefault + - priority + - conditions + - prices + PricingTierInput: + title: PricingTierInput + type: object + description: >- + Input schema for creating a pricing tier. The tier ID will be + automatically generated server-side. + + + When creating a model with pricing tiers: + + - Exactly one tier must have isDefault=true (the fallback tier) + + - The default tier must have priority=0 and conditions=[] + + - All tier names and priorities must be unique within the model + + - Each tier must define at least one price + + + See PricingTier for detailed information about how tiers work and why + they're useful. + properties: + name: + type: string + description: >- + Name of the pricing tier for display and identification purposes. + + + Must be unique within the model. Common patterns: "Standard", "High + Volume Tier", "Extended Context" + isDefault: + type: boolean + description: >- + Whether this is the default tier. Exactly one tier per model must be + marked as default. + + + Requirements for default tier: + + - Must have isDefault=true + + - Must have priority=0 + + - Must have empty conditions array (conditions=[]) + + + The default tier acts as a fallback when no conditional tiers match. + priority: + type: integer + description: >- + Priority for tier matching evaluation. Lower numbers = higher + priority (evaluated first). + + + Must be unique within the model. The default tier must have + priority=0. + + Conditional tiers should use priority 1, 2, 3, etc. based on their + specificity. + conditions: + type: array + items: + $ref: '#/components/schemas/PricingTierCondition' + description: >- + Array of conditions that must ALL be met for this tier to match (AND + logic). + + + The default tier must have an empty array (conditions=[]). + + Conditional tiers should define one or more conditions that specify + when this tier's pricing applies. + + + Each condition specifies a regex pattern, operator, and threshold + value for matching against usage details. + prices: + type: object + additionalProperties: + type: number + format: double + description: >- + Prices (USD) by usage type for this tier. At least one price must be + defined. + + + Common usage types: "input", "output", "total", "request", "image" + + Prices are in USD per unit (e.g., per token). + + + Example: {"input": 0.000003, "output": 0.000015} represents $3 per + million input tokens and $15 per million output tokens. + required: + - name + - isDefault + - priority + - conditions + - prices + PricingTierOperator: + title: PricingTierOperator + type: string + enum: + - gt + - gte + - lt + - lte + - eq + - neq + description: Comparison operators for pricing tier conditions + ModelUsageUnit: + title: ModelUsageUnit + type: string + enum: + - CHARACTERS + - TOKENS + - MILLISECONDS + - SECONDS + - IMAGES + - REQUESTS + description: Unit of usage in Langfuse + ObservationLevel: + title: ObservationLevel + type: string + enum: + - DEBUG + - DEFAULT + - WARNING + - ERROR + MapValue: + title: MapValue + oneOf: + - type: string + nullable: true + - type: integer + nullable: true + - type: integer + format: float + nullable: true + - type: boolean + nullable: true + - type: array + items: + type: string + nullable: true + CommentObjectType: + title: CommentObjectType + type: string + enum: + - TRACE + - OBSERVATION + - SESSION + - PROMPT + DatasetStatus: + title: DatasetStatus + type: string + enum: + - ACTIVE + - ARCHIVED + ScoreSource: + title: ScoreSource + type: string + enum: + - ANNOTATION + - API + - EVAL + ScoreConfigDataType: + title: ScoreConfigDataType + type: string + enum: + - NUMERIC + - BOOLEAN + - CATEGORICAL + - TEXT + ScoreDataType: + title: ScoreDataType + type: string + enum: + - NUMERIC + - BOOLEAN + - CATEGORICAL + - CORRECTION + - TEXT + DeleteDatasetItemResponse: + title: DeleteDatasetItemResponse + type: object + properties: + message: + type: string + description: Success message after deletion + required: + - message + CreateDatasetItemRequest: + title: CreateDatasetItemRequest + type: object + properties: + datasetName: + type: string + input: + nullable: true + expectedOutput: + nullable: true + metadata: + nullable: true + sourceTraceId: + type: string + nullable: true + sourceObservationId: + type: string + nullable: true + id: + type: string + nullable: true + description: >- + Dataset items are upserted on their id. Id needs to be unique + (project-level) and cannot be reused across datasets. + status: + $ref: '#/components/schemas/DatasetStatus' + nullable: true + description: Defaults to ACTIVE for newly created items + required: + - datasetName + PaginatedDatasetItems: + title: PaginatedDatasetItems + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/DatasetItem' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + CreateDatasetRunItemRequest: + title: CreateDatasetRunItemRequest + type: object + properties: + runName: + type: string + runDescription: + type: string + nullable: true + description: Description of the run. If run exists, description will be updated. + metadata: + nullable: true + description: Metadata of the dataset run, updates run if run already exists + datasetItemId: + type: string + observationId: + type: string + nullable: true + traceId: + type: string + nullable: true + description: >- + traceId should always be provided. For compatibility with older SDK + versions it can also be inferred from the provided observationId. + datasetVersion: + type: string + format: date-time + nullable: true + description: >- + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., + "2026-01-21T14:35:42Z"). + + Specifies the dataset version to use for this experiment run. + + If provided, the experiment will use dataset items as they existed + at or before this timestamp. + + If not provided, uses the latest version of dataset items. + createdAt: + type: string + format: date-time + nullable: true + description: >- + Optional timestamp to set the createdAt field of the dataset run + item. If not provided or null, defaults to current timestamp. + required: + - runName + - datasetItemId + PaginatedDatasetRunItems: + title: PaginatedDatasetRunItems + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/DatasetRunItem' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + PaginatedDatasets: + title: PaginatedDatasets + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Dataset' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + CreateDatasetRequest: + title: CreateDatasetRequest + type: object + properties: + name: + type: string + description: + type: string + nullable: true + metadata: + nullable: true + inputSchema: + nullable: true + description: >- + JSON Schema for validating dataset item inputs. When set, all new + and existing dataset items will be validated against this schema. + expectedOutputSchema: + nullable: true + description: >- + JSON Schema for validating dataset item expected outputs. When set, + all new and existing dataset items will be validated against this + schema. + required: + - name + PaginatedDatasetRuns: + title: PaginatedDatasetRuns + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/DatasetRun' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + DeleteDatasetRunResponse: + title: DeleteDatasetRunResponse + type: object + properties: + message: + type: string + required: + - message + HealthResponse: + title: HealthResponse + type: object + properties: + version: + type: string + description: Langfuse server version + example: 1.25.0 + status: + type: string + example: OK + required: + - version + - status + IngestionEvent: + title: IngestionEvent + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - trace-create + - $ref: '#/components/schemas/TraceEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - score-create + - $ref: '#/components/schemas/ScoreEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - span-create + - $ref: '#/components/schemas/CreateSpanEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - span-update + - $ref: '#/components/schemas/UpdateSpanEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - generation-create + - $ref: '#/components/schemas/CreateGenerationEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - generation-update + - $ref: '#/components/schemas/UpdateGenerationEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - event-create + - $ref: '#/components/schemas/CreateEventEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - sdk-log + - $ref: '#/components/schemas/SDKLogEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - observation-create + - $ref: '#/components/schemas/CreateObservationEvent' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - observation-update + - $ref: '#/components/schemas/UpdateObservationEvent' + required: + - type + ObservationType: + title: ObservationType + type: string + enum: + - SPAN + - GENERATION + - EVENT + - AGENT + - TOOL + - CHAIN + - RETRIEVER + - EVALUATOR + - EMBEDDING + - GUARDRAIL + IngestionUsage: + title: IngestionUsage + oneOf: + - $ref: '#/components/schemas/Usage' + - $ref: '#/components/schemas/OpenAIUsage' + OpenAIUsage: + title: OpenAIUsage + type: object + description: Usage interface of OpenAI for improved compatibility. + properties: + promptTokens: + type: integer + nullable: true + completionTokens: + type: integer + nullable: true + totalTokens: + type: integer + nullable: true + OptionalObservationBody: + title: OptionalObservationBody + type: object + properties: + traceId: + type: string + nullable: true + name: + type: string + nullable: true + startTime: + type: string + format: date-time + nullable: true + metadata: + nullable: true + input: + nullable: true + output: + nullable: true + level: + $ref: '#/components/schemas/ObservationLevel' + nullable: true + statusMessage: + type: string + nullable: true + parentObservationId: + type: string + nullable: true + version: + type: string + nullable: true + environment: + type: string + nullable: true + CreateEventBody: + title: CreateEventBody + type: object + properties: + id: + type: string + nullable: true + allOf: + - $ref: '#/components/schemas/OptionalObservationBody' + UpdateEventBody: + title: UpdateEventBody + type: object + properties: + id: + type: string + required: + - id + allOf: + - $ref: '#/components/schemas/OptionalObservationBody' + CreateSpanBody: + title: CreateSpanBody + type: object + properties: + endTime: + type: string + format: date-time + nullable: true + allOf: + - $ref: '#/components/schemas/CreateEventBody' + UpdateSpanBody: + title: UpdateSpanBody + type: object + properties: + endTime: + type: string + format: date-time + nullable: true + allOf: + - $ref: '#/components/schemas/UpdateEventBody' + CreateGenerationBody: + title: CreateGenerationBody + type: object + properties: + completionStartTime: + type: string + format: date-time + nullable: true + model: + type: string + nullable: true + modelParameters: + type: object + additionalProperties: + $ref: '#/components/schemas/MapValue' + nullable: true + usage: + $ref: '#/components/schemas/IngestionUsage' + nullable: true + usageDetails: + $ref: '#/components/schemas/UsageDetails' + nullable: true + costDetails: + type: object + additionalProperties: + type: number + format: double + nullable: true + promptName: + type: string + nullable: true + promptVersion: + type: integer + nullable: true + allOf: + - $ref: '#/components/schemas/CreateSpanBody' + UpdateGenerationBody: + title: UpdateGenerationBody + type: object + properties: + completionStartTime: + type: string + format: date-time + nullable: true + model: + type: string + nullable: true + modelParameters: + type: object + additionalProperties: + $ref: '#/components/schemas/MapValue' + nullable: true + usage: + $ref: '#/components/schemas/IngestionUsage' + nullable: true + promptName: + type: string + nullable: true + usageDetails: + $ref: '#/components/schemas/UsageDetails' + nullable: true + costDetails: + type: object + additionalProperties: + type: number + format: double + nullable: true + promptVersion: + type: integer + nullable: true + allOf: + - $ref: '#/components/schemas/UpdateSpanBody' + ObservationBody: + title: ObservationBody + type: object + properties: + id: + type: string + nullable: true + traceId: + type: string + nullable: true + type: + $ref: '#/components/schemas/ObservationType' + name: + type: string + nullable: true + startTime: + type: string + format: date-time + nullable: true + endTime: + type: string + format: date-time + nullable: true + completionStartTime: + type: string + format: date-time + nullable: true + model: + type: string + nullable: true + modelParameters: + type: object + additionalProperties: + $ref: '#/components/schemas/MapValue' + nullable: true + input: + nullable: true + version: + type: string + nullable: true + metadata: + nullable: true + output: + nullable: true + usage: + $ref: '#/components/schemas/Usage' + nullable: true + level: + $ref: '#/components/schemas/ObservationLevel' + nullable: true + statusMessage: + type: string + nullable: true + parentObservationId: + type: string + nullable: true + environment: + type: string + nullable: true + required: + - type + TraceBody: + title: TraceBody + type: object + properties: + id: + type: string + nullable: true + timestamp: + type: string + format: date-time + nullable: true + name: + type: string + nullable: true + userId: + type: string + nullable: true + input: + nullable: true + output: + nullable: true + sessionId: + type: string + nullable: true + release: + type: string + nullable: true + version: + type: string + nullable: true + metadata: + nullable: true + tags: + type: array + items: + type: string + nullable: true + environment: + type: string + nullable: true + public: + type: boolean + nullable: true + description: Make trace publicly accessible via url + SDKLogBody: + title: SDKLogBody + type: object + properties: + log: {} + required: + - log + ScoreBody: + title: ScoreBody + type: object + properties: + id: + type: string + nullable: true + traceId: + type: string + nullable: true + sessionId: + type: string + nullable: true + observationId: + type: string + nullable: true + datasetRunId: + type: string + nullable: true + name: + type: string + description: >- + The name of the score. Always overrides "output" for correction + scores. + example: novelty + environment: + type: string + nullable: true + queueId: + type: string + nullable: true + description: >- + The annotation queue referenced by the score. Indicates if score was + initially created while processing annotation queue. + value: + $ref: '#/components/schemas/CreateScoreValue' + description: >- + The value of the score. Must be passed as string for categorical and + text scores, and numeric for boolean and numeric scores. Boolean + score values must equal either 1 or 0 (true or false). Text score + values must be between 1 and 500 characters. + comment: + type: string + nullable: true + metadata: + nullable: true + dataType: + $ref: '#/components/schemas/ScoreDataType' + nullable: true + description: >- + When set, must match the score value's type. If not set, will be + inferred from the score value or config + configId: + type: string + nullable: true + description: >- + Reference a score config on a score. When set, the score name must + equal the config name and scores must comply with the config's range + and data type. For categorical scores, the value must map to a + config category. Numeric scores might be constrained by the score + config's max and min values + required: + - name + - value + BaseEvent: + title: BaseEvent + type: object + properties: + id: + type: string + description: UUID v4 that identifies the event + timestamp: + type: string + description: >- + Datetime (ISO 8601) of event creation in client. Should be as close + to actual event creation in client as possible, this timestamp will + be used for ordering of events in future release. Resolution: + milliseconds (required), microseconds (optimal). + metadata: + nullable: true + description: Optional. Metadata field used by the Langfuse SDKs for debugging. + required: + - id + - timestamp + TraceEvent: + title: TraceEvent + type: object + properties: + body: + $ref: '#/components/schemas/TraceBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + CreateObservationEvent: + title: CreateObservationEvent + type: object + properties: + body: + $ref: '#/components/schemas/ObservationBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + UpdateObservationEvent: + title: UpdateObservationEvent + type: object + properties: + body: + $ref: '#/components/schemas/ObservationBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + ScoreEvent: + title: ScoreEvent + type: object + properties: + body: + $ref: '#/components/schemas/ScoreBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + SDKLogEvent: + title: SDKLogEvent + type: object + properties: + body: + $ref: '#/components/schemas/SDKLogBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + CreateGenerationEvent: + title: CreateGenerationEvent + type: object + properties: + body: + $ref: '#/components/schemas/CreateGenerationBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + UpdateGenerationEvent: + title: UpdateGenerationEvent + type: object + properties: + body: + $ref: '#/components/schemas/UpdateGenerationBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + CreateSpanEvent: + title: CreateSpanEvent + type: object + properties: + body: + $ref: '#/components/schemas/CreateSpanBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + UpdateSpanEvent: + title: UpdateSpanEvent + type: object + properties: + body: + $ref: '#/components/schemas/UpdateSpanBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + CreateEventEvent: + title: CreateEventEvent + type: object + properties: + body: + $ref: '#/components/schemas/CreateEventBody' + required: + - body + allOf: + - $ref: '#/components/schemas/BaseEvent' + IngestionSuccess: + title: IngestionSuccess + type: object + properties: + id: + type: string + status: + type: integer + required: + - id + - status + IngestionError: + title: IngestionError + type: object + properties: + id: + type: string + status: + type: integer + message: + type: string + nullable: true + error: + nullable: true + required: + - id + - status + IngestionResponse: + title: IngestionResponse + type: object + properties: + successes: + type: array + items: + $ref: '#/components/schemas/IngestionSuccess' + errors: + type: array + items: + $ref: '#/components/schemas/IngestionError' + required: + - successes + - errors + OpenAICompletionUsageSchema: + title: OpenAICompletionUsageSchema + type: object + description: OpenAI Usage schema from (Chat-)Completion APIs + properties: + prompt_tokens: + type: integer + completion_tokens: + type: integer + total_tokens: + type: integer + prompt_tokens_details: + type: object + additionalProperties: + type: integer + nullable: true + nullable: true + completion_tokens_details: + type: object + additionalProperties: + type: integer + nullable: true + nullable: true + required: + - prompt_tokens + - completion_tokens + - total_tokens + OpenAIResponseUsageSchema: + title: OpenAIResponseUsageSchema + type: object + description: OpenAI Usage schema from Response API + properties: + input_tokens: + type: integer + output_tokens: + type: integer + total_tokens: + type: integer + input_tokens_details: + type: object + additionalProperties: + type: integer + nullable: true + nullable: true + output_tokens_details: + type: object + additionalProperties: + type: integer + nullable: true + nullable: true + required: + - input_tokens + - output_tokens + - total_tokens + UsageDetails: + title: UsageDetails + oneOf: + - type: object + additionalProperties: + type: integer + - $ref: '#/components/schemas/OpenAICompletionUsageSchema' + - $ref: '#/components/schemas/OpenAIResponseUsageSchema' + legacyMetricsResponse: + title: legacyMetricsResponse + type: object + properties: + data: + type: array + items: + type: object + additionalProperties: true + description: >- + The metrics data. Each item in the list contains the metric values + and dimensions requested in the query. + + Format varies based on the query parameters. + + Histograms will return an array with [lower, upper, height] tuples. + required: + - data + legacyObservations: + title: legacyObservations + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Observation' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + legacyObservationsViews: + title: legacyObservationsViews + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ObservationsView' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + legacyCreateScoreRequest: + title: legacyCreateScoreRequest + type: object + properties: + id: + type: string + nullable: true + traceId: + type: string + nullable: true + sessionId: + type: string + nullable: true + observationId: + type: string + nullable: true + datasetRunId: + type: string + nullable: true + name: + type: string + example: novelty + value: + $ref: '#/components/schemas/CreateScoreValue' + description: >- + The value of the score. Must be passed as string for categorical and + text scores, and numeric for boolean and numeric scores. Boolean + score values must equal either 1 or 0 (true or false). Text score + values must be between 1 and 500 characters. + comment: + type: string + nullable: true + metadata: + type: object + additionalProperties: true + nullable: true + environment: + type: string + nullable: true + description: >- + The environment of the score. Can be any lowercase alphanumeric + string with hyphens and underscores that does not start with + 'langfuse'. + queueId: + type: string + nullable: true + description: >- + The annotation queue referenced by the score. Indicates if score was + initially created while processing annotation queue. + dataType: + $ref: '#/components/schemas/ScoreDataType' + nullable: true + description: >- + The data type of the score. When passing a configId this field is + inferred. Otherwise, this field must be passed or will default to + numeric. + configId: + type: string + nullable: true + description: >- + Reference a score config on a score. The unique langfuse identifier + of a score config. When passing this field, the dataType and + stringValue fields are automatically populated. + source: + $ref: '#/components/schemas/legacyCreateScoreSource' + nullable: true + description: >- + The source of the score. Defaults to API. Set to ANNOTATION to + prefill scores (e.g. from an LLM) for a human reviewer to verify in + an annotation queue. When source is ANNOTATION, a configId is + required unless dataType is CORRECTION. EVAL is reserved for + internal evaluator outputs and is not accepted on this endpoint. + required: + - name + - value + legacyCreateScoreSource: + title: legacyCreateScoreSource + type: string + enum: + - API + - ANNOTATION + description: |- + Source values accepted when creating a score via the public REST API. + EVAL is reserved for internal evaluator outputs and is intentionally not + exposed here β€” use commons.ScoreSource when reading scores. + legacyCreateScoreResponse: + title: legacyCreateScoreResponse + type: object + properties: + id: + type: string + description: The id of the created object in Langfuse + required: + - id + LlmConnection: + title: LlmConnection + type: object + description: LLM API connection configuration (secrets excluded) + properties: + id: + type: string + provider: + type: string + description: >- + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in + project, used for upserting. + adapter: + type: string + description: The adapter used to interface with the LLM + displaySecretKey: + type: string + description: Masked version of the secret key for display purposes + baseURL: + type: string + nullable: true + description: Custom base URL for the LLM API + customModels: + type: array + items: + type: string + description: List of custom model names available for this connection + withDefaultModels: + type: boolean + description: Whether to include default models for this adapter + extraHeaderKeys: + type: array + items: + type: string + description: >- + Keys of extra headers sent with requests (values excluded for + security) + config: + type: object + additionalProperties: true + nullable: true + description: >- + Adapter-specific configuration. Required for Bedrock + (`{"region":"us-east-1"}`), optional for VertexAI + (`{"location":"us-central1"}`), not used by other adapters. + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - provider + - adapter + - displaySecretKey + - customModels + - withDefaultModels + - extraHeaderKeys + - createdAt + - updatedAt + PaginatedLlmConnections: + title: PaginatedLlmConnections + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/LlmConnection' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + UpsertLlmConnectionRequest: + title: UpsertLlmConnectionRequest + type: object + description: Request to create or update an LLM connection (upsert) + properties: + provider: + type: string + description: >- + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in + project, used for upserting. + adapter: + $ref: '#/components/schemas/LlmAdapter' + description: The adapter used to interface with the LLM + secretKey: + type: string + description: Secret key for the LLM API. + baseURL: + type: string + nullable: true + description: Custom base URL for the LLM API + customModels: + type: array + items: + type: string + nullable: true + description: List of custom model names + withDefaultModels: + type: boolean + nullable: true + description: Whether to include default models. Default is true. + extraHeaders: + type: object + additionalProperties: + type: string + nullable: true + description: Extra headers to send with requests + config: + type: object + additionalProperties: true + nullable: true + description: >- + Adapter-specific configuration. Validation rules: - **Bedrock**: + Required. Must be `{"region": ""}` (e.g., + `{"region":"us-east-1"}`) - **VertexAI**: Optional. If provided, + must be `{"location": ""}` (e.g., + `{"location":"us-central1"}`) - **Other adapters**: Not supported. + Omit this field or set to null. + required: + - provider + - adapter + - secretKey + DeleteLlmConnectionResponse: + title: DeleteLlmConnectionResponse + type: object + properties: + message: + type: string + required: + - message + LlmAdapter: + title: LlmAdapter + type: string + enum: + - anthropic + - openai + - azure + - bedrock + - google-vertex-ai + - google-ai-studio + GetMediaResponse: + title: GetMediaResponse + type: object + properties: + mediaId: + type: string + description: The unique langfuse identifier of a media record + contentType: + type: string + description: The MIME type of the media record + contentLength: + type: integer + description: The size of the media record in bytes + uploadedAt: + type: string + format: date-time + description: The date and time when the media record was uploaded + url: + type: string + description: The download URL of the media record + urlExpiry: + type: string + description: The expiry date and time of the media record download URL + required: + - mediaId + - contentType + - contentLength + - uploadedAt + - url + - urlExpiry + PatchMediaBody: + title: PatchMediaBody + type: object + properties: + uploadedAt: + type: string + format: date-time + description: The date and time when the media record was uploaded + uploadHttpStatus: + type: integer + description: The HTTP status code of the upload + uploadHttpError: + type: string + nullable: true + description: The HTTP error message of the upload + uploadTimeMs: + type: integer + nullable: true + description: The time in milliseconds it took to upload the media record + required: + - uploadedAt + - uploadHttpStatus + GetMediaUploadUrlRequest: + title: GetMediaUploadUrlRequest + type: object + properties: + traceId: + type: string + description: The trace ID associated with the media record + observationId: + type: string + nullable: true + description: >- + The observation ID associated with the media record. If the media + record is associated directly with a trace, this will be null. + contentType: + $ref: '#/components/schemas/MediaContentType' + contentLength: + type: integer + description: The size of the media record in bytes + sha256Hash: + type: string + description: The SHA-256 hash of the media record + field: + type: string + description: >- + The trace / observation field the media record is associated with. + This can be one of `input`, `output`, `metadata` + required: + - traceId + - contentType + - contentLength + - sha256Hash + - field + GetMediaUploadUrlResponse: + title: GetMediaUploadUrlResponse + type: object + properties: + uploadUrl: + type: string + nullable: true + description: >- + The presigned upload URL. If the asset is already uploaded, this + will be null + mediaId: + type: string + description: The unique langfuse identifier of a media record + required: + - mediaId + MediaContentType: + title: MediaContentType + type: string + enum: + - image/png + - image/jpeg + - image/jpg + - image/webp + - image/gif + - image/svg+xml + - image/tiff + - image/bmp + - image/avif + - image/heic + - audio/mpeg + - audio/mp3 + - audio/wav + - audio/ogg + - audio/oga + - audio/aac + - audio/mp4 + - audio/flac + - audio/opus + - audio/webm + - video/mp4 + - video/webm + - video/ogg + - video/mpeg + - video/quicktime + - video/x-msvideo + - video/x-matroska + - text/plain + - text/html + - text/css + - text/csv + - text/markdown + - text/x-python + - application/javascript + - text/x-typescript + - application/x-yaml + - application/pdf + - application/msword + - application/vnd.ms-excel + - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + - application/zip + - application/json + - application/xml + - application/octet-stream + - >- + application/vnd.openxmlformats-officedocument.wordprocessingml.document + - >- + application/vnd.openxmlformats-officedocument.presentationml.presentation + - application/rtf + - application/x-ndjson + - application/vnd.apache.parquet + - application/gzip + - application/x-tar + - application/x-7z-compressed + description: The MIME type of the media record + MetricsV2Response: + title: MetricsV2Response + type: object + properties: + data: + type: array + items: + type: object + additionalProperties: true + description: >- + The metrics data. Each item in the list contains the metric values + and dimensions requested in the query. + + Format varies based on the query parameters. + + Histograms will return an array with [lower, upper, height] tuples. + required: + - data + PaginatedModels: + title: PaginatedModels + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Model' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + CreateModelRequest: + title: CreateModelRequest + type: object + properties: + modelName: + type: string + description: >- + Name of the model definition. If multiple with the same name exist, + they are applied in the following order: (1) custom over built-in, + (2) newest according to startTime where + model.startTime- + Regex pattern which matches this model definition to + generation.model. Useful in case of fine-tuned models. If you want + to exact match, use `(?i)^modelname$` + startDate: + type: string + format: date-time + nullable: true + description: Apply only to generations which are newer than this ISO date. + unit: + $ref: '#/components/schemas/ModelUsageUnit' + nullable: true + description: Unit used by this model. + inputPrice: + type: number + format: double + nullable: true + description: >- + Deprecated. Use 'pricingTiers' instead. Price (USD) per input unit. + Creates a default tier if pricingTiers not provided. + outputPrice: + type: number + format: double + nullable: true + description: >- + Deprecated. Use 'pricingTiers' instead. Price (USD) per output unit. + Creates a default tier if pricingTiers not provided. + totalPrice: + type: number + format: double + nullable: true + description: >- + Deprecated. Use 'pricingTiers' instead. Price (USD) per total units. + Cannot be set if input or output price is set. Creates a default + tier if pricingTiers not provided. + pricingTiers: + type: array + items: + $ref: '#/components/schemas/PricingTierInput' + nullable: true + description: >- + Optional. Array of pricing tiers for this model. + + + Use pricing tiers for all models - both those with threshold-based + pricing variations and those with simple flat pricing: + + + - For models with standard flat pricing: Create a single default + tier with your prices + (e.g., one tier with isDefault=true, priority=0, conditions=[], and your standard prices) + + - For models with threshold-based pricing: Create a default tier + plus additional conditional tiers + (e.g., default tier for standard usage + high-volume tier for usage above certain thresholds) + + Requirements: + + - Cannot be provided with flat prices + (inputPrice/outputPrice/totalPrice) - use one approach or the other + + - Must include exactly one default tier with isDefault=true, + priority=0, and conditions=[] + + - All tier names and priorities must be unique within the model + + - Each tier must define at least one price + + + If omitted, you must provide flat prices instead + (inputPrice/outputPrice/totalPrice), + + which will automatically create a single default tier named + "Standard". + tokenizerId: + type: string + nullable: true + description: >- + Optional. Tokenizer to be applied to observations which match to + this model. See docs for more details. + tokenizerConfig: + nullable: true + description: >- + Optional. Configuration for the selected tokenizer. Needs to be + JSON. See docs for more details. + required: + - modelName + - matchPattern + ObservationsV2Response: + title: ObservationsV2Response + type: object + description: >- + Response containing observations with field-group-based filtering and + cursor-based pagination. + + + The `data` array contains observation objects with only the requested + field groups included. + + Use the `cursor` in `meta` to retrieve the next page of results. + properties: + data: + type: array + items: + $ref: '#/components/schemas/ObservationV2' + description: >- + Array of observation objects. Fields included depend on the `fields` + parameter in the request. + meta: + $ref: '#/components/schemas/ObservationsV2Meta' + required: + - data + - meta + ObservationsV2Meta: + title: ObservationsV2Meta + type: object + description: Metadata for cursor-based pagination + properties: + cursor: + type: string + nullable: true + description: >- + Base64-encoded cursor to use for retrieving the next page. If not + present, there are no more results. + OtelResourceSpan: + title: OtelResourceSpan + type: object + description: >- + Represents a collection of spans from a single resource as per OTLP + specification + properties: + resource: + $ref: '#/components/schemas/OtelResource' + nullable: true + description: Resource information + scopeSpans: + type: array + items: + $ref: '#/components/schemas/OtelScopeSpan' + nullable: true + description: Array of scope spans + OtelResource: + title: OtelResource + type: object + description: Resource attributes identifying the source of telemetry + properties: + attributes: + type: array + items: + $ref: '#/components/schemas/OtelAttribute' + nullable: true + description: Resource attributes like service.name, service.version, etc. + OtelScopeSpan: + title: OtelScopeSpan + type: object + description: Collection of spans from a single instrumentation scope + properties: + scope: + $ref: '#/components/schemas/OtelScope' + nullable: true + description: Instrumentation scope information + spans: + type: array + items: + $ref: '#/components/schemas/OtelSpan' + nullable: true + description: Array of spans + OtelScope: + title: OtelScope + type: object + description: Instrumentation scope information + properties: + name: + type: string + nullable: true + description: Instrumentation scope name + version: + type: string + nullable: true + description: Instrumentation scope version + attributes: + type: array + items: + $ref: '#/components/schemas/OtelAttribute' + nullable: true + description: Additional scope attributes + OtelSpan: + title: OtelSpan + type: object + description: Individual span representing a unit of work or operation + properties: + traceId: + nullable: true + description: Trace ID (16 bytes, hex-encoded string in JSON or Buffer in binary) + spanId: + nullable: true + description: Span ID (8 bytes, hex-encoded string in JSON or Buffer in binary) + parentSpanId: + nullable: true + description: Parent span ID if this is a child span + name: + type: string + nullable: true + description: Span name describing the operation + kind: + type: integer + nullable: true + description: Span kind (1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER) + startTimeUnixNano: + nullable: true + description: Start time in nanoseconds since Unix epoch + endTimeUnixNano: + nullable: true + description: End time in nanoseconds since Unix epoch + attributes: + type: array + items: + $ref: '#/components/schemas/OtelAttribute' + nullable: true + description: >- + Span attributes including Langfuse-specific attributes + (langfuse.observation.*) + status: + nullable: true + description: Span status object + OtelAttribute: + title: OtelAttribute + type: object + description: Key-value attribute pair for resources, scopes, or spans + properties: + key: + type: string + nullable: true + description: Attribute key (e.g., "service.name", "langfuse.observation.type") + value: + $ref: '#/components/schemas/OtelAttributeValue' + nullable: true + description: Attribute value + OtelAttributeValue: + title: OtelAttributeValue + type: object + description: Attribute value wrapper supporting different value types + properties: + stringValue: + type: string + nullable: true + description: String value + intValue: + type: integer + nullable: true + description: Integer value + doubleValue: + type: number + format: double + nullable: true + description: Double value + boolValue: + type: boolean + nullable: true + description: Boolean value + OtelTraceResponse: + title: OtelTraceResponse + type: object + description: Response from trace export request. Empty object indicates success. + properties: {} + MembershipRole: + title: MembershipRole + type: string + enum: + - OWNER + - ADMIN + - MEMBER + - VIEWER + MembershipRequest: + title: MembershipRequest + type: object + properties: + userId: + type: string + role: + $ref: '#/components/schemas/MembershipRole' + required: + - userId + - role + DeleteMembershipRequest: + title: DeleteMembershipRequest + type: object + properties: + userId: + type: string + required: + - userId + MembershipResponse: + title: MembershipResponse + type: object + properties: + userId: + type: string + role: + $ref: '#/components/schemas/MembershipRole' + email: + type: string + name: + type: string + required: + - userId + - role + - email + - name + MembershipDeletionResponse: + title: MembershipDeletionResponse + type: object + properties: + message: + type: string + userId: + type: string + required: + - message + - userId + MembershipsResponse: + title: MembershipsResponse + type: object + properties: + memberships: + type: array + items: + $ref: '#/components/schemas/MembershipResponse' + required: + - memberships + OrganizationProject: + title: OrganizationProject + type: object + properties: + id: + type: string + name: + type: string + metadata: + type: object + additionalProperties: true + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - id + - name + - createdAt + - updatedAt + OrganizationProjectsResponse: + title: OrganizationProjectsResponse + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/OrganizationProject' + required: + - projects + OrganizationApiKey: + title: OrganizationApiKey + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + nullable: true + lastUsedAt: + type: string + format: date-time + nullable: true + note: + type: string + nullable: true + publicKey: + type: string + displaySecretKey: + type: string + required: + - id + - createdAt + - publicKey + - displaySecretKey + OrganizationApiKeysResponse: + title: OrganizationApiKeysResponse + type: object + properties: + apiKeys: + type: array + items: + $ref: '#/components/schemas/OrganizationApiKey' + required: + - apiKeys + Projects: + title: Projects + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Project' + required: + - data + Organization: + title: Organization + type: object + properties: + id: + type: string + description: The unique identifier of the organization + name: + type: string + description: The name of the organization + required: + - id + - name + Project: + title: Project + type: object + properties: + id: + type: string + name: + type: string + organization: + $ref: '#/components/schemas/Organization' + description: The organization this project belongs to + metadata: + type: object + additionalProperties: true + description: Metadata for the project + retentionDays: + type: integer + nullable: true + description: >- + Number of days to retain data. Null or 0 means no retention. Omitted + if no retention is configured. + required: + - id + - name + - organization + - metadata + ProjectDeletionResponse: + title: ProjectDeletionResponse + type: object + properties: + success: + type: boolean + message: + type: string + required: + - success + - message + ApiKeyList: + title: ApiKeyList + type: object + description: List of API keys for a project + properties: + apiKeys: + type: array + items: + $ref: '#/components/schemas/ApiKeySummary' + required: + - apiKeys + ApiKeySummary: + title: ApiKeySummary + type: object + description: Summary of an API key + properties: + id: + type: string + createdAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + nullable: true + lastUsedAt: + type: string + format: date-time + nullable: true + note: + type: string + nullable: true + publicKey: + type: string + displaySecretKey: + type: string + required: + - id + - createdAt + - publicKey + - displaySecretKey + ApiKeyResponse: + title: ApiKeyResponse + type: object + description: Response for API key creation + properties: + id: + type: string + createdAt: + type: string + format: date-time + publicKey: + type: string + secretKey: + type: string + displaySecretKey: + type: string + note: + type: string + nullable: true + required: + - id + - createdAt + - publicKey + - secretKey + - displaySecretKey + ApiKeyDeletionResponse: + title: ApiKeyDeletionResponse + type: object + description: Response for API key deletion + properties: + success: + type: boolean + required: + - success + PromptMetaListResponse: + title: PromptMetaListResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/PromptMeta' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + PromptMeta: + title: PromptMeta + type: object + properties: + name: + type: string + type: + $ref: '#/components/schemas/PromptType' + description: Indicates whether the prompt is a text or chat prompt. + versions: + type: array + items: + type: integer + labels: + type: array + items: + type: string + tags: + type: array + items: + type: string + lastUpdatedAt: + type: string + format: date-time + lastConfig: + description: >- + Config object of the most recent prompt version that matches the + filters (if any are provided) + required: + - name + - type + - versions + - labels + - tags + - lastUpdatedAt + - lastConfig + CreatePromptRequest: + title: CreatePromptRequest + oneOf: + - $ref: '#/components/schemas/CreateChatPromptRequest' + - $ref: '#/components/schemas/CreateTextPromptRequest' + CreateChatPromptRequest: + title: CreateChatPromptRequest + type: object + properties: + name: + type: string + prompt: + type: array + items: + $ref: '#/components/schemas/ChatMessageWithPlaceholders' + config: + nullable: true + type: + $ref: '#/components/schemas/CreateChatPromptType' + labels: + type: array + items: + type: string + nullable: true + description: List of deployment labels of this prompt version. + tags: + type: array + items: + type: string + nullable: true + description: List of tags to apply to all versions of this prompt. + commitMessage: + type: string + nullable: true + description: Commit message for this prompt version. + required: + - name + - prompt + - type + CreateTextPromptRequest: + title: CreateTextPromptRequest + type: object + properties: + name: + type: string + prompt: + type: string + config: + nullable: true + type: + $ref: '#/components/schemas/CreateTextPromptType' + nullable: true + labels: + type: array + items: + type: string + nullable: true + description: List of deployment labels of this prompt version. + tags: + type: array + items: + type: string + nullable: true + description: List of tags to apply to all versions of this prompt. + commitMessage: + type: string + nullable: true + description: Commit message for this prompt version. + required: + - name + - prompt + Prompt: + title: Prompt + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - chat + - $ref: '#/components/schemas/ChatPrompt' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - text + - $ref: '#/components/schemas/TextPrompt' + required: + - type + PromptType: + title: PromptType + type: string + enum: + - chat + - text + BasePrompt: + title: BasePrompt + type: object + properties: + name: + type: string + version: + type: integer + config: {} + labels: + type: array + items: + type: string + description: List of deployment labels of this prompt version. + tags: + type: array + items: + type: string + description: >- + List of tags. Used to filter via UI and API. The same across + versions of a prompt. + commitMessage: + type: string + nullable: true + description: Commit message for this prompt version. + resolutionGraph: + type: object + additionalProperties: true + nullable: true + description: >- + The dependency resolution graph for the current prompt. Null if the + prompt has no dependencies or if `resolve=false` was used. + required: + - name + - version + - config + - labels + - tags + ChatMessageWithPlaceholders: + title: ChatMessageWithPlaceholders + oneOf: + - $ref: '#/components/schemas/ChatMessage' + - $ref: '#/components/schemas/PlaceholderMessage' + ChatMessage: + title: ChatMessage + type: object + properties: + role: + type: string + content: + type: string + type: + $ref: '#/components/schemas/ChatMessageType' + nullable: true + required: + - role + - content + ChatMessageType: + title: ChatMessageType + type: string + enum: + - chatmessage + PlaceholderMessage: + title: PlaceholderMessage + type: object + properties: + name: + type: string + type: + $ref: '#/components/schemas/PlaceholderMessageType' + nullable: true + required: + - name + PlaceholderMessageType: + title: PlaceholderMessageType + type: string + enum: + - placeholder + TextPrompt: + title: TextPrompt + type: object + properties: + prompt: + type: string + required: + - prompt + allOf: + - $ref: '#/components/schemas/BasePrompt' + ChatPrompt: + title: ChatPrompt + type: object + properties: + prompt: + type: array + items: + $ref: '#/components/schemas/ChatMessageWithPlaceholders' + required: + - prompt + allOf: + - $ref: '#/components/schemas/BasePrompt' + CreateChatPromptType: + title: CreateChatPromptType + type: string + enum: + - chat + CreateTextPromptType: + title: CreateTextPromptType + type: string + enum: + - text + ServiceProviderConfig: + title: ServiceProviderConfig + type: object + properties: + schemas: + type: array + items: + type: string + documentationUri: + type: string + patch: + $ref: '#/components/schemas/ScimFeatureSupport' + bulk: + $ref: '#/components/schemas/BulkConfig' + filter: + $ref: '#/components/schemas/FilterConfig' + changePassword: + $ref: '#/components/schemas/ScimFeatureSupport' + sort: + $ref: '#/components/schemas/ScimFeatureSupport' + etag: + $ref: '#/components/schemas/ScimFeatureSupport' + authenticationSchemes: + type: array + items: + $ref: '#/components/schemas/AuthenticationScheme' + meta: + $ref: '#/components/schemas/ResourceMeta' + required: + - schemas + - documentationUri + - patch + - bulk + - filter + - changePassword + - sort + - etag + - authenticationSchemes + - meta + ScimFeatureSupport: + title: ScimFeatureSupport + type: object + properties: + supported: + type: boolean + required: + - supported + BulkConfig: + title: BulkConfig + type: object + properties: + supported: + type: boolean + maxOperations: + type: integer + maxPayloadSize: + type: integer + required: + - supported + - maxOperations + - maxPayloadSize + FilterConfig: + title: FilterConfig + type: object + properties: + supported: + type: boolean + maxResults: + type: integer + required: + - supported + - maxResults + ResourceMeta: + title: ResourceMeta + type: object + properties: + resourceType: + type: string + location: + type: string + required: + - resourceType + - location + AuthenticationScheme: + title: AuthenticationScheme + type: object + properties: + name: + type: string + description: + type: string + specUri: + type: string + type: + type: string + primary: + type: boolean + required: + - name + - description + - specUri + - type + - primary + ResourceTypesResponse: + title: ResourceTypesResponse + type: object + properties: + schemas: + type: array + items: + type: string + totalResults: + type: integer + Resources: + type: array + items: + $ref: '#/components/schemas/ResourceType' + required: + - schemas + - totalResults + - Resources + ResourceType: + title: ResourceType + type: object + properties: + schemas: + type: array + items: + type: string + nullable: true + id: + type: string + name: + type: string + endpoint: + type: string + description: + type: string + schema: + type: string + schemaExtensions: + type: array + items: + $ref: '#/components/schemas/SchemaExtension' + meta: + $ref: '#/components/schemas/ResourceMeta' + required: + - id + - name + - endpoint + - description + - schema + - schemaExtensions + - meta + SchemaExtension: + title: SchemaExtension + type: object + properties: + schema: + type: string + required: + type: boolean + required: + - schema + - required + SchemasResponse: + title: SchemasResponse + type: object + properties: + schemas: + type: array + items: + type: string + totalResults: + type: integer + Resources: + type: array + items: + $ref: '#/components/schemas/SchemaResource' + required: + - schemas + - totalResults + - Resources + SchemaResource: + title: SchemaResource + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + attributes: + type: array + items: {} + meta: + $ref: '#/components/schemas/ResourceMeta' + required: + - id + - name + - description + - attributes + - meta + ScimUsersListResponse: + title: ScimUsersListResponse + type: object + properties: + schemas: + type: array + items: + type: string + totalResults: + type: integer + startIndex: + type: integer + itemsPerPage: + type: integer + Resources: + type: array + items: + $ref: '#/components/schemas/ScimUser' + required: + - schemas + - totalResults + - startIndex + - itemsPerPage + - Resources + ScimUser: + title: ScimUser + type: object + properties: + schemas: + type: array + items: + type: string + id: + type: string + userName: + type: string + name: + $ref: '#/components/schemas/ScimName' + emails: + type: array + items: + $ref: '#/components/schemas/ScimEmail' + meta: + $ref: '#/components/schemas/UserMeta' + required: + - schemas + - id + - userName + - name + - emails + - meta + UserMeta: + title: UserMeta + type: object + properties: + resourceType: + type: string + created: + type: string + nullable: true + lastModified: + type: string + nullable: true + required: + - resourceType + ScimName: + title: ScimName + type: object + properties: + formatted: + type: string + nullable: true + ScimEmail: + title: ScimEmail + type: object + properties: + primary: + type: boolean + value: + type: string + type: + type: string + required: + - primary + - value + - type + EmptyResponse: + title: EmptyResponse + type: object + description: Empty response for 204 No Content responses + properties: {} + ScoreConfigs: + title: ScoreConfigs + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ScoreConfig' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + CreateScoreConfigRequest: + title: CreateScoreConfigRequest + type: object + properties: + name: + type: string + description: >- + Name of the score config. Max 35 characters. Only letters, numbers, + underscores, spaces, periods, parentheses, and hyphens are allowed. + dataType: + $ref: '#/components/schemas/ScoreConfigDataType' + categories: + type: array + items: + $ref: '#/components/schemas/ConfigCategory' + nullable: true + description: >- + Configure custom categories for categorical scores. Pass a list of + objects with `label` and `value` properties. Categories are + autogenerated for boolean configs and cannot be passed + minValue: + type: number + format: double + nullable: true + description: >- + Configure a minimum value for numerical scores. If not set, the + minimum value defaults to -∞ + maxValue: + type: number + format: double + nullable: true + description: >- + Configure a maximum value for numerical scores. If not set, the + maximum value defaults to +∞ + description: + type: string + nullable: true + description: >- + Description is shown across the Langfuse UI and can be used to e.g. + explain the config categories in detail, why a numeric range was + set, or provide additional context on config name or usage + required: + - name + - dataType + UpdateScoreConfigRequest: + title: UpdateScoreConfigRequest + type: object + properties: + isArchived: + type: boolean + nullable: true + description: The status of the score config showing if it is archived or not + name: + type: string + nullable: true + description: >- + Name of the score config. Max 35 characters. Only letters, numbers, + underscores, spaces, periods, parentheses, and hyphens are allowed. + categories: + type: array + items: + $ref: '#/components/schemas/ConfigCategory' + nullable: true + description: >- + Configure custom categories for categorical scores. Pass a list of + objects with `label` and `value` properties. Categories are + autogenerated for boolean configs and cannot be passed + minValue: + type: number + format: double + nullable: true + description: >- + Configure a minimum value for numerical scores. If not set, the + minimum value defaults to -∞ + maxValue: + type: number + format: double + nullable: true + description: >- + Configure a maximum value for numerical scores. If not set, the + maximum value defaults to +∞ + description: + type: string + nullable: true + description: >- + Description is shown across the Langfuse UI and can be used to e.g. + explain the config categories in detail, why a numeric range was + set, or provide additional context on config name or usage + GetScoresResponseTraceData: + title: GetScoresResponseTraceData + type: object + properties: + userId: + type: string + nullable: true + description: The user ID associated with the trace referenced by score + tags: + type: array + items: + type: string + nullable: true + description: A list of tags associated with the trace referenced by score + environment: + type: string + nullable: true + description: The environment of the trace referenced by score + sessionId: + type: string + nullable: true + description: The session ID associated with the trace referenced by score + GetScoresResponseDataNumeric: + title: GetScoresResponseDataNumeric + type: object + properties: + trace: + $ref: '#/components/schemas/GetScoresResponseTraceData' + nullable: true + allOf: + - $ref: '#/components/schemas/NumericScore' + GetScoresResponseDataCategorical: + title: GetScoresResponseDataCategorical + type: object + properties: + trace: + $ref: '#/components/schemas/GetScoresResponseTraceData' + nullable: true + allOf: + - $ref: '#/components/schemas/CategoricalScore' + GetScoresResponseDataBoolean: + title: GetScoresResponseDataBoolean + type: object + properties: + trace: + $ref: '#/components/schemas/GetScoresResponseTraceData' + nullable: true + allOf: + - $ref: '#/components/schemas/BooleanScore' + GetScoresResponseDataCorrection: + title: GetScoresResponseDataCorrection + type: object + properties: + trace: + $ref: '#/components/schemas/GetScoresResponseTraceData' + nullable: true + allOf: + - $ref: '#/components/schemas/CorrectionScore' + GetScoresResponseDataText: + title: GetScoresResponseDataText + type: object + properties: + trace: + $ref: '#/components/schemas/GetScoresResponseTraceData' + nullable: true + allOf: + - $ref: '#/components/schemas/TextScore' + GetScoresResponseData: + title: GetScoresResponseData + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: '#/components/schemas/GetScoresResponseDataNumeric' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: '#/components/schemas/GetScoresResponseDataCategorical' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: '#/components/schemas/GetScoresResponseDataBoolean' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CORRECTION + - $ref: '#/components/schemas/GetScoresResponseDataCorrection' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - TEXT + - $ref: '#/components/schemas/GetScoresResponseDataText' + required: + - dataType + GetScoresResponse: + title: GetScoresResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/GetScoresResponseData' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + PaginatedSessions: + title: PaginatedSessions + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Session' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + Traces: + title: Traces + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/TraceWithDetails' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + DeleteTraceResponse: + title: DeleteTraceResponse + type: object + properties: + message: + type: string + required: + - message + Sort: + title: Sort + type: object + properties: + id: + type: string + required: + - id + unstableEvaluatorType: + title: unstableEvaluatorType + type: string + enum: + - llm_as_judge + description: >- + The evaluator engine type. + + + The unstable public API currently supports only LLM-as-a-judge + evaluators. + unstableEvaluatorScope: + title: unstableEvaluatorScope + type: string + enum: + - project + - managed + description: |- + Where an evaluator comes from. + + - `project`: created in your project + - `managed`: provided by Langfuse + unstableEvaluationRuleTarget: + title: unstableEvaluationRuleTarget + type: string + enum: + - observation + - experiment + description: >- + The ingestion object type that should trigger evaluation runs. + + + Choose the target first, because it changes both the valid filter + columns and the valid variable-mapping sources: + + - `observation` evaluates live-ingested observations such as + generations, spans, and events. + It supports mapping from `input`, `output`, and `metadata`. + - `experiment` evaluates live experiment executions and can additionally + map `expected_output` and `experiment_item_metadata`. + It currently supports filtering by `datasetId`. + Discover valid dataset IDs with `GET /api/public/v2/datasets`, then use the returned dataset `id` values in your filter. + unstableEvaluationRuleStatus: + title: unstableEvaluationRuleStatus + type: string + enum: + - active + - inactive + - paused + description: >- + Effective runtime status of the evaluation rule. + + + - `active`: enabled and currently runnable. + + - `inactive`: disabled by configuration. + + - `paused`: enabled, but Langfuse has blocked execution until the + underlying issue is resolved. + unstableEvaluationRuleMappingSource: + title: unstableEvaluationRuleMappingSource + type: string + enum: + - input + - output + - metadata + - expected_output + - experiment_item_metadata + description: >- + Source field used to populate a prompt variable. + + + Use these values when mapping evaluator prompt variables to live data. + + + Target-specific rules: + + - `target=observation` supports `input`, `output`, and `metadata` + + - `target=experiment` supports `input`, `output`, `metadata`, + `expected_output`, and `experiment_item_metadata` + + + Source semantics: + + - `input`: the observation or experiment input payload + + - `output`: the observation or experiment output payload + + - `metadata`: the metadata object for the target. Combine with + `jsonPath` when you need one nested field instead of the whole object. + + - `expected_output`: the experiment item's expected output. Only valid + for `target=experiment`. + + - `experiment_item_metadata`: the experiment item's metadata object. + Only valid for `target=experiment`. + unstableEvaluatorModelConfig: + title: unstableEvaluatorModelConfig + type: object + description: >- + Optional explicit model configuration for an evaluator. + + + If omitted, Langfuse uses the project's default evaluation model. + + If provided, the model must be available to the project when the + evaluator or evaluation rule is enabled. + + + To discover valid configured `provider` values for a project, call `GET + /api/public/llm-connections` and read the `provider` field from the + returned connections. + + Use a `provider` value that matches one of the connections already + configured in the same project. + + + Recovery guidance: + + - If evaluator creation returns `422` with + `code=evaluator_preflight_failed`, either provide a valid explicit + `modelConfig` here or configure the project's default evaluation model, + then retry the same request. + properties: + provider: + type: string + description: >- + Provider identifier to use for this evaluator, for example `openai` + or `anthropic`. + + + To discover valid values for the current project, call `GET + /api/public/llm-connections` and use one of the returned `provider` + values. + example: openai + model: + type: string + description: >- + Model identifier exposed by the provider, for example + `gpt-4.1-mini`. + example: gpt-4.1-mini + required: + - provider + - model + unstableEvaluatorOutputDataType: + title: unstableEvaluatorOutputDataType + type: string + enum: + - NUMERIC + - BOOLEAN + - CATEGORICAL + description: >- + Structured score type returned by an evaluator. + + + This controls the type of score value Langfuse stores for evaluation + results: + + - `NUMERIC`: a numeric score such as `0.82` + + - `BOOLEAN`: a boolean score such as `true` + + - `CATEGORICAL`: one or more category labels from a fixed list + unstableEvaluatorOutputFieldDefinition: + title: unstableEvaluatorOutputFieldDefinition + type: object + properties: + description: + type: string + description: >- + Human-readable instructions for what the evaluator should return in + this field. + required: + - description + unstableEvaluatorOutputDefinition: + title: unstableEvaluatorOutputDefinition + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: >- + #/components/schemas/unstablePublicNumericEvaluatorOutputDefinition + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: >- + #/components/schemas/unstablePublicBooleanEvaluatorOutputDefinition + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: >- + #/components/schemas/unstablePublicCategoricalEvaluatorOutputDefinition + required: + - dataType + description: >- + Structured output definition to send when creating an evaluator. + + + Agent guidance: + + - `dataType` is required. + + - Do not send `version`; that is an internal storage detail and is not + part of the public request contract. + + - For `NUMERIC` and `BOOLEAN`, provide `reasoning.description` and + `score.description`. + + - For `CATEGORICAL`, also provide `score.categories` and + `score.shouldAllowMultipleMatches`. + unstablePublicNumericEvaluatorOutputDefinition: + title: unstablePublicNumericEvaluatorOutputDefinition + type: object + properties: + dataType: + $ref: '#/components/schemas/unstableEvaluatorOutputDataType' + description: Always `NUMERIC`. + reasoning: + $ref: '#/components/schemas/unstableEvaluatorOutputFieldDefinition' + score: + $ref: '#/components/schemas/unstableEvaluatorOutputFieldDefinition' + required: + - dataType + - reasoning + - score + unstablePublicBooleanEvaluatorOutputDefinition: + title: unstablePublicBooleanEvaluatorOutputDefinition + type: object + properties: + dataType: + $ref: '#/components/schemas/unstableEvaluatorOutputDataType' + description: Always `BOOLEAN`. + reasoning: + $ref: '#/components/schemas/unstableEvaluatorOutputFieldDefinition' + score: + $ref: '#/components/schemas/unstableEvaluatorOutputFieldDefinition' + required: + - dataType + - reasoning + - score + unstablePublicCategoricalEvaluatorOutputScoreDefinition: + title: unstablePublicCategoricalEvaluatorOutputScoreDefinition + type: object + properties: + description: + type: string + categories: + type: array + items: + type: string + shouldAllowMultipleMatches: + type: boolean + required: + - description + - categories + - shouldAllowMultipleMatches + unstablePublicCategoricalEvaluatorOutputDefinition: + title: unstablePublicCategoricalEvaluatorOutputDefinition + type: object + properties: + dataType: + $ref: '#/components/schemas/unstableEvaluatorOutputDataType' + description: Always `CATEGORICAL`. + reasoning: + $ref: '#/components/schemas/unstableEvaluatorOutputFieldDefinition' + score: + $ref: >- + #/components/schemas/unstablePublicCategoricalEvaluatorOutputScoreDefinition + required: + - dataType + - reasoning + - score + unstablePublicEvaluatorOutputDefinition: + title: unstablePublicEvaluatorOutputDefinition + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: >- + #/components/schemas/unstablePublicNumericEvaluatorOutputDefinition + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: >- + #/components/schemas/unstablePublicBooleanEvaluatorOutputDefinition + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: >- + #/components/schemas/unstablePublicCategoricalEvaluatorOutputDefinition + required: + - dataType + description: >- + Evaluator output definition returned by the public API. + + + This response always includes `dataType` and never includes an internal + output-definition `version`. + + Legacy stored evaluator definitions are normalized into this shape + before they are returned. + + + Use this response shape when deciding how to interpret future evaluation + scores: + + - `NUMERIC`: expect numeric score values + + - `BOOLEAN`: expect `true` / `false` + + - `CATEGORICAL`: expect one or more values from `score.categories` + unstableEvaluationRuleStringFilterOperator: + title: unstableEvaluationRuleStringFilterOperator + type: string + enum: + - '=' + - contains + - does not contain + - starts with + - ends with + unstableEvaluationRuleNumberFilterOperator: + title: unstableEvaluationRuleNumberFilterOperator + type: string + enum: + - '=' + - '>' + - < + - '>=' + - <= + unstableEvaluationRuleOptionsFilterOperator: + title: unstableEvaluationRuleOptionsFilterOperator + type: string + enum: + - any of + - none of + unstableEvaluationRuleArrayOptionsFilterOperator: + title: unstableEvaluationRuleArrayOptionsFilterOperator + type: string + enum: + - any of + - none of + - all of + unstableEvaluationRuleBooleanFilterOperator: + title: unstableEvaluationRuleBooleanFilterOperator + type: string + enum: + - '=' + - <> + unstableEvaluationRuleNullFilterOperator: + title: unstableEvaluationRuleNullFilterOperator + type: string + enum: + - is null + - is not null + unstableDateTimeEvaluationRuleFilter: + title: unstableDateTimeEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleNumberFilterOperator' + description: Comparison operator for datetime values. + value: + type: string + format: date-time + description: Datetime value to compare against. + required: + - column + - operator + - value + unstableStringEvaluationRuleFilter: + title: unstableStringEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleStringFilterOperator' + value: + type: string + required: + - column + - operator + - value + unstableNumberEvaluationRuleFilter: + title: unstableNumberEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleNumberFilterOperator' + value: + type: number + format: double + required: + - column + - operator + - value + unstableStringOptionsEvaluationRuleFilter: + title: unstableStringOptionsEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleOptionsFilterOperator' + value: + type: array + items: + type: string + description: One or more allowed string values. + required: + - column + - operator + - value + unstableArrayOptionsEvaluationRuleFilter: + title: unstableArrayOptionsEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: >- + #/components/schemas/unstableEvaluationRuleArrayOptionsFilterOperator + value: + type: array + items: + type: string + description: One or more array elements to match. + required: + - column + - operator + - value + unstableStringObjectEvaluationRuleFilter: + title: unstableStringObjectEvaluationRuleFilter + type: object + properties: + column: + type: string + description: >- + Object-valued column to filter on. In the unstable public API this + is currently `metadata`. + key: + type: string + description: Top-level key inside the object-valued column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleStringFilterOperator' + value: + type: string + required: + - column + - key + - operator + - value + unstableNumberObjectEvaluationRuleFilter: + title: unstableNumberObjectEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Object-valued column to filter on. + key: + type: string + description: Key inside the object-valued column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleNumberFilterOperator' + value: + type: number + format: double + required: + - column + - key + - operator + - value + unstableCategoryOptionsEvaluationRuleFilter: + title: unstableCategoryOptionsEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Object-valued column to filter on. + key: + type: string + description: Key inside the object-valued column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleOptionsFilterOperator' + value: + type: array + items: + type: string + required: + - column + - key + - operator + - value + unstableBooleanEvaluationRuleFilter: + title: unstableBooleanEvaluationRuleFilter + type: object + properties: + column: + type: string + description: Column to filter on. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleBooleanFilterOperator' + value: + type: boolean + required: + - column + - operator + - value + unstableNullEvaluationRuleFilter: + title: unstableNullEvaluationRuleFilter + type: object + properties: + column: + type: string + description: >- + Column to filter on. In the unstable public API this is currently + `parentObservationId`. + operator: + $ref: '#/components/schemas/unstableEvaluationRuleNullFilterOperator' + value: + type: string + nullable: true + description: >- + Ignored placeholder value. Clients may omit it or send an empty + string. + required: + - column + - operator + unstableEvaluationRuleMapping: + title: unstableEvaluationRuleMapping + type: object + description: >- + Maps one evaluator prompt variable to one source field from the target + object. + + + How to build a valid mapping list: + + 1. Create the evaluator or fetch it with `GET /evaluators/{id}`. + + 2. Read the evaluator `variables` array. + + 3. Add exactly one mapping object for each variable in that array. + + 4. Use the variable name exactly as returned, without braces such as + `{{` or `}}`. + + 5. Choose a `source` that is valid for the selected `target`. + + + `jsonPath` is optional. Use it only when the selected source is a JSON + object and you want to extract one nested field before inserting it into + the evaluator prompt. + + + Recovery guidance: + + - `invalid_variable_mapping`: the variable name is unknown for this + evaluator, or the selected `source` is not valid for the chosen `target` + + - `missing_variable_mapping`: one or more evaluator variables are not + mapped yet + + - `duplicate_variable_mapping`: the same evaluator variable appears more + than once + + - `invalid_json_path`: the JSONPath expression is malformed. Remove it + or correct it. + properties: + variable: + type: string + description: >- + Prompt variable name without braces. + + + Example: for the prompt `Judge {{input}} against {{output}}`, use + `input` and `output`. + example: input + source: + $ref: '#/components/schemas/unstableEvaluationRuleMappingSource' + description: >- + Source field that should populate the prompt variable. + + + Quick reference: + + - `target=observation`: `input`, `output`, `metadata` + + - `target=experiment`: `input`, `output`, `metadata`, + `expected_output`, `experiment_item_metadata` + jsonPath: + type: string + nullable: true + description: >- + Optional JSONPath selector applied to the selected source before it + is passed to the evaluator prompt. + + + Requirements: + + - Must start with `$` + + - Must be a syntactically valid JSONPath expression + + - Most useful with `source=metadata` + required: + - variable + - source + unstableEvaluationRuleFilter: + title: unstableEvaluationRuleFilter + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - datetime + - $ref: '#/components/schemas/unstableDateTimeEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - string + - $ref: '#/components/schemas/unstableStringEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - number + - $ref: '#/components/schemas/unstableNumberEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - stringOptions + - $ref: '#/components/schemas/unstableStringOptionsEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - categoryOptions + - $ref: '#/components/schemas/unstableCategoryOptionsEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - arrayOptions + - $ref: '#/components/schemas/unstableArrayOptionsEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - stringObject + - $ref: '#/components/schemas/unstableStringObjectEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - numberObject + - $ref: '#/components/schemas/unstableNumberObjectEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - boolean + - $ref: '#/components/schemas/unstableBooleanEvaluationRuleFilter' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - 'null' + - $ref: '#/components/schemas/unstableNullEvaluationRuleFilter' + required: + - type + description: >- + One filter condition used to decide whether a live-ingested target + should be evaluated. + + + An evaluation rule can include zero or more filter objects. All filters + must be satisfied for the target to run. + + + How to build a valid filter object: + + - Pick the `target` first, because it changes the supported columns. + + - Pick the filter `type`. That determines which fields are required. + + - Use `key` only for object filters such as `metadata`. + + - Use the correct `value` shape for the chosen filter `type`. + + + Operator quick reference by filter `type`: + + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends + with` + + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + + - `stringOptions`: `any of`, `none of` + + - `arrayOptions`: `any of`, `none of`, `all of` + + - `stringObject`: same operators as `string` + + - `null`: `is null`, `is not null` + + + Supported columns by target: + + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + + - `invalid_filter_value` with `details.column` but no `invalidValues`: + the selected `column` is not supported for the chosen `target` + + - `invalid_filter_value` with `details.invalidValues`: the selected + values are not allowed for that column. Replace them with one of + `details.allowedValues` when provided. + + - `invalid_filter_value` for `column=datasetId`: call `GET + /api/public/v2/datasets`, then retry with dataset `id` values from that + response. + unstablePublicApiErrorCode: + title: unstablePublicApiErrorCode + type: string + enum: + - authentication_failed + - access_denied + - invalid_request + - invalid_query + - invalid_body + - invalid_filter_value + - invalid_json_path + - invalid_variable_mapping + - missing_variable_mapping + - duplicate_variable_mapping + - resource_not_found + - name_conflict + - evaluator_preflight_failed + - conflict + - unprocessable_content + - rate_limited + - method_not_allowed + - internal_error + description: >- + Machine-readable error code returned by the unstable evaluators API. + + + SDKs, CLIs, and agents should branch on `code` rather than parsing the + human-readable `message`. + + The HTTP status still indicates the broad error class, while `code` + gives the specific failure reason. + unstablePublicApiValidationIssue: + title: unstablePublicApiValidationIssue + type: object + description: >- + One validation issue returned for malformed request bodies or query + parameters. + + + This mirrors the most important parts of a Zod issue: a machine-readable + `code`, + + a human-readable `message`, and a structured `path`. + properties: + code: + type: string + description: >- + Machine-readable validation issue code emitted by the server + validator. + message: + type: string + description: Human-readable explanation of the validation failure. + path: + type: array + items: {} + description: Path to the invalid field, for example `["mapping", 0, "jsonPath"]`. + required: + - code + - message + - path + unstablePublicApiErrorDetails: + title: unstablePublicApiErrorDetails + type: object + description: >- + Optional structured context attached to an unstable-evals error. + + + The populated fields depend on the error `code`: + + - request parsing failures populate `issues` + + - filter validation failures populate `field`, `column`, + `invalidValues`, and `allowedValues` + + - variable mapping failures populate `field`, `variable`, or `variables` + + - JSONPath validation failures populate `field`, `variable`, and `value` + + - evaluator preflight failures populate `evaluatorName`, `provider`, and + `model` + + - rate limiting populates `retryAfterSeconds`, `limit`, `remaining`, and + `resetAt` + properties: + issues: + type: array + items: + $ref: '#/components/schemas/unstablePublicApiValidationIssue' + nullable: true + description: Validation issues for malformed request bodies or query parameters. + field: + type: string + nullable: true + description: >- + Path-like reference to the failing field, for example + `mapping[1].jsonPath`. + column: + type: string + nullable: true + description: Filter column that failed validation. + invalidValues: + type: array + items: + type: string + nullable: true + description: Unsupported values supplied by the caller. + allowedValues: + type: array + items: + type: string + nullable: true + description: Allowed values for the failing filter column. + variable: + type: string + nullable: true + description: Evaluator variable involved in the failure. + variables: + type: array + items: + type: string + nullable: true + description: >- + Multiple evaluator variables involved in the failure, for example + missing mappings. + value: + type: string + nullable: true + description: Raw invalid value supplied by the caller. + evaluatorName: + type: string + nullable: true + description: Evaluator name used during preflight validation. + provider: + type: string + nullable: true + description: Provider resolved during evaluator preflight, if any. + model: + type: string + nullable: true + description: Model resolved during evaluator preflight, if any. + retryAfterSeconds: + type: integer + nullable: true + description: Suggested retry delay for rate-limited requests. + limit: + type: integer + nullable: true + description: >- + Numeric limit associated with the failure, for example the active + evaluation-rule cap or the current rate-limit window. + remaining: + type: integer + nullable: true + description: Remaining requests in the current rate-limit window. + resetAt: + type: string + nullable: true + description: ISO-8601 timestamp when the current rate-limit window resets. + unstablePublicApiError: + title: unstablePublicApiError + type: object + description: >- + Standard error envelope for the unstable evaluators API. + + + Response handling guidance: + + - Use the HTTP status code for the broad class of failure. + + - Use `code` for precise branching in SDKs, CLIs, or agents. + + - Inspect `details` for field-level validation context such as invalid + filter values, malformed JSONPath expressions, or missing variable + mappings. + + - Retry only after fixing the specific issue described by `code` and + `details`. + properties: + message: + type: string + description: Human-readable description of the failure. + example: 'Filter column "type" contains unsupported value(s): INVALID' + code: + $ref: '#/components/schemas/unstablePublicApiErrorCode' + description: Stable machine-readable error code. + details: + $ref: '#/components/schemas/unstablePublicApiErrorDetails' + nullable: true + description: >- + Optional structured error context. Inspect the populated fields + based on `code`. + required: + - message + - code + unstableEvaluationRule: + title: unstableEvaluationRule + type: object + description: >- + Live evaluation rule for incoming data. + + + An evaluation rule answers: + + - which evaluator should be used + + - which target objects should trigger scoring + + - how often scoring should run + + - which target fields should populate each evaluator variable + + - whether the deployment is active, inactive, or paused + + + Important status semantics: + + - `enabled` is the desired on/off setting from the client + + - `status` is the effective runtime state after Langfuse applies + validation and blocking rules + + - `enabled=true` with `status=paused` means the rule should run, but + Langfuse has paused it until the underlying problem is fixed + properties: + id: + type: string + description: Stable evaluation rule identifier. + example: erule_123 + name: + type: string + description: >- + Human-readable deployment name. This is independent from the + evaluator name. + example: answer-correctness-live + evaluator: + $ref: '#/components/schemas/unstableEvaluationRuleEvaluator' + description: >- + Evaluator currently used by this rule. + + + `name` and `scope` identify the evaluator family conceptually. + + `id` is the currently active evaluator version in that family. + + If you create a newer project version with the same evaluator name + later, existing evaluation rules are moved to it automatically. + target: + $ref: '#/components/schemas/unstableEvaluationRuleTarget' + description: Target object type that should trigger scoring. + enabled: + type: boolean + description: Desired enabled state configured by the client. + example: true + status: + $ref: '#/components/schemas/unstableEvaluationRuleStatus' + description: >- + Effective runtime status after Langfuse applies validation and + blocking rules. + pausedReason: + type: string + nullable: true + description: Machine-readable reason when `status=paused`, otherwise `null`. + pausedMessage: + type: string + nullable: true + description: Human-readable explanation when `status=paused`, otherwise `null`. + sampling: + type: number + format: double + description: |- + Fraction of matching target objects that should be evaluated. + + Must be greater than `0` and less than or equal to `1`. + - `1` means evaluate every matching target. + - `0.25` means evaluate approximately 25% of matching targets. + example: 1 + filter: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleFilter' + description: >- + List of filter conditions used to decide whether a target should be + evaluated. + mapping: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleMapping' + description: >- + Variable mappings used to populate the evaluator prompt from the + live target object. + createdAt: + type: string + format: date-time + description: Timestamp when the evaluation rule was created. + example: '2026-03-30T09:20:00.000Z' + updatedAt: + type: string + format: date-time + description: Timestamp when the evaluation rule was last updated. + example: '2026-03-30T09:20:00.000Z' + required: + - id + - name + - evaluator + - target + - enabled + - status + - sampling + - filter + - mapping + - createdAt + - updatedAt + unstableEvaluationRules: + title: unstableEvaluationRules + type: object + description: Paginated list of evaluation rules. + properties: + data: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRule' + description: Evaluation rules in the current page. + meta: + $ref: '#/components/schemas/utilsMetaResponse' + description: Standard pagination metadata. + required: + - data + - meta + unstableCreateEvaluationRuleRequest: + title: unstableCreateEvaluationRuleRequest + type: object + description: >- + Request body for creating an evaluation rule. + + + Checklist for agents and SDK clients: + + - reference an existing evaluator family by `evaluator.name` and + `evaluator.scope` + + - choose `target=observation` or `target=experiment` + + - if `target=experiment` and you want a dataset filter, call `GET + /api/public/v2/datasets` first and use dataset `id` values in + `filter[].value` + + - fetch or inspect the evaluator first, then provide a complete variable + mapping for every evaluator variable listed in `variables` + + - optionally narrow execution with `filter` + + - set `enabled=true` only when you want live execution immediately + properties: + name: + type: string + description: Human-readable deployment name. + example: answer-correctness-live + evaluator: + $ref: '#/components/schemas/unstableEvaluationRuleEvaluatorReference' + description: >- + Evaluator family to use. + + + Use `name` and `scope` from the evaluator endpoints. + + Langfuse resolves that family to its latest version before saving + the rule. + target: + $ref: '#/components/schemas/unstableEvaluationRuleTarget' + description: Target object type to evaluate. + enabled: + type: boolean + description: Whether the deployment should be active immediately after creation. + example: true + sampling: + type: number + format: double + nullable: true + description: Optional sampling fraction. Defaults to `1`. + filter: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleFilter' + nullable: true + description: >- + Optional filter list. + + + Omit or pass an empty list to evaluate all matching targets for the + selected `target`. + + Each filter object must use a column that is valid for that + `target`. + + For `target=experiment`, `column=datasetId` expects dataset `id` + values from `GET /api/public/v2/datasets`, not dataset names. + mapping: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleMapping' + description: >- + Required variable mappings. + + + Every evaluator variable must appear exactly once. + + Build this list from the evaluator `variables` array returned by the + evaluator endpoints. + required: + - name + - evaluator + - target + - enabled + - mapping + unstableUpdateEvaluationRuleRequest: + title: unstableUpdateEvaluationRuleRequest + type: object + description: >- + Partial update body for an evaluation rule. + + + Provide only the fields you want to change. + + An empty body is rejected. + + + Practical guidance: + + - If you only want to rename the rule or change sampling, send just + those fields. + + - If you change `evaluator`, send a fresh `mapping` unless you are + certain the existing mapping still matches the evaluator variables. + + - If you change `target`, usually send both `filter` and `mapping` in + the same request. + + - If you change an experiment `datasetId` filter, call `GET + /api/public/v2/datasets` and use dataset `id` values from that response. + properties: + name: + type: string + nullable: true + description: Updated deployment name. + evaluator: + $ref: '#/components/schemas/unstableEvaluationRuleEvaluatorReference' + nullable: true + description: >- + Updated evaluator family. + + + Langfuse resolves the provided evaluator family to its latest + version before saving the rule. + target: + $ref: '#/components/schemas/unstableEvaluationRuleTarget' + nullable: true + description: Updated target object type. + enabled: + type: boolean + nullable: true + description: Updated desired enabled state. + sampling: + type: number + format: double + nullable: true + description: Updated sampling fraction. + filter: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleFilter' + nullable: true + description: >- + Updated filter list. + + + For `target=experiment`, `column=datasetId` expects dataset `id` + values from `GET /api/public/v2/datasets`, not dataset names. + mapping: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleMapping' + nullable: true + description: Updated variable mappings. + unstableDeleteEvaluationRuleResponse: + title: unstableDeleteEvaluationRuleResponse + type: object + description: Confirmation response returned after successful deletion. + properties: + message: + type: string + description: Always `Evaluation rule successfully deleted`. + required: + - message + unstableEvaluationRuleEvaluatorReference: + title: unstableEvaluationRuleEvaluatorReference + type: object + description: >- + Evaluator family reference used when creating or updating an evaluation + rule. + + + `name` and `scope` are enough to identify the evaluator family in the + authenticated project context. + properties: + name: + type: string + description: Evaluator family name. + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: Whether the evaluator family is project-owned or Langfuse-managed. + required: + - name + - scope + unstableEvaluationRuleEvaluator: + title: unstableEvaluationRuleEvaluator + type: object + description: |- + Resolved evaluator currently used by the evaluation rule. + + `id` is the exact active evaluator version. + `name` and `scope` identify the evaluator family conceptually. + properties: + id: + type: string + description: >- + Identifier of the exact evaluator version currently used by the + rule. + name: + type: string + description: Evaluator family name. + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: Whether the evaluator family is project-owned or Langfuse-managed. + required: + - id + - name + - scope + unstableEvaluator: + title: unstableEvaluator + type: object + description: >- + One evaluator that can be used for scoring. + + + An evaluator describes **how** to score data: + + - prompt + + - extracted prompt variables + + - output schema + + - optional explicit model configuration + + + It does not define **which** live objects are evaluated. That is the job + of `evaluation-rules`. + + + For agent clients, the most important fields are: + + - `variables`: use these exact names when building the evaluation-rule + `mapping` array + + - `outputDefinition`: tells you the expected score type and the + evaluator's response instructions + + - `modelConfig`: tells you whether the evaluator uses the project + default model (`null`) or an explicit provider/model + + + Versioning behavior: + + - `GET /evaluators` returns the latest version of each available + evaluator. + + - `GET /evaluators/{id}` can return an older version. + + - Evaluation rules always run against the latest version for the + selected evaluator name within the same source (`project` or `managed`). + properties: + id: + type: string + description: Identifier of this evaluator. + example: evaltmpl_123 + name: + type: string + description: Evaluator name. + example: answer-correctness + version: + type: integer + description: Version number of this evaluator. + example: 2 + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: >- + Where this evaluator comes from: your project or Langfuse-managed + defaults. + type: + $ref: '#/components/schemas/unstableEvaluatorType' + description: Evaluator engine type. Currently always `llm_as_judge`. + prompt: + type: string + description: Prompt template used during evaluation. + example: | + You are grading an answer. + + Input: + {{input}} + + Output: + {{output}} + + Return a score between 0 and 1. + variables: + type: array + items: + type: string + description: >- + Variables extracted from the evaluator prompt. + + + Every variable in this list must be mapped exactly once when + creating an evaluation rule. + example: + - input + - output + outputDefinition: + $ref: '#/components/schemas/unstablePublicEvaluatorOutputDefinition' + description: >- + Structured output schema returned by this evaluator. + + + Responses always include `dataType` and omit the internal + output-definition `version`. + + Use `dataType` to decide how future scores should be interpreted. + modelConfig: + $ref: '#/components/schemas/unstableEvaluatorModelConfig' + nullable: true + description: >- + Explicit model configuration, or `null` when the project default + evaluation model is used. + evaluationRuleCount: + type: integer + description: >- + Number of evaluation rules in the project that currently use this + evaluator version. + example: 0 + createdAt: + type: string + format: date-time + description: Timestamp when this evaluator was created. + example: '2026-03-30T09:00:00.000Z' + updatedAt: + type: string + format: date-time + description: Timestamp when this evaluator was last updated. + example: '2026-03-30T09:00:00.000Z' + required: + - id + - name + - version + - scope + - type + - prompt + - variables + - outputDefinition + - evaluationRuleCount + - createdAt + - updatedAt + unstableEvaluators: + title: unstableEvaluators + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/unstableEvaluator' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + unstableCreateEvaluatorRequest: + title: unstableCreateEvaluatorRequest + type: object + description: >- + Request body for creating an evaluator. + + + If the same `name` already exists in your project, Langfuse creates the + next version and returns it. + + Existing evaluation rules in the same project are then moved to that new + latest version automatically. + properties: + name: + type: string + description: Evaluator name within the authenticated project. + example: answer-correctness + prompt: + type: string + description: Prompt template used by the evaluator. + example: | + You are grading an answer. + + Input: + {{input}} + + Output: + {{output}} + + Return a score between 0 and 1. + outputDefinition: + $ref: '#/components/schemas/unstableEvaluatorOutputDefinition' + description: >- + Structured output schema the evaluator must return. + + + Always send `dataType`. + + Do not send `version`; it is an internal storage detail and not part + of the public request contract. + modelConfig: + $ref: '#/components/schemas/unstableEvaluatorModelConfig' + nullable: true + description: >- + Optional explicit model configuration. Omit or set to `null` to use + the project default evaluation model. + required: + - name + - prompt + - outputDefinition + utilsMetaResponse: + title: utilsMetaResponse + type: object + properties: + page: + type: integer + description: current page number + limit: + type: integer + description: number of items per page + totalItems: + type: integer + description: number of total items given the current filters/selection (if any) + totalPages: + type: integer + description: number of total pages given the current limit + required: + - page + - limit + - totalItems + - totalPages + securitySchemes: + BasicAuth: + type: http + scheme: basic diff --git a/pom.xml b/pom.xml index 38a311f..871f6d3 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,12 @@ 4.0.0 com.langfuse - langfuse-java + langfuse-java-parent 0.2.1-SNAPSHOT - jar + pom langfuse-java - Java client for the Langfuse API + Parent POM for the Langfuse Java SDK https://langfuse.com @@ -34,19 +34,197 @@ HEAD + + langfuse-java-api + langfuse-java-client + langfuse-java-testcontainers + langfuse-java-legacy + + UTF-8 - 2.18.6 + 17 + 2.21.3 + 3.1.3 4.11.0 - 5.9.3 + 6.1.0 + 2.0.18 + 7.22.0 + 2.0.5 + + + + + com.langfuse + langfuse-java-api + ${project.version} + + + com.langfuse + langfuse-java-testcontainers + ${project.version} + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + + tools.jackson.core + jackson-databind + ${jackson3.version} + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.27.7 + test + + + org.awaitility + awaitility + 4.3.0 + test + + + io.github.cdimascio + dotenv-java + 3.0.2 + test + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + package + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + false + none + + + + attach-javadocs + package + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + integration + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + integration + + **/*Test.java + + + + + + integration-test + verify + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator.version} + + + dev.jbang + jbang-maven-plugin + 0.0.8 + + + + + central - org.apache.maven.plugins maven-gpg-plugin @@ -59,7 +237,6 @@ sign - --pinentry-mode loopback @@ -68,8 +245,6 @@ - - org.sonatype.central central-publishing-maven-plugin @@ -84,133 +259,4 @@ - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.0 - - - attach-sources - package - - jar-no-fork - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.6.3 - - - attach-javadocs - package - - jar - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - integration - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.2.5 - - integration - - **/*Test.java - - - - - - integration-test - verify - - - - - - - - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - - - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - - - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - - - org.assertj - assertj-core - 3.27.7 - test - - - - - io.github.cdimascio - dotenv-java - 3.0.2 - test - - diff --git a/scripts/RelocateToSubpackages.java b/scripts/RelocateToSubpackages.java new file mode 100644 index 0000000..6a5f61b --- /dev/null +++ b/scripts/RelocateToSubpackages.java @@ -0,0 +1,78 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? + +// +// Relocates generated Java API files to match their declared package. +// +// The openapi-generator places all API files in a flat directory based on apiPackage, +// but our custom templates declare tag-based sub-packages (e.g., com.langfuse.api.health). +// This script reads each file's package declaration and moves it to the correct directory. +// +// Usage: RelocateToSubpackages [dir2] ... [-- ] +// Example: RelocateToSubpackages target/generated-sources/openapi-apis -- '*.java' +// + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; + +public class RelocateToSubpackages { + + private static final Pattern PACKAGE_PATTERN = Pattern.compile("^package\\s+([^;]+);"); + + public static void main(String[] args) throws IOException { + var argList = List.of(args); + var separatorIndex = argList.indexOf("--"); + var dirs = (separatorIndex >= 0) ? argList.subList(0, separatorIndex) : argList; + var pattern = (separatorIndex >= 0 && separatorIndex + 1 < argList.size()) + ? argList.get(separatorIndex + 1) + : "*.java"; + + var matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); + + dirs.stream() + .map(Path::of) + .filter(Files::isDirectory) + .flatMap(dir -> { + try { + return Files.walk(dir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .filter(Files::isRegularFile) + .filter(path -> matcher.matches(path.getFileName())) + .forEach(RelocateToSubpackages::relocate); + } + + private static void relocate(Path file) { + try { + Files.readAllLines(file).stream() + .flatMap(line -> PACKAGE_PATTERN.matcher(line).results()) + .map(match -> match.group(1)) + .findFirst() + .ifPresent(pkg -> moveToPackageDir(file, pkg)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static void moveToPackageDir(Path file, String pkg) { + var filePath = file.toAbsolutePath().toString().replace('\\', '/'); + var base = filePath.replaceFirst("(/src/main/java/).*", "$1"); + var targetDir = Path.of(base, pkg.replace(".", "/")); + var currentDir = file.getParent().toAbsolutePath().normalize(); + + if (!currentDir.equals(targetDir.toAbsolutePath().normalize())) { + try { + Files.createDirectories(targetDir); + Files.move(file, targetDir.resolve(file.getFileName())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +}