diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 876c786..8ed7656 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: - java-version: "23" + java-version: "25" distribution: "temurin" - name: Build with Maven 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..8567386 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,299 @@ -# 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. + +## Langfuse Version Compatibility + +This SDK targets **Langfuse v4** (`events_only` write mode). See [Migrating from Langfuse v3 to v4](#migrating-from-langfuse-v3-to-v4) for details on breaking changes and API replacements. + +## 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 + +``` + +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} ``` -## Usage +### Create a client + +```java +var langfuse = LangfuseApi.builder() + .username("pk-lf-...") // Langfuse public key + .password("sk-lf-...") // Langfuse secret key + .url("https://cloud.langfuse.com") + .build(); +``` + +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. + +### Ingest a trace (via OpenTelemetry) -Instantiate the Langfuse Client with the respective endpoint and your API Keys. +Langfuse v4 uses the OpenTelemetry endpoint for trace ingestion: ```java -import com.langfuse.client.LangfuseClient; +var traceId = UUID.randomUUID().toString().replace("-", ""); +var spanId = traceId.substring(0, 16); +var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); -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-...") +langfuse.opentelemetry().opentelemetryExportTraces( + OpentelemetryApi.APIOpentelemetryExportTracesRequest.newBuilder() + .opentelemetryExportTracesRequest(OpentelemetryExportTracesRequest.builder() + .resourceSpans(List.of(OtelResourceSpan.builder() + .resource(OtelResource.builder() + .attributes(List.of( + OtelAttribute.builder() + .key("langfuse.trace.name") + .value(OtelAttributeValue.builder() + .stringValue("my-trace") + .build()) + .build(), + OtelAttribute.builder() + .key("langfuse.trace.user.id") + .value(OtelAttributeValue.builder() + .stringValue("user-123") + .build()) + .build())) + .build()) + .scopeSpans(List.of(OtelScopeSpan.builder() + .scope(OtelScope.builder().name("my-service").build()) + .spans(List.of(OtelSpan.builder() + .traceId(traceId) + .spanId(spanId) + .name("root-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(nowNanos) + .build())) + .build())) + .build())) + .build()) + .build()); +``` + +### Query observations + +```java +var observations = langfuse.observations().observationsGetMany( + ObservationsApi.APIObservationsGetManyRequest.newBuilder() + .traceId(traceId) + .fields("core,basic,usage") + .build()); + +observations.getData().forEach(obs -> + System.out.println(obs.getId() + ": " + obs.getName())); +``` + +### Check health + +```java +var health = langfuse.health().healthHealth(); +System.out.println("Status: " + health.getStatus()); // OK +System.out.println("Version: " + health.getVersion()); // 4.x.x +``` + +### Async API + +Every synchronous API has an async counterpart that returns `CompletionStage`: + +```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(); ``` -An async client is also available via `AsyncLangfuseClient.builder()` with the same configuration options. +Sensitive headers (e.g. `Authorization`) are automatically masked in log output. -Make requests using the clients: +## Integration Testing with Testcontainers + +Add the testcontainers module to your test dependencies: + +```xml + + com.langfuse + langfuse-java-testcontainers + 0.2.1-SNAPSHOT + test + +``` + +Start a full Langfuse environment (PostgreSQL, ClickHouse, Redis, MinIO, Langfuse web + worker): ```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 langfuse = new LangfuseContainer(); +langfuse.start(); + +var client = LangfuseApi.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .build(); ``` -## Testing +For sharing a single container across test classes, use the +[Testcontainers singleton pattern](https://testcontainers.com/guides/testcontainers-container-lifecycle/#_using_singleton_containers): -### Unit tests +```java +abstract class AbstractIntegrationTest { + static LangfuseContainer langfuse = new LangfuseContainer(); -Unit tests (deserialization, query string mapping) run without any credentials: + static { + langfuse.start(); + } +} +``` + +See the [testcontainers module README](langfuse-java-testcontainers/) for configuration options. + +## Building ```bash -mvn test +./mvnw clean verify ``` -### Integration tests +## Migrating from Langfuse v3 to v4 + +This SDK was updated from Langfuse v3 to v4 (`events_only` write mode). If you are upgrading from a v3-based version, the following changes apply. + +### Infrastructure + +The Langfuse container images have been updated: + +| Component | v3 | v4 | +|---|---|---| +| Langfuse Web | `langfuse/langfuse:3` | `langfuse/langfuse:4` | +| Langfuse Worker | `langfuse/langfuse-worker:3` | `langfuse/langfuse-worker:4` | +| ClickHouse | `clickhouse/clickhouse-server` (untagged) | `clickhouse/clickhouse-server:25.12` (minimum) | + +Langfuse v4 requires ClickHouse 25.12+, PostgreSQL 15+, and Redis 7.0+. + +### Ingestion: legacy batch API replaced by OpenTelemetry + +The legacy batch ingestion endpoint (`POST /api/public/ingestion` with `TRACE_CREATE`, `SPAN_CREATE`, `GENERATION_CREATE`, etc.) returns **400** in `events_only` mode. All trace ingestion must use the OpenTelemetry endpoint: + +| v3 | v4 | +|---|---| +| `langfuse.ingestion().ingestionBatch(...)` | `langfuse.opentelemetry().opentelemetryExportTraces(...)` | + +Key differences in the OTel format: +- **Trace IDs** are 32-character hex strings (no dashes): `UUID.randomUUID().toString().replace("-", "")` +- **Span IDs** are 16-character hex strings: `traceId.substring(0, 16)` +- **Timestamps** are nanoseconds since epoch as strings: `String.valueOf(System.currentTimeMillis() * 1_000_000L)` +- **Trace metadata** (name, user ID, session ID) is set via OTel resource attributes prefixed with `langfuse.trace.`: + - `langfuse.trace.name` -- trace name + - `langfuse.trace.user.id` -- user ID + - `langfuse.trace.session.id` -- session ID + +### Read APIs: legacy endpoints replaced by v2/v3 -Integration tests connect to a real Langfuse project. They require credentials and are excluded from `mvn test`. +Several legacy read endpoints return **404** in `events_only` mode. Use the v4 replacements: -1. Copy `.env.example` to `.env` and fill in your API keys: - ```bash - cp .env.example .env - ``` +| Legacy Endpoint (404 in v4) | v4 Replacement | +|---|---| +| `trace().traceGet(...)` / `traceList(...)` | `observations().observationsGetMany(...)` (v2, with `traceId` filter) | +| `sessions().sessionsGet(...)` / `sessionsList(...)` | `observations().observationsGetMany(...)` (v2, with `traceId` filter) | +| `scores().scoresGetMany(...)` (v2) | `scoresV3().scoresV3GetManyV3(...)` | +| `legacyObservationsV1().legacyObservationsV1GetMany(...)` | `observations().observationsGetMany(...)` (v2) | +| `legacyMetricsV1().legacyMetricsV1Metrics(...)` | `metrics().metricsGetMany(...)` (v2) | -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 +The v2 observations endpoint uses cursor-based pagination (not page-based) and supports field selection via the `fields` parameter (`core`, `basic`, `time`, `io`, `metadata`, `model`, `usage`, `prompt`, `metrics`, `trace_context`). -3. Run all tests (unit + integration): - ```bash - mvn verify - ``` +### Score creation: renamed types - Or run only integration tests: - ```bash - mvn failsafe:integration-test - ``` +The score creation API was re-tagged from `LegacyScoreV1` to `Scores`, and the request/response types were renamed: -Integration tests skip gracefully when credentials are absent. +| v3 | v4 | +|---|---| +| `LegacyCreateScoreRequest` | `CreateScoreRequest` | +| `LegacyCreateScoreSource` | `CreateScoreSource` | +| `LegacyCreateScoreResponse` | `CreateScoreResponse` | +| `legacyScoreV1().legacyScoreV1Create(...)` | `scores().scoresCreate(...)` | -## Drafting a Release +### New APIs in v4 -Run `./mvnw release:prepare -DreleaseVersion=` with the version you want to create. -Push the changes including the tag. +The following API groups are new in v4: -## Publishing to Maven Central +| API | Accessor | Description | +|---|---|---| +| Experiments | `experiments()` | List experiments and experiment items | +| Feedback | `feedback()` | Submit feedback (cloud-hosted only) | +| Scores V3 | `scoresV3()` | Query scores with polymorphic `value` field and cursor pagination | +| Unstable Dashboards | `unstableDashboards()` | CRUD for custom dashboards | +| Unstable Dashboard Widgets | `unstableDashboardWidgets()` | CRUD for dashboard widgets | -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: +### OpenAPI spec: `const` keyword incompatibility -- `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 +The v4 OpenAPI spec uses the JSON Schema `const` keyword in the `unstableCodeEvaluationRuleEvaluatorReference` schema. This keyword is not supported by [openapi-generator](https://openapi-generator.tech/), so spec validation is disabled (`true`) in the Maven plugin configuration. The `const` keyword is silently ignored during code generation -- the generated field is typed as a plain `String` rather than a single-value enum. This does not affect runtime behavior since the server enforces the constraint. -## 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..1e78a48 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,180 @@ +# 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:4 + 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://minio:9000} + 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:4 + restart: always + depends_on: *langfuse-depends-on + ports: + - 3000:3000 + environment: + <<: *langfuse-worker-env + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret} # CHANGEME + LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090} + 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:25.12 + 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..906ade0 --- /dev/null +++ b/langfuse-java-api/pom.xml @@ -0,0 +1,259 @@ + + 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.1 + + + 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 + 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 + 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 + 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.2.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..b293886 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApi.java @@ -0,0 +1,420 @@ +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.experiments.ExperimentsApi; +import com.langfuse.api.feedback.FeedbackApi; +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.scoresV3.ScoresV3Api; +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.unstableDashboardWidgets.UnstableDashboardWidgetsApi; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi; +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 Experiments API */ + ExperimentsApi experiments(); + + /** @return the async Experiments API */ + com.langfuse.api.experiments.async.ExperimentsApi asyncExperiments(); + + /** @return the Feedback API */ + FeedbackApi feedback(); + + /** @return the async Feedback API */ + com.langfuse.api.feedback.async.FeedbackApi asyncFeedback(); + + /** @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 Scores V3 API */ + ScoresV3Api scoresV3(); + + /** @return the async Scores V3 API */ + com.langfuse.api.scoresV3.async.ScoresV3Api asyncScoresV3(); + + /** @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 Dashboard Widgets API */ + UnstableDashboardWidgetsApi unstableDashboardWidgets(); + + /** @return the async Unstable Dashboard Widgets API */ + com.langfuse.api.unstableDashboardWidgets.async.UnstableDashboardWidgetsApi asyncUnstableDashboardWidgets(); + + /** @return the Unstable Dashboards API */ + UnstableDashboardsApi unstableDashboards(); + + /** @return the async Unstable Dashboards API */ + com.langfuse.api.unstableDashboards.async.UnstableDashboardsApi asyncUnstableDashboards(); + + /** @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..7467c72 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseApis.java @@ -0,0 +1,71 @@ +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.experiments.ExperimentsApi; +import com.langfuse.api.feedback.FeedbackApi; +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.scoresV3.ScoresV3Api; +import com.langfuse.api.sessions.SessionsApi; +import com.langfuse.api.trace.TraceApi; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi; + +public interface LangfuseApis extends + AnnotationQueuesApi, + BlobStorageIntegrationsApi, + CommentsApi, + DatasetItemsApi, + DatasetRunItemsApi, + DatasetsApi, + ExperimentsApi, + FeedbackApi, + HealthApi, + IngestionApi, + LegacyMetricsV1Api, + LegacyObservationsV1Api, + LegacyScoreV1Api, + LlmConnectionsApi, + MediaApi, + MetricsApi, + ModelsApi, + ObservationsApi, + OpentelemetryApi, + OrganizationsApi, + ProjectsApi, + PromptVersionApi, + PromptsApi, + ScimApi, + ScoreConfigsApi, + ScoresApi, + ScoresV3Api, + SessionsApi, + TraceApi, + UnstableDashboardWidgetsApi, + UnstableDashboardsApi, + 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..fab2549 --- /dev/null +++ b/langfuse-java-api/src/main/java/com/langfuse/api/LangfuseAsyncApis.java @@ -0,0 +1,71 @@ +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.experiments.async.ExperimentsApi; +import com.langfuse.api.feedback.async.FeedbackApi; +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.scoresV3.async.ScoresV3Api; +import com.langfuse.api.sessions.async.SessionsApi; +import com.langfuse.api.trace.async.TraceApi; +import com.langfuse.api.unstableDashboardWidgets.async.UnstableDashboardWidgetsApi; +import com.langfuse.api.unstableDashboards.async.UnstableDashboardsApi; +import com.langfuse.api.unstableEvaluationRules.async.UnstableEvaluationRulesApi; +import com.langfuse.api.unstableEvaluators.async.UnstableEvaluatorsApi; + +public interface LangfuseAsyncApis extends + AnnotationQueuesApi, + BlobStorageIntegrationsApi, + CommentsApi, + DatasetItemsApi, + DatasetRunItemsApi, + DatasetsApi, + ExperimentsApi, + FeedbackApi, + HealthApi, + IngestionApi, + LegacyMetricsV1Api, + LegacyObservationsV1Api, + LegacyScoreV1Api, + LlmConnectionsApi, + MediaApi, + MetricsApi, + ModelsApi, + ObservationsApi, + OpentelemetryApi, + OrganizationsApi, + ProjectsApi, + PromptVersionApi, + PromptsApi, + ScimApi, + ScoreConfigsApi, + ScoresApi, + ScoresV3Api, + SessionsApi, + TraceApi, + UnstableDashboardWidgetsApi, + UnstableDashboardsApi, + 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..08ae09e --- /dev/null +++ b/langfuse-java-api/src/main/java/module-info.java @@ -0,0 +1,83 @@ +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.experiments; + exports com.langfuse.api.experiments.async; + exports com.langfuse.api.feedback; + exports com.langfuse.api.feedback.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.scoresV3; + exports com.langfuse.api.scoresV3.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.unstableDashboardWidgets; + exports com.langfuse.api.unstableDashboardWidgets.async; + exports com.langfuse.api.unstableDashboards; + exports com.langfuse.api.unstableDashboards.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..125cca7 --- /dev/null +++ b/langfuse-java-api/src/main/templates/anyof_model.mustache @@ -0,0 +1,411 @@ +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>xmlAnnotation}} +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = {{classname}}Deserializer.class) +@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = {{classname}}Serializer.class) +@tools.jackson.databind.annotation.JsonDeserialize(using = {{classname}}Jackson3Deserializer.class) +@tools.jackson.databind.annotation.JsonSerialize(using = {{classname}}Jackson3Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}} implements {{{.}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-implements}} { + + // 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_}} + +} + +// ---- Jackson 2 serializer (top-level to avoid ClassNotFoundException when Jackson 2 is absent) ---- + +class {{classname}}Serializer extends com.fasterxml.jackson.databind.ser.std.StdSerializer<{{classname}}> { + {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, com.fasterxml.jackson.core.JsonGenerator jgen, com.fasterxml.jackson.databind.SerializerProvider provider) throws IOException, com.fasterxml.jackson.core.JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } +} + +// ---- Jackson 2 deserializer (top-level to avoid ClassNotFoundException when Jackson 2 is absent) ---- + +class {{classname}}Deserializer extends com.fasterxml.jackson.databind.deser.std.StdDeserializer<{{classname}}> { + private static final Logger log = Logger.getLogger({{classname}}Deserializer.class.getName()); + + {{classname}}Deserializer() { + this({{classname}}.class); + } + + {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(com.fasterxml.jackson.core.JsonParser jp, com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException, com.fasterxml.jackson.core.JsonProcessingException { + com.fasterxml.jackson.databind.JsonNode tree = ctxt.readTree(jp); + + Object deserialized = null; + {{#discriminator}} + Class cls = null; // discriminator lookup not available in API module + if (cls != null) { + deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + {{/discriminator}} + {{#anyOf}} + // deserialize {{{.}}} + try { + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } catch (Exception e) { + log.log(Level.FINER, "Input data does not match '{{classname}}'", e); + } + + {{/anyOf}} + throw new IOException("Failed deserialization for {{classname}}: no match found"); + } + + @Override + public {{classname}} getNullValue(com.fasterxml.jackson.databind.DeserializationContext ctxt) throws com.fasterxml.jackson.databind.JsonMappingException { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw new com.fasterxml.jackson.databind.JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } +} + +// ---- Jackson 3 serializer (top-level to avoid ClassNotFoundException when Jackson 3 is absent) ---- + +class {{classname}}Jackson3Serializer extends tools.jackson.databind.ser.std.StdSerializer<{{classname}}> { + {{classname}}Jackson3Serializer() { + super({{classname}}.class); + } + + @Override + public void serialize({{classname}} value, tools.jackson.core.JsonGenerator jgen, tools.jackson.databind.SerializationContext serializationContext) throws tools.jackson.core.JacksonException { + serializationContext.writeValue(jgen, value.getActualInstance()); + } +} + +// ---- Jackson 3 deserializer (top-level to avoid ClassNotFoundException when Jackson 3 is absent) ---- + +class {{classname}}Jackson3Deserializer extends tools.jackson.databind.deser.std.StdDeserializer<{{classname}}> { + private static final Logger log = Logger.getLogger({{classname}}Jackson3Deserializer.class.getName()); + + {{classname}}Jackson3Deserializer() { + super({{classname}}.class); + } + + @Override + public {{classname}} deserialize(tools.jackson.core.JsonParser jp, tools.jackson.databind.DeserializationContext ctxt) throws tools.jackson.core.JacksonException { + tools.jackson.databind.JsonNode tree = ctxt.readTree(jp); + + Object deserialized = null; + {{#discriminator}} + Class cls = null; // discriminator lookup not available in API module + if (cls != null) { + deserialized = ctxt.readTreeAsValue(tree, ctxt.constructType(cls)); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + {{/discriminator}} + {{#anyOf}} + // deserialize {{{.}}} + try { + deserialized = ctxt.readTreeAsValue(tree, {{{.}}}.class); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } catch (Exception e) { + log.log(Level.FINER, "Input data does not match '{{classname}}'", e); + } + + {{/anyOf}} + throw tools.jackson.databind.DatabindException.from(jp, "Failed deserialization for {{classname}}: no match found"); + } + + @Override + public {{classname}} getNullValue(tools.jackson.databind.DeserializationContext ctxt) { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw tools.jackson.databind.DatabindException.from(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } +} 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/beanValidation.mustache b/langfuse-java-api/src/main/templates/beanValidation.mustache new file mode 100644 index 0000000..f1b3a00 --- /dev/null +++ b/langfuse-java-api/src/main/templates/beanValidation.mustache @@ -0,0 +1,11 @@ +{{#required}} +{{^isReadOnly}} + @NotNull +{{/isReadOnly}} +{{/required}} +{{^isContainer}} +{{^isPrimitiveType}} + @Valid +{{/isPrimitiveType}} +{{/isContainer}} +{{>beanValidationCore}} \ No newline at end of file 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..b4ab765 --- /dev/null +++ b/langfuse-java-api/src/main/templates/oneof_model.mustache @@ -0,0 +1,470 @@ +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>xmlAnnotation}} +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = {{classname}}Deserializer.class) +@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = {{classname}}Serializer.class) +@tools.jackson.databind.annotation.JsonDeserialize(using = {{classname}}Jackson3Deserializer.class) +@tools.jackson.databind.annotation.JsonSerialize(using = {{classname}}Jackson3Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}} implements {{{.}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-implements}} { + + // 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_}} + +} + +// ---- Jackson 2 serializer (top-level to avoid ClassNotFoundException when Jackson 2 is absent) ---- + +class {{classname}}Serializer extends com.fasterxml.jackson.databind.ser.std.StdSerializer<{{classname}}> { + {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, com.fasterxml.jackson.core.JsonGenerator jgen, com.fasterxml.jackson.databind.SerializerProvider provider) throws IOException, com.fasterxml.jackson.core.JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } +} + +// ---- Jackson 2 deserializer (top-level to avoid ClassNotFoundException when Jackson 2 is absent) ---- + +class {{classname}}Deserializer extends com.fasterxml.jackson.databind.deser.std.StdDeserializer<{{classname}}> { + private static final Logger log = Logger.getLogger({{classname}}Deserializer.class.getName()); + + {{classname}}Deserializer() { + this({{classname}}.class); + } + + {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(com.fasterxml.jackson.core.JsonParser jp, com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException, com.fasterxml.jackson.core.JsonProcessingException { + com.fasterxml.jackson.databind.JsonNode tree = ctxt.readTree(jp); + Object deserialized = null; + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + {{classname}} new{{classname}} = new {{classname}}(); + Map result2 = tree.traverse(jp.getCodec()).readValueAs(new com.fasterxml.jackson.core.type.TypeReference>() {}); + String discriminatorValue = (String)result2.get("{{{propertyBaseName}}}"); + switch (discriminatorValue) { + {{#mappedModels}} + case "{{{mappingName}}}": + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{modelName}}}.class); + 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}} + boolean typeCoercion = ctxt.isEnabled(com.fasterxml.jackson.databind.MapperFeature.ALLOW_COERCION_OF_SCALARS); + int match = 0; + com.fasterxml.jackson.core.JsonToken token = tree.traverse(jp.getCodec()).nextToken(); + {{#oneOf}} + // deserialize {{{.}}} + try { + boolean attemptParsing = true; + 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 == com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT); + attemptParsing |= (({{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class)) && token == com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_FLOAT); + attemptParsing |= ({{{.}}}.class.equals(Boolean.class) && (token == com.fasterxml.jackson.core.JsonToken.VALUE_FALSE || token == com.fasterxml.jackson.core.JsonToken.VALUE_TRUE)); + attemptParsing |= ({{{.}}}.class.equals(String.class) && token == com.fasterxml.jackson.core.JsonToken.VALUE_STRING); + {{#isNullable}} + attemptParsing |= (token == com.fasterxml.jackson.core.JsonToken.VALUE_NULL); + {{/isNullable}} + } + } + if (attemptParsing) { + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); + match++; + log.log(Level.FINER, "Input data matches schema '{{{.}}}'"); + } + } catch (Exception e) { + log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e); + } + + {{/oneOf}} + if (match >= 1) { + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + throw new IOException(String.format(java.util.Locale.ROOT, "Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); + } + + @Override + public {{classname}} getNullValue(com.fasterxml.jackson.databind.DeserializationContext ctxt) throws com.fasterxml.jackson.databind.JsonMappingException { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw new com.fasterxml.jackson.databind.JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } +} + +// ---- Jackson 3 serializer (top-level to avoid ClassNotFoundException when Jackson 3 is absent) ---- + +class {{classname}}Jackson3Serializer extends tools.jackson.databind.ser.std.StdSerializer<{{classname}}> { + {{classname}}Jackson3Serializer() { + super({{classname}}.class); + } + + @Override + public void serialize({{classname}} value, tools.jackson.core.JsonGenerator jgen, tools.jackson.databind.SerializationContext serializationContext) throws tools.jackson.core.JacksonException { + serializationContext.writeValue(jgen, value.getActualInstance()); + } +} + +// ---- Jackson 3 deserializer (top-level to avoid ClassNotFoundException when Jackson 3 is absent) ---- + +class {{classname}}Jackson3Deserializer extends tools.jackson.databind.deser.std.StdDeserializer<{{classname}}> { + private static final Logger log = Logger.getLogger({{classname}}Jackson3Deserializer.class.getName()); + + {{classname}}Jackson3Deserializer() { + super({{classname}}.class); + } + + @Override + public {{classname}} deserialize(tools.jackson.core.JsonParser jp, tools.jackson.databind.DeserializationContext ctxt) throws tools.jackson.core.JacksonException { + tools.jackson.databind.JsonNode tree = ctxt.readTree(jp); + Object deserialized = null; + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + {{classname}} new{{classname}} = new {{classname}}(); + String discriminatorValue = tree.path("{{{propertyBaseName}}}").asText(null); + switch (discriminatorValue) { + {{#mappedModels}} + case "{{{mappingName}}}": + deserialized = ctxt.readTreeAsValue(tree, {{{modelName}}}.class); + 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}} + boolean typeCoercion = false; + int match = 0; + tools.jackson.core.JsonToken token = tree.asToken(); + {{#oneOf}} + // deserialize {{{.}}} + try { + boolean attemptParsing = true; + 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 == tools.jackson.core.JsonToken.VALUE_NUMBER_INT); + attemptParsing |= (({{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class)) && token == tools.jackson.core.JsonToken.VALUE_NUMBER_FLOAT); + attemptParsing |= ({{{.}}}.class.equals(Boolean.class) && (token == tools.jackson.core.JsonToken.VALUE_FALSE || token == tools.jackson.core.JsonToken.VALUE_TRUE)); + attemptParsing |= ({{{.}}}.class.equals(String.class) && token == tools.jackson.core.JsonToken.VALUE_STRING); + {{#isNullable}} + attemptParsing |= (token == tools.jackson.core.JsonToken.VALUE_NULL); + {{/isNullable}} + } + } + if (attemptParsing) { + deserialized = ctxt.readTreeAsValue(tree, {{{.}}}.class); + match++; + log.log(Level.FINER, "Input data matches schema '{{{.}}}'"); + } + } catch (Exception e) { + log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e); + } + + {{/oneOf}} + if (match >= 1) { + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + throw tools.jackson.databind.DatabindException.from(jp, String.format(java.util.Locale.ROOT, "Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); + } + + @Override + public {{classname}} getNullValue(tools.jackson.databind.DeserializationContext ctxt) { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw tools.jackson.databind.DatabindException.from(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } +} 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..20142bb --- /dev/null +++ b/langfuse-java-client/pom.xml @@ -0,0 +1,248 @@ + + 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.1 + + + 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 + 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 + 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.2.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..bc50cea --- /dev/null +++ b/langfuse-java-client/src/main/java/com/langfuse/client/LangfuseClient.java @@ -0,0 +1,616 @@ +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.experiments.ExperimentsApi; +import com.langfuse.api.feedback.FeedbackApi; +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.scoresV3.ScoresV3Api; +import com.langfuse.api.sessions.SessionsApi; +import com.langfuse.api.trace.TraceApi; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi; +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 ExperimentsApi experimentsApi; + private final com.langfuse.api.experiments.async.ExperimentsApi asyncExperimentsApi; + private final FeedbackApi feedbackApi; + private final com.langfuse.api.feedback.async.FeedbackApi asyncFeedbackApi; + 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 ScoresV3Api scoresV3Api; + private final com.langfuse.api.scoresV3.async.ScoresV3Api asyncScoresV3Api; + 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 UnstableDashboardWidgetsApi unstableDashboardWidgetsApi; + private final com.langfuse.api.unstableDashboardWidgets.async.UnstableDashboardWidgetsApi asyncUnstableDashboardWidgetsApi; + private final UnstableDashboardsApi unstableDashboardsApi; + private final com.langfuse.api.unstableDashboards.async.UnstableDashboardsApi asyncUnstableDashboardsApi; + 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.experimentsApi = new com.langfuse.client.experiments.ExperimentsApi(this); + this.asyncExperimentsApi = new com.langfuse.client.experiments.async.ExperimentsApi(this); + this.feedbackApi = new com.langfuse.client.feedback.FeedbackApi(this); + this.asyncFeedbackApi = new com.langfuse.client.feedback.async.FeedbackApi(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.scoresV3Api = new com.langfuse.client.scoresV3.ScoresV3Api(this); + this.asyncScoresV3Api = new com.langfuse.client.scoresV3.async.ScoresV3Api(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.unstableDashboardWidgetsApi = new com.langfuse.client.unstableDashboardWidgets.UnstableDashboardWidgetsApi(this); + this.asyncUnstableDashboardWidgetsApi = new com.langfuse.client.unstableDashboardWidgets.async.UnstableDashboardWidgetsApi(this); + this.unstableDashboardsApi = new com.langfuse.client.unstableDashboards.UnstableDashboardsApi(this); + this.asyncUnstableDashboardsApi = new com.langfuse.client.unstableDashboards.async.UnstableDashboardsApi(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 ExperimentsApi experiments() { + return experimentsApi; + } + + @Override + public com.langfuse.api.experiments.async.ExperimentsApi asyncExperiments() { + return asyncExperimentsApi; + } + + @Override + public FeedbackApi feedback() { + return feedbackApi; + } + + @Override + public com.langfuse.api.feedback.async.FeedbackApi asyncFeedback() { + return asyncFeedbackApi; + } + + @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 ScoresV3Api scoresV3() { + return scoresV3Api; + } + + @Override + public com.langfuse.api.scoresV3.async.ScoresV3Api asyncScoresV3() { + return asyncScoresV3Api; + } + + @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 UnstableDashboardWidgetsApi unstableDashboardWidgets() { + return unstableDashboardWidgetsApi; + } + + @Override + public com.langfuse.api.unstableDashboardWidgets.async.UnstableDashboardWidgetsApi asyncUnstableDashboardWidgets() { + return asyncUnstableDashboardWidgetsApi; + } + + @Override + public UnstableDashboardsApi unstableDashboards() { + return unstableDashboardsApi; + } + + @Override + public com.langfuse.api.unstableDashboards.async.UnstableDashboardsApi asyncUnstableDashboards() { + return asyncUnstableDashboardsApi; + } + + @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..287d362 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AbstractLangfuseClientTest.java @@ -0,0 +1,211 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +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.api.model.OpentelemetryExportTracesRequest; +import com.langfuse.api.model.OtelAttribute; +import com.langfuse.api.model.OtelAttributeValue; +import com.langfuse.api.model.OtelResource; +import com.langfuse.api.model.OtelResourceSpan; +import com.langfuse.api.model.OtelScope; +import com.langfuse.api.model.OtelScopeSpan; +import com.langfuse.api.model.OtelSpan; +import com.langfuse.api.opentelemetry.OpentelemetryApi.APIOpentelemetryExportTracesRequest; +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. + * + *

Subclasses must implement {@link #createClient()} to select the Jackson version. + * Two shared client instances (Jackson 2 and Jackson 3) are available via + * {@link #jackson2Client()} and {@link #jackson3Client()}. + * + * @author Eric Deandrea + * @see Singleton Containers + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractLangfuseClientTest { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractLangfuseClientTest.class); + + static LangfuseContainer langfuse = new LangfuseContainer(); + private static LangfuseApi jackson2Client; + private static LangfuseApi jackson3Client; + + static { + langfuse.start(); + jackson2Client = createJackson2Client(); + jackson3Client = createJackson3Client(); + } + + LangfuseApi client; + + @BeforeAll + void initClient() { + client = createClient(); + } + + /** + * Returns the {@link LangfuseApi} client to use for this test class. + * Concrete subclasses select the Jackson version by returning either + * {@link #jackson2Client()} or {@link #jackson3Client()}. + * + * @return the client instance + */ + abstract LangfuseApi 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)); + } + }; + + /** + * Ingests a trace with a single root span via the OTel endpoint. + * + * @param traceId 32-char hex trace ID + * @param spanId 16-char hex span ID + * @param traceName the trace name (set as {@code langfuse.trace.name} resource attribute) + */ + void ingestTrace(String traceId, String spanId, String traceName) { + ingestTrace(traceId, spanId, traceName, Map.of()); + } + + /** + * Ingests a trace with a single root span and additional resource attributes via the OTel endpoint. + * + * @param traceId 32-char hex trace ID + * @param spanId 16-char hex span ID + * @param traceName the trace name (set as {@code langfuse.trace.name} resource attribute) + * @param traceAttributes additional {@code langfuse.trace.*} attributes (e.g. {@code user.id}, {@code session.id}) + */ + void ingestTrace(String traceId, String spanId, String traceName, Map traceAttributes) { + var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); + + var span = OtelSpan.builder() + .traceId(traceId) + .spanId(spanId) + .name("root-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(String.valueOf(Long.parseLong(nowNanos) + 1_000_000_000L)) + .build(); + + ingestTraceWithSpans(traceName, traceAttributes, span); + } + + /** + * Ingests a trace with the given spans via the OTel endpoint. + * + * @param traceName the trace name (set as {@code langfuse.trace.name} resource attribute) + * @param traceAttributes additional {@code langfuse.trace.*} attributes + * @param spans one or more spans to include + */ + void ingestTraceWithSpans(String traceName, Map traceAttributes, OtelSpan... spans) { + var attributes = new ArrayList(); + + attributes.add(OtelAttribute.builder() + .key("langfuse.trace.name") + .value(OtelAttributeValue.builder() + .stringValue(traceName) + .build()) + .build()); + + traceAttributes.forEach((key, value) -> + attributes.add(OtelAttribute.builder() + .key("langfuse.trace." + key) + .value(OtelAttributeValue.builder() + .stringValue(value) + .build()) + .build())); + + var resourceSpan = OtelResourceSpan.builder() + .resource(OtelResource.builder() + .attributes(attributes) + .build()) + .scopeSpans(List.of(OtelScopeSpan.builder() + .scope(OtelScope.builder() + .name("test") + .build()) + .spans(List.of(spans)) + .build())) + .build(); + + var response = client.opentelemetry().opentelemetryExportTraces( + APIOpentelemetryExportTracesRequest.newBuilder() + .opentelemetryExportTracesRequest(OpentelemetryExportTracesRequest.builder() + .resourceSpans(List.of(resourceSpan)) + .build()) + .build()); + + assertThat(response) + .isNotNull(); + } + + /** + * Returns the shared Jackson 2 client instance. + * + * @return the Jackson 2 client + */ + static LangfuseApi jackson2Client() { + return jackson2Client; + } + + /** + * Returns the shared Jackson 3 client instance. + * + * @return the Jackson 3 client + */ + static LangfuseApi jackson3Client() { + return jackson3Client; + } + + private static LangfuseApi createJackson2Client() { + return LangfuseJackson2Client.builder() + .username(langfuse.getPublicKey()) + .password(langfuse.getSecretKey()) + .url(langfuse.getLangfuseUrl()) + .logRequests() + .logResponses() + .prettyPrint() + .build(); + } + + private static LangfuseApi createJackson3Client() { + return LangfuseJackson3Client.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/AnnotationQueuesApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncJackson2Test.java new file mode 100644 index 0000000..944bd1c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class AnnotationQueuesApiAsyncJackson2Test extends AnnotationQueuesApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncJackson3Test.java new file mode 100644 index 0000000..3ce52da --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class AnnotationQueuesApiAsyncJackson3Test extends AnnotationQueuesApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncTest.java new file mode 100644 index 0000000..1d441ed --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiAsyncTest.java @@ -0,0 +1,225 @@ +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.Disabled; +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.APIAnnotationQueuesCreateQueueAssignmentRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesCreateQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesCreateQueueRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesDeleteQueueAssignmentRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesDeleteQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesGetQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesGetQueueRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesListQueueItemsRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesListQueuesRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesUpdateQueueItemRequest; +import com.langfuse.api.model.AnnotationQueue; +import com.langfuse.api.model.AnnotationQueueAssignmentRequest; +import com.langfuse.api.model.AnnotationQueueObjectType; +import com.langfuse.api.model.AnnotationQueueStatus; +import com.langfuse.api.model.CreateAnnotationQueueItemRequest; +import com.langfuse.api.model.CreateAnnotationQueueRequest; +import com.langfuse.api.model.CreateScoreConfigRequest; +import com.langfuse.api.model.ScoreConfigDataType; +import com.langfuse.api.model.UpdateAnnotationQueueItemRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsCreateRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class AnnotationQueuesApiAsyncTest extends AbstractLangfuseClientTest { + + private final String QUEUE_NAME = "async-test-queue-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String queueId; + private static String queueItemId; + + @Test + @Order(1) + void createQueue() { + var scoreConfig = client.scoreConfigs().scoreConfigsCreate( + APIScoreConfigsCreateRequest.newBuilder() + .createScoreConfigRequest(CreateScoreConfigRequest.builder() + .name("async-queue-cfg-" + UUID.randomUUID().toString().substring(0, 6)) + .dataType(ScoreConfigDataType.NUMERIC) + .minValue(0.0) + .maxValue(1.0) + .build()) + .build()); + + assertThat(client.asyncAnnotationQueues().annotationQueuesCreateQueue( + APIAnnotationQueuesCreateQueueRequest.newBuilder() + .createAnnotationQueueRequest(CreateAnnotationQueueRequest.builder() + .name(QUEUE_NAME) + .description("Async test annotation queue") + .scoreConfigIds(List.of(scoreConfig.getId())) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(queue -> { + assertThat(queue.getId()).isNotBlank(); + assertThat(queue.getName()).isEqualTo(QUEUE_NAME); + assertThat(queue.getDescription()).isEqualTo("Async test annotation queue"); + assertThat(queue.getCreatedAt()).isNotNull(); + queueId = queue.getId(); + }); + } + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "async-annotation-queue-test-trace"); + } + + @Test + @Order(2) + void getQueue() { + assertThat(client.asyncAnnotationQueues().annotationQueuesGetQueue( + APIAnnotationQueuesGetQueueRequest.newBuilder() + .queueId(queueId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(AnnotationQueue::getId, AnnotationQueue::getName) + .containsExactly(queueId, QUEUE_NAME); + } + + @Test + @Order(2) + void listQueues() { + assertThat(client.asyncAnnotationQueues().annotationQueuesListQueues( + APIAnnotationQueuesListQueuesRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(queues -> { + assertThat(queues.getData()) + .isNotEmpty() + .anyMatch(q -> QUEUE_NAME.equals(q.getName())); + assertThat(queues.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(3) + void createQueueItem() { + assertThat(client.asyncAnnotationQueues().annotationQueuesCreateQueueItem( + APIAnnotationQueuesCreateQueueItemRequest.newBuilder() + .queueId(queueId) + .createAnnotationQueueItemRequest(CreateAnnotationQueueItemRequest.builder() + .objectId(TRACE_ID) + .objectType(AnnotationQueueObjectType.TRACE) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(item -> { + assertThat(item.getId()).isNotBlank(); + assertThat(item.getQueueId()).isEqualTo(queueId); + assertThat(item.getObjectId()).isEqualTo(TRACE_ID); + assertThat(item.getObjectType()).isEqualTo(AnnotationQueueObjectType.TRACE); + assertThat(item.getStatus()).isNotNull(); + assertThat(item.getCreatedAt()).isNotNull(); + queueItemId = item.getId(); + }); + } + + @Test + @Order(4) + void getQueueItem() { + assertThat(client.asyncAnnotationQueues().annotationQueuesGetQueueItem( + APIAnnotationQueuesGetQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(item -> { + assertThat(item.getId()).isEqualTo(queueItemId); + assertThat(item.getQueueId()).isEqualTo(queueId); + assertThat(item.getObjectId()).isEqualTo(TRACE_ID); + }); + } + + @Test + @Order(4) + void listQueueItems() { + assertThat(client.asyncAnnotationQueues().annotationQueuesListQueueItems( + APIAnnotationQueuesListQueueItemsRequest.newBuilder() + .queueId(queueId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(items -> { + assertThat(items.getData()) + .isNotEmpty() + .anyMatch(i -> queueItemId.equals(i.getId())); + assertThat(items.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(5) + void updateQueueItem() { + assertThat(client.asyncAnnotationQueues().annotationQueuesUpdateQueueItem( + APIAnnotationQueuesUpdateQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .updateAnnotationQueueItemRequest(UpdateAnnotationQueueItemRequest.builder() + .status(AnnotationQueueStatus.COMPLETED) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(item -> { + assertThat(item.getId()).isEqualTo(queueItemId); + assertThat(item.getStatus()).isEqualTo(AnnotationQueueStatus.COMPLETED); + }); + } + + @Disabled("Requires a valid user ID not available via API key auth") + @Test + @Order(6) + void createAndDeleteQueueAssignment() { + var userId = client.projects().projectsGet().getData().get(0).getOrganization().getId(); + + assertThat(client.asyncAnnotationQueues().annotationQueuesCreateQueueAssignment( + APIAnnotationQueuesCreateQueueAssignmentRequest.newBuilder() + .queueId(queueId) + .annotationQueueAssignmentRequest(AnnotationQueueAssignmentRequest.builder() + .userId(userId) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getQueueId()).isEqualTo(queueId); + assertThat(response.getUserId()).isEqualTo(userId); + }); + + assertThat(client.asyncAnnotationQueues().annotationQueuesDeleteQueueAssignment( + APIAnnotationQueuesDeleteQueueAssignmentRequest.newBuilder() + .queueId(queueId) + .annotationQueueAssignmentRequest(AnnotationQueueAssignmentRequest.builder() + .userId(userId) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getSuccess()).isTrue()); + } + + @Test + @Order(7) + void deleteQueueItem() { + assertThat(client.asyncAnnotationQueues().annotationQueuesDeleteQueueItem( + APIAnnotationQueuesDeleteQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson2Test.java new file mode 100644 index 0000000..f3f6db8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class AnnotationQueuesApiJackson2Test extends AnnotationQueuesApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson3Test.java new file mode 100644 index 0000000..5e7c207 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class AnnotationQueuesApiJackson3Test extends AnnotationQueuesApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..cf3065a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/AnnotationQueuesApiTest.java @@ -0,0 +1,219 @@ +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.Disabled; +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.APIAnnotationQueuesCreateQueueAssignmentRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesCreateQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesCreateQueueRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesDeleteQueueAssignmentRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesDeleteQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesGetQueueItemRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesGetQueueRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesListQueueItemsRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesListQueuesRequest; +import com.langfuse.api.annotationQueues.AnnotationQueuesApi.APIAnnotationQueuesUpdateQueueItemRequest; +import com.langfuse.api.model.AnnotationQueue; +import com.langfuse.api.model.AnnotationQueueAssignmentRequest; +import com.langfuse.api.model.AnnotationQueueObjectType; +import com.langfuse.api.model.AnnotationQueueStatus; +import com.langfuse.api.model.CreateAnnotationQueueItemRequest; +import com.langfuse.api.model.CreateAnnotationQueueRequest; +import com.langfuse.api.model.CreateScoreConfigRequest; +import com.langfuse.api.model.ScoreConfigDataType; +import com.langfuse.api.model.UpdateAnnotationQueueItemRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsCreateRequest; + +/** + * Integration tests for the Annotation Queues API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class AnnotationQueuesApiTest extends AbstractLangfuseClientTest { + + private final String QUEUE_NAME = "test-queue-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String queueId; + private static String queueItemId; + + @Test + @Order(1) + void createQueue() { + var scoreConfig = client.scoreConfigs().scoreConfigsCreate( + 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( + 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(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "annotation-queue-test-trace"); + } + + @Test + @Order(2) + void getQueue() { + assertThat(client.annotationQueues().annotationQueuesGetQueue( + APIAnnotationQueuesGetQueueRequest.newBuilder() + .queueId(queueId) + .build())) + .extracting(AnnotationQueue::getId, AnnotationQueue::getName) + .containsExactly(queueId, QUEUE_NAME); + } + + @Test + @Order(2) + void listQueues() { + assertThat(client.annotationQueues().annotationQueuesListQueues( + APIAnnotationQueuesListQueuesRequest.newBuilder() + .build())) + .satisfies(queues -> { + assertThat(queues.getData()) + .isNotEmpty() + .anyMatch(q -> QUEUE_NAME.equals(q.getName())); + assertThat(queues.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(3) + void createQueueItem() { + assertThat(client.annotationQueues().annotationQueuesCreateQueueItem( + APIAnnotationQueuesCreateQueueItemRequest.newBuilder() + .queueId(queueId) + .createAnnotationQueueItemRequest(CreateAnnotationQueueItemRequest.builder() + .objectId(TRACE_ID) + .objectType(AnnotationQueueObjectType.TRACE) + .build()) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isNotBlank(); + assertThat(item.getQueueId()).isEqualTo(queueId); + assertThat(item.getObjectId()).isEqualTo(TRACE_ID); + assertThat(item.getObjectType()).isEqualTo(AnnotationQueueObjectType.TRACE); + assertThat(item.getStatus()).isNotNull(); + assertThat(item.getCreatedAt()).isNotNull(); + queueItemId = item.getId(); + }); + } + + @Test + @Order(4) + void getQueueItem() { + assertThat(client.annotationQueues().annotationQueuesGetQueueItem( + APIAnnotationQueuesGetQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isEqualTo(queueItemId); + assertThat(item.getQueueId()).isEqualTo(queueId); + assertThat(item.getObjectId()).isEqualTo(TRACE_ID); + }); + } + + @Test + @Order(4) + void listQueueItems() { + assertThat(client.annotationQueues().annotationQueuesListQueueItems( + APIAnnotationQueuesListQueueItemsRequest.newBuilder() + .queueId(queueId) + .build())) + .satisfies(items -> { + assertThat(items.getData()) + .isNotEmpty() + .anyMatch(i -> queueItemId.equals(i.getId())); + assertThat(items.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(5) + void updateQueueItem() { + assertThat(client.annotationQueues().annotationQueuesUpdateQueueItem( + APIAnnotationQueuesUpdateQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .updateAnnotationQueueItemRequest(UpdateAnnotationQueueItemRequest.builder() + .status(AnnotationQueueStatus.COMPLETED) + .build()) + .build())) + .satisfies(item -> { + assertThat(item.getId()).isEqualTo(queueItemId); + assertThat(item.getStatus()).isEqualTo(AnnotationQueueStatus.COMPLETED); + }); + } + + @Disabled("Requires a valid user ID not available via API key auth") + @Test + @Order(6) + void createAndDeleteQueueAssignment() { + var userId = client.projects().projectsGet().getData().get(0).getOrganization().getId(); + + assertThat(client.annotationQueues().annotationQueuesCreateQueueAssignment( + APIAnnotationQueuesCreateQueueAssignmentRequest.newBuilder() + .queueId(queueId) + .annotationQueueAssignmentRequest(AnnotationQueueAssignmentRequest.builder() + .userId(userId) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getQueueId()).isEqualTo(queueId); + assertThat(response.getUserId()).isEqualTo(userId); + }); + + assertThat(client.annotationQueues().annotationQueuesDeleteQueueAssignment( + APIAnnotationQueuesDeleteQueueAssignmentRequest.newBuilder() + .queueId(queueId) + .annotationQueueAssignmentRequest(AnnotationQueueAssignmentRequest.builder() + .userId(userId) + .build()) + .build())) + .satisfies(response -> + assertThat(response.getSuccess()).isTrue()); + } + + @Test + @Order(7) + void deleteQueueItem() { + assertThat(client.annotationQueues().annotationQueuesDeleteQueueItem( + APIAnnotationQueuesDeleteQueueItemRequest.newBuilder() + .queueId(queueId) + .itemId(queueItemId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson2Test.java new file mode 100644 index 0000000..a713d40 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class BlobStorageIntegrationsApiAsyncJackson2Test extends BlobStorageIntegrationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson3Test.java new file mode 100644 index 0000000..b2ea91e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class BlobStorageIntegrationsApiAsyncJackson3Test extends BlobStorageIntegrationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncTest.java new file mode 100644 index 0000000..2f6ef6e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiAsyncTest.java @@ -0,0 +1,78 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Disabled; +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.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsDeleteBlobStorageIntegrationRequest; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsGetBlobStorageIntegrationStatusRequest; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsUpsertBlobStorageIntegrationRequest; +import com.langfuse.api.model.CreateBlobStorageIntegrationRequest; + +/** + * Async integration tests for the Blob Storage Integrations API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class BlobStorageIntegrationsApiAsyncTest extends AbstractLangfuseClientTest { + + private static String integrationId; + + @Test + @Order(1) + void listBlobStorageIntegrations() { + assertThat(client.asyncBlobStorageIntegrations().blobStorageIntegrationsGetBlobStorageIntegrations()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + void upsertBlobStorageIntegration() { + assertThat(client.asyncBlobStorageIntegrations().blobStorageIntegrationsUpsertBlobStorageIntegration( + APIBlobStorageIntegrationsUpsertBlobStorageIntegrationRequest.newBuilder() + .createBlobStorageIntegrationRequest(CreateBlobStorageIntegrationRequest.builder() + .bucketName("test-bucket") + .region("us-east-1") + .accessKeyId("test-key") + .secretAccessKey("test-secret") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(3) + void getIntegrationStatus() { + assertThat(client.asyncBlobStorageIntegrations().blobStorageIntegrationsGetBlobStorageIntegrationStatus( + APIBlobStorageIntegrationsGetBlobStorageIntegrationStatusRequest.newBuilder() + .id(integrationId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(4) + void deleteIntegration() { + assertThat(client.asyncBlobStorageIntegrations().blobStorageIntegrationsDeleteBlobStorageIntegration( + APIBlobStorageIntegrationsDeleteBlobStorageIntegrationRequest.newBuilder() + .id(integrationId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson2Test.java new file mode 100644 index 0000000..e17e415 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class BlobStorageIntegrationsApiJackson2Test extends BlobStorageIntegrationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson3Test.java new file mode 100644 index 0000000..bb7f370 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class BlobStorageIntegrationsApiJackson3Test extends BlobStorageIntegrationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiTest.java new file mode 100644 index 0000000..5e0cc6b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/BlobStorageIntegrationsApiTest.java @@ -0,0 +1,72 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Disabled; +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.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsDeleteBlobStorageIntegrationRequest; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsGetBlobStorageIntegrationStatusRequest; +import com.langfuse.api.blobStorageIntegrations.BlobStorageIntegrationsApi.APIBlobStorageIntegrationsUpsertBlobStorageIntegrationRequest; +import com.langfuse.api.model.CreateBlobStorageIntegrationRequest; + +/** + * Integration tests for the Blob Storage Integrations API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class BlobStorageIntegrationsApiTest extends AbstractLangfuseClientTest { + + private static String integrationId; + + @Test + @Order(1) + void listBlobStorageIntegrations() { + assertThat(client.blobStorageIntegrations().blobStorageIntegrationsGetBlobStorageIntegrations()) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + void upsertBlobStorageIntegration() { + assertThat(client.blobStorageIntegrations().blobStorageIntegrationsUpsertBlobStorageIntegration( + APIBlobStorageIntegrationsUpsertBlobStorageIntegrationRequest.newBuilder() + .createBlobStorageIntegrationRequest(CreateBlobStorageIntegrationRequest.builder() + .bucketName("test-bucket") + .region("us-east-1") + .accessKeyId("test-key") + .secretAccessKey("test-secret") + .build()) + .build())) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(3) + void getIntegrationStatus() { + assertThat(client.blobStorageIntegrations().blobStorageIntegrationsGetBlobStorageIntegrationStatus( + APIBlobStorageIntegrationsGetBlobStorageIntegrationStatusRequest.newBuilder() + .id(integrationId) + .build())) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + @Order(4) + void deleteIntegration() { + assertThat(client.blobStorageIntegrations().blobStorageIntegrationsDeleteBlobStorageIntegration( + APIBlobStorageIntegrationsDeleteBlobStorageIntegrationRequest.newBuilder() + .id(integrationId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson2Test.java new file mode 100644 index 0000000..37210bb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class CommentsApiAsyncJackson2Test extends CommentsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson3Test.java new file mode 100644 index 0000000..85249fc --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class CommentsApiAsyncJackson3Test extends CommentsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..d1680a4 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiAsyncTest.java @@ -0,0 +1,91 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.comments.CommentsApi.APICommentsCreateRequest; +import com.langfuse.api.comments.CommentsApi.APICommentsGetByIdRequest; +import com.langfuse.api.comments.CommentsApi.APICommentsGetRequest; +import com.langfuse.api.model.Comment; +import com.langfuse.api.model.CreateCommentRequest; + +/** + * Async integration tests for the Comments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class CommentsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String commentId; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "async-comments-test-trace"); + } + + @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( + 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( + 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( + 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/CommentsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiJackson2Test.java new file mode 100644 index 0000000..3b0e67a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class CommentsApiJackson2Test extends CommentsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiJackson3Test.java new file mode 100644 index 0000000..74642d4 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class CommentsApiJackson3Test extends CommentsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..ee7250f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/CommentsApiTest.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.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.APICommentsCreateRequest; +import com.langfuse.api.comments.CommentsApi.APICommentsGetByIdRequest; +import com.langfuse.api.comments.CommentsApi.APICommentsGetRequest; +import com.langfuse.api.model.Comment; +import com.langfuse.api.model.CommentObjectType; +import com.langfuse.api.model.CreateCommentRequest; + +/** + * Integration tests for the Comments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class CommentsApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String commentId; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "comments-test-trace"); + } + + @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( + 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( + 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( + 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/DatasetItemsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncJackson2Test.java new file mode 100644 index 0000000..d68881e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetItemsApiAsyncJackson2Test extends DatasetItemsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncJackson3Test.java new file mode 100644 index 0000000..f92a97d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetItemsApiAsyncJackson3Test extends DatasetItemsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..7304090 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiAsyncTest.java @@ -0,0 +1,95 @@ +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsDeleteRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsGetRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsListRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +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) +abstract class DatasetItemsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "async-test-dataset-items-" + UUID.randomUUID(); + private static String datasetItemId; + + @Test + @Order(1) + void createDatasetAndItem() { + client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + assertThat(client.asyncDatasetItems().datasetItemsCreate( + 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( + 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( + APIDatasetItemsListRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(items -> + assertThat(items.getData()) + .isNotEmpty() + .anyMatch(i -> datasetItemId.equals(i.getId()))); + } + + @Test + @Order(3) + void deleteDatasetItem() { + assertThat(client.asyncDatasetItems().datasetItemsDelete( + APIDatasetItemsDeleteRequest.newBuilder() + .id(datasetItemId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson2Test.java new file mode 100644 index 0000000..c3f4cd0 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetItemsApiJackson2Test extends DatasetItemsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson3Test.java new file mode 100644 index 0000000..69639c7 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetItemsApiJackson3Test extends DatasetItemsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..5c3d1ea --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetItemsApiTest.java @@ -0,0 +1,105 @@ +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsDeleteRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsGetRequest; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsListRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.DatasetItem; + +/** + * Integration tests for the Dataset Items API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class DatasetItemsApiTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "test-dataset-items-" + UUID.randomUUID(); + private static String datasetItemId; + + @Test + @Order(1) + void createDatasetAndItem() { + var dataset = client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + assertThat(dataset.getName()).isEqualTo(DATASET_NAME); + + assertThat(client.datasetItems().datasetItemsCreate( + 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( + 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( + 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); + } + + @Test + @Order(3) + void deleteDatasetItem() { + assertThat(client.datasetItems().datasetItemsDelete( + APIDatasetItemsDeleteRequest.newBuilder() + .id(datasetItemId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson2Test.java new file mode 100644 index 0000000..be8d0f5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetRunItemsApiAsyncJackson2Test extends DatasetRunItemsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson3Test.java new file mode 100644 index 0000000..a643569 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetRunItemsApiAsyncJackson3Test extends DatasetRunItemsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncTest.java new file mode 100644 index 0000000..0d48a82 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiAsyncTest.java @@ -0,0 +1,96 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.LangfuseApiException; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsListRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.CreateDatasetRunItemRequest; +import com.langfuse.api.model.DatasetRunItem; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class DatasetRunItemsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "async-test-run-items-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "async-test-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetId; + private static String datasetItemId; + + @Test + @Order(1) + void setupDatasetAndTrace() { + var dataset = client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + datasetId = dataset.getId(); + + assertThat(client.datasetItems().datasetItemsCreate( + 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(); + }); + + ingestTrace(TRACE_ID, SPAN_ID, "async-run-items-test-trace"); + } + + @Test + @Order(2) + void createDatasetRunItem() { + assertThat(client.asyncDatasetRunItems().datasetRunItemsCreate( + APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(datasetItemId) + .traceId(TRACE_ID) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(runItem -> { + assertThat(runItem.getId()).isNotBlank(); + assertThat(runItem.getCreatedAt()).isNotNull(); + }) + .extracting(DatasetRunItem::getDatasetRunName, DatasetRunItem::getDatasetItemId, DatasetRunItem::getTraceId) + .containsExactly(RUN_NAME, datasetItemId, TRACE_ID); + } + + @Test + @Order(3) + void listDatasetRunItemsReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.datasetRunItems().datasetRunItemsList( + APIDatasetRunItemsListRequest.newBuilder() + .datasetId(datasetId) + .runName(RUN_NAME) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson2Test.java new file mode 100644 index 0000000..fdffd63 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetRunItemsApiJackson2Test extends DatasetRunItemsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson3Test.java new file mode 100644 index 0000000..c14d75b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetRunItemsApiJackson3Test extends DatasetRunItemsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..36c85c7 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetRunItemsApiTest.java @@ -0,0 +1,99 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsListRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.model.CreateDatasetRunItemRequest; +import com.langfuse.api.model.DatasetRunItem; + +/** + * Integration tests for the Dataset Run Items API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class DatasetRunItemsApiTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "test-run-items-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "test-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetId; + private static String datasetItemId; + + @Test + @Order(1) + void setupDatasetAndTrace() { + var dataset = client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + datasetId = dataset.getId(); + + assertThat(client.datasetItems().datasetItemsCreate( + 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(); + }); + + ingestTrace(TRACE_ID, SPAN_ID, "run-items-test-trace"); + } + + @Test + @Order(2) + void createDatasetRunItem() { + assertThat(client.datasetRunItems().datasetRunItemsCreate( + 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); + } + + @Test + @Order(3) + void listDatasetRunItemsReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.datasetRunItems().datasetRunItemsList( + APIDatasetRunItemsListRequest.newBuilder() + .datasetId(datasetId) + .runName(RUN_NAME) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson2Test.java new file mode 100644 index 0000000..a31e18f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetsApiAsyncJackson2Test extends DatasetsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson3Test.java new file mode 100644 index 0000000..3376454 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetsApiAsyncJackson3Test extends DatasetsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..a206c83 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiAsyncTest.java @@ -0,0 +1,145 @@ +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.LangfuseApiException; +import com.langfuse.api.datasetItems.DatasetItemsApi.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsDeleteRunRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRunRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRunsRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsListRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.CreateDatasetRunItemRequest; +import com.langfuse.api.model.Dataset; + +/** + * Async integration tests for the Datasets API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class DatasetsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "async-test-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "async-test-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetId; + + @Test + @Order(1) + void createDataset() { + assertThat(client.asyncDatasets().datasetsCreate( + 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( + 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( + APIDatasetsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(datasets -> + assertThat(datasets.getData()) + .isNotEmpty() + .anyMatch(d -> DATASET_NAME.equals(d.getName()))); + } + + @Test + @Order(3) + void createRunForDataset() { + var item = client.datasetItems().datasetItemsCreate( + APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "async test")) + .build()) + .build()); + + ingestTrace(TRACE_ID, SPAN_ID, "async-datasets-run-test-trace"); + + client.datasetRunItems().datasetRunItemsCreate( + APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(item.getId()) + .traceId(TRACE_ID) + .build()) + .build()); + } + + @Test + @Order(4) + void getRunsReturns404InEventsOnlyMode() { + assertThat(client.asyncDatasets().datasetsGetRuns( + APIDatasetsGetRunsRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + @Order(4) + void getRunByNameReturns404InEventsOnlyMode() { + assertThat(client.asyncDatasets().datasetsGetRun( + APIDatasetsGetRunRequest.newBuilder() + .datasetName(DATASET_NAME) + .runName(RUN_NAME) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + @Order(5) + void deleteRunReturns404InEventsOnlyMode() { + assertThat(client.asyncDatasets().datasetsDeleteRun( + APIDatasetsDeleteRunRequest.newBuilder() + .datasetName(DATASET_NAME) + .runName(RUN_NAME) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson2Test.java new file mode 100644 index 0000000..1dab003 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetsApiJackson2Test extends DatasetsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson3Test.java new file mode 100644 index 0000000..6972b25 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class DatasetsApiJackson3Test extends DatasetsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..47bc0bc --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/DatasetsApiTest.java @@ -0,0 +1,154 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsDeleteRunRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRunRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsGetRunsRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsListRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.model.CreateDatasetRunItemRequest; +import com.langfuse.api.model.Dataset; + +/** + * Integration tests for the Datasets API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class DatasetsApiTest extends AbstractLangfuseClientTest { + + private final String DATASET_NAME = "test-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "test-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetId; + + @Test + @Order(1) + void createDataset() { + assertThat(client.datasets().datasetsCreate( + 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( + 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( + APIDatasetsListRequest.newBuilder() + .build())) + .satisfies(datasets -> { + assertThat(datasets.getData()) + .isNotEmpty() + .anyMatch(d -> DATASET_NAME.equals(d.getName())); + assertThat(datasets.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(3) + void createRunForDataset() { + var item = client.datasetItems().datasetItemsCreate( + APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "test")) + .build()) + .build()); + + ingestTrace(TRACE_ID, SPAN_ID, "datasets-run-test-trace"); + + client.datasetRunItems().datasetRunItemsCreate( + APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(item.getId()) + .traceId(TRACE_ID) + .build()) + .build()); + } + + @Test + @Order(4) + void getRunsReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.datasets().datasetsGetRuns( + APIDatasetsGetRunsRequest.newBuilder() + .datasetName(DATASET_NAME) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + @Order(4) + void getRunByNameReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.datasets().datasetsGetRun( + APIDatasetsGetRunRequest.newBuilder() + .datasetName(DATASET_NAME) + .runName(RUN_NAME) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + @Order(5) + void deleteRunReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.datasets().datasetsDeleteRun( + APIDatasetsDeleteRunRequest.newBuilder() + .datasetName(DATASET_NAME) + .runName(RUN_NAME) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson2Test.java new file mode 100644 index 0000000..a2c71fa --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ExperimentsApiAsyncJackson2Test extends ExperimentsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson3Test.java new file mode 100644 index 0000000..d5db12f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ExperimentsApiAsyncJackson3Test extends ExperimentsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncTest.java new file mode 100644 index 0000000..ef168a9 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiAsyncTest.java @@ -0,0 +1,101 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.OffsetDateTime; +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.experiments.ExperimentsApi.APIExperimentsListItemsRequest; +import com.langfuse.api.experiments.ExperimentsApi.APIExperimentsListRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.CreateDatasetRunItemRequest; + +/** + * Async integration tests for the Experiments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ExperimentsApiAsyncTest extends AbstractLangfuseClientTest { + + private final OffsetDateTime TEST_START = OffsetDateTime.now(); + private final String DATASET_NAME = "async-test-experiments-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "async-test-experiment-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetItemId; + + @Test + @Order(1) + void setupDatasetAndExperiment() { + client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + var item = client.datasetItems().datasetItemsCreate( + APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "What is an async experiment?")) + .build()) + .build()); + + assertThat(item.getId()).isNotBlank(); + datasetItemId = item.getId(); + + ingestTrace(TRACE_ID, SPAN_ID, "async-experiment-test-trace"); + + client.datasetRunItems().datasetRunItemsCreate( + APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(datasetItemId) + .traceId(TRACE_ID) + .build()) + .build()); + } + + @Test + @Order(2) + void listExperiments() { + assertThat(client.asyncExperiments().experimentsList( + APIExperimentsListRequest.newBuilder() + .fromStartTime(TEST_START.minusMinutes(1)) + .limit(50) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getData()).isNotNull(); + assertThat(response.getMeta()).isNotNull(); + }); + } + + @Test + @Order(2) + void listExperimentItems() { + assertThat(client.asyncExperiments().experimentsListItems( + APIExperimentsListItemsRequest.newBuilder() + .fromStartTime(TEST_START.minusMinutes(1)) + .limit(50) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getData()).isNotNull(); + assertThat(response.getMeta()).isNotNull(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson2Test.java new file mode 100644 index 0000000..8308a21 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ExperimentsApiJackson2Test extends ExperimentsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson3Test.java new file mode 100644 index 0000000..60d84b8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ExperimentsApiJackson3Test extends ExperimentsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiTest.java new file mode 100644 index 0000000..1e8fd03 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ExperimentsApiTest.java @@ -0,0 +1,98 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.OffsetDateTime; +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.APIDatasetItemsCreateRequest; +import com.langfuse.api.datasetRunItems.DatasetRunItemsApi.APIDatasetRunItemsCreateRequest; +import com.langfuse.api.datasets.DatasetsApi.APIDatasetsCreateRequest; +import com.langfuse.api.experiments.ExperimentsApi.APIExperimentsListItemsRequest; +import com.langfuse.api.experiments.ExperimentsApi.APIExperimentsListRequest; +import com.langfuse.api.model.CreateDatasetItemRequest; +import com.langfuse.api.model.CreateDatasetRequest; +import com.langfuse.api.model.CreateDatasetRunItemRequest; + +/** + * Integration tests for the Experiments API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ExperimentsApiTest extends AbstractLangfuseClientTest { + + private final OffsetDateTime TEST_START = OffsetDateTime.now(); + private final String DATASET_NAME = "test-experiments-dataset-" + UUID.randomUUID(); + private final String RUN_NAME = "test-experiment-run-" + UUID.randomUUID().toString().substring(0, 8); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String datasetItemId; + + @Test + @Order(1) + void setupDatasetAndExperiment() { + client.datasets().datasetsCreate( + APIDatasetsCreateRequest.newBuilder() + .createDatasetRequest(CreateDatasetRequest.builder() + .name(DATASET_NAME) + .build()) + .build()); + + var item = client.datasetItems().datasetItemsCreate( + APIDatasetItemsCreateRequest.newBuilder() + .createDatasetItemRequest(CreateDatasetItemRequest.builder() + .datasetName(DATASET_NAME) + .input(Map.of("question", "What is an experiment?")) + .build()) + .build()); + + assertThat(item.getId()).isNotBlank(); + datasetItemId = item.getId(); + + ingestTrace(TRACE_ID, SPAN_ID, "experiment-test-trace"); + + client.datasetRunItems().datasetRunItemsCreate( + APIDatasetRunItemsCreateRequest.newBuilder() + .createDatasetRunItemRequest(CreateDatasetRunItemRequest.builder() + .runName(RUN_NAME) + .datasetItemId(datasetItemId) + .traceId(TRACE_ID) + .build()) + .build()); + } + + @Test + @Order(2) + void listExperiments() { + assertThat(client.experiments().experimentsList( + APIExperimentsListRequest.newBuilder() + .fromStartTime(TEST_START.minusMinutes(1)) + .limit(50) + .build())) + .satisfies(response -> { + assertThat(response.getData()).isNotNull(); + assertThat(response.getMeta()).isNotNull(); + }); + } + + @Test + @Order(2) + void listExperimentItems() { + assertThat(client.experiments().experimentsListItems( + APIExperimentsListItemsRequest.newBuilder() + .fromStartTime(TEST_START.minusMinutes(1)) + .limit(50) + .build())) + .satisfies(response -> { + assertThat(response.getData()).isNotNull(); + assertThat(response.getMeta()).isNotNull(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson2Test.java new file mode 100644 index 0000000..9f7d9eb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class FeedbackApiAsyncJackson2Test extends FeedbackApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson3Test.java new file mode 100644 index 0000000..73b51eb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class FeedbackApiAsyncJackson3Test extends FeedbackApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncTest.java new file mode 100644 index 0000000..bec7aac --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiAsyncTest.java @@ -0,0 +1,35 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.feedback.FeedbackApi.APIFeedbackSubmitRequest; +import com.langfuse.api.model.FeedbackTargetType; +import com.langfuse.api.model.SubmitFeedbackRequest; + +/** + * Async integration tests for the Feedback API. + * + * @author Eric Deandrea + */ +abstract class FeedbackApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void submitFeedback() { + assertThat(client.asyncFeedback().feedbackSubmit( + APIFeedbackSubmitRequest.newBuilder() + .submitFeedbackRequest(SubmitFeedbackRequest.builder() + .targetType(FeedbackTargetType.PUBLIC_API) + .target("scores-create") + .feedback("Async integration test feedback") + .build()) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson2Test.java new file mode 100644 index 0000000..8e6522a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class FeedbackApiJackson2Test extends FeedbackApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson3Test.java new file mode 100644 index 0000000..42004bf --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class FeedbackApiJackson3Test extends FeedbackApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiTest.java new file mode 100644 index 0000000..549e4df --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/FeedbackApiTest.java @@ -0,0 +1,31 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.feedback.FeedbackApi.APIFeedbackSubmitRequest; +import com.langfuse.api.model.FeedbackTargetType; +import com.langfuse.api.model.SubmitFeedbackRequest; + +/** + * Integration tests for the Feedback API. + * + * @author Eric Deandrea + */ +abstract class FeedbackApiTest extends AbstractLangfuseClientTest { + + @Test + void submitFeedback() { + assertThatThrownBy(() -> client.feedback().feedbackSubmit( + APIFeedbackSubmitRequest.newBuilder() + .submitFeedbackRequest(SubmitFeedbackRequest.builder() + .targetType(FeedbackTargetType.PUBLIC_API) + .target("scores-create") + .feedback("Integration test feedback") + .build()) + .build())) + .isInstanceOf(LangfuseApiException.class); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson2Test.java new file mode 100644 index 0000000..91ffd86 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class HealthApiAsyncJackson2Test extends HealthApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson3Test.java new file mode 100644 index 0000000..704b306 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class HealthApiAsyncJackson3Test extends HealthApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..4bc2b44 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiAsyncTest.java @@ -0,0 +1,28 @@ +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 + */ +abstract 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/HealthApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiJackson2Test.java new file mode 100644 index 0000000..04f4ae8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class HealthApiJackson2Test extends HealthApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiJackson3Test.java new file mode 100644 index 0000000..72994de --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/HealthApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class HealthApiJackson3Test extends HealthApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..16c0833 --- /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 + */ +abstract 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/IngestionApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncJackson2Test.java new file mode 100644 index 0000000..dafdb73 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class IngestionApiAsyncJackson2Test extends IngestionApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncJackson3Test.java new file mode 100644 index 0000000..723521d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class IngestionApiAsyncJackson3Test extends IngestionApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..4579101 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiAsyncTest.java @@ -0,0 +1,138 @@ +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.APIIngestionBatchRequest; +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.IngestionEventOneOf1; +import com.langfuse.api.model.IngestionSuccess; +import com.langfuse.api.model.ScoreBody; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.model.TraceBody; + +/** + * Async integration tests for the Ingestion API. + * + * @author Eric Deandrea + */ +abstract class IngestionApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void traceCreateReturnsErrorInEventsOnlyMode() { + 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("async-test-trace") + .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.getErrors()) + .isNotEmpty() + .first() + .satisfies(error -> assertThat(error.getId()).isEqualTo(eventId)); + assertThat(response.getSuccesses()).isEmpty(); + }); + } + + @Test + void multipleTraceCreatesReturnErrorsInEventsOnlyMode() { + 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("async-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("async-batch-trace-2") + .build()) + .build())); + + assertThat(client.asyncIngestion().ingestionBatch( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(events) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getErrors()) + .hasSize(2) + .extracting("id") + .containsExactlyInAnyOrder(eventId1, eventId2); + assertThat(response.getSuccesses()).isEmpty(); + }); + } + + @Test + void scoreCreateSucceedsInEventsOnlyMode() { + var traceId = UUID.randomUUID().toString().replace("-", ""); + var spanId = traceId.substring(0, 16); + + ingestTrace(traceId, spanId, "async-score-ingestion-test-trace"); + + var eventId = UUID.randomUUID().toString(); + + var scoreEvent = IngestionEventOneOf1.builder() + .id(eventId) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf1.TypeEnum.SCORE_CREATE) + .body(ScoreBody.builder() + .traceId(traceId) + .name("async-ingestion-test-score") + .value(new CreateScoreValue(0.75)) + .dataType(ScoreDataType.NUMERIC) + .build()) + .build(); + + assertThat(client.asyncIngestion().ingestionBatch( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(scoreEvent))) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .hasSize(1) + .first() + .extracting(IngestionSuccess::getId, IngestionSuccess::getStatus) + .containsExactly(eventId, 201); + assertThat(response.getErrors()).isEmpty(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson2Test.java new file mode 100644 index 0000000..b3d10b4 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class IngestionApiJackson2Test extends IngestionApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson3Test.java new file mode 100644 index 0000000..12cadde --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class IngestionApiJackson3Test extends IngestionApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..844df55 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/IngestionApiTest.java @@ -0,0 +1,134 @@ +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.APIIngestionBatchRequest; +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.IngestionEventOneOf1; +import com.langfuse.api.model.IngestionSuccess; +import com.langfuse.api.model.ScoreBody; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.model.TraceBody; + +/** + * Integration tests for the Ingestion API. + * + * @author Eric Deandrea + */ +abstract class IngestionApiTest extends AbstractLangfuseClientTest { + + @Test + void traceCreateReturnsErrorInEventsOnlyMode() { + 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( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(traceEvent))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getErrors()) + .isNotEmpty() + .first() + .satisfies(error -> assertThat(error.getId()).isEqualTo(eventId)); + assertThat(response.getSuccesses()).isEmpty(); + }); + } + + @Test + void multipleTraceCreatesReturnErrorsInEventsOnlyMode() { + 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( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(events) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getErrors()) + .hasSize(2) + .extracting("id") + .containsExactlyInAnyOrder(eventId1, eventId2); + assertThat(response.getSuccesses()).isEmpty(); + }); + } + + @Test + void scoreCreateSucceedsInEventsOnlyMode() { + var traceId = UUID.randomUUID().toString().replace("-", ""); + var spanId = traceId.substring(0, 16); + + ingestTrace(traceId, spanId, "score-ingestion-test-trace"); + + var eventId = UUID.randomUUID().toString(); + + var scoreEvent = IngestionEventOneOf1.builder() + .id(eventId) + .timestamp(OffsetDateTime.now().toString()) + .type(IngestionEventOneOf1.TypeEnum.SCORE_CREATE) + .body(ScoreBody.builder() + .traceId(traceId) + .name("ingestion-test-score") + .value(new CreateScoreValue(0.75)) + .dataType(ScoreDataType.NUMERIC) + .build()) + .build(); + + assertThat(client.ingestion().ingestionBatch( + APIIngestionBatchRequest.newBuilder() + .ingestionBatchRequest(IngestionBatchRequest.builder() + .batch(List.of(new IngestionEvent(scoreEvent))) + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getSuccesses()) + .hasSize(1) + .first() + .extracting(IngestionSuccess::getId, IngestionSuccess::getStatus) + .containsExactly(eventId, 201); + 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..d1f75d5 --- /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 org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApi; + +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/LegacyMetricsV1ApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncJackson2Test.java new file mode 100644 index 0000000..d13e03d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyMetricsV1ApiAsyncJackson2Test extends LegacyMetricsV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncJackson3Test.java new file mode 100644 index 0000000..2c3dc34 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyMetricsV1ApiAsyncJackson3Test extends LegacyMetricsV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncTest.java new file mode 100644 index 0000000..06f8d5c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiAsyncTest.java @@ -0,0 +1,33 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.legacyMetricsV1.LegacyMetricsV1Api.APILegacyMetricsV1MetricsRequest; + +abstract class LegacyMetricsV1ApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void legacyMetricsReturns404InEventsOnlyMode() { + var query = """ + { + "view": "traces", + "metrics": [{"measure": "count", "aggregation": "count"}], + "fromTimestamp": "2020-01-01T00:00:00.000Z", + "toTimestamp": "2099-01-01T00:00:00.000Z" + }"""; + + assertThatThrownBy(() -> + client.legacyMetricsV1().legacyMetricsV1Metrics( + APILegacyMetricsV1MetricsRequest.newBuilder() + .query(query) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson2Test.java new file mode 100644 index 0000000..d7509f3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyMetricsV1ApiJackson2Test extends LegacyMetricsV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson3Test.java new file mode 100644 index 0000000..8218232 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyMetricsV1ApiJackson3Test extends LegacyMetricsV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiTest.java new file mode 100644 index 0000000..9df4b14 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyMetricsV1ApiTest.java @@ -0,0 +1,40 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.legacyMetricsV1.LegacyMetricsV1Api.APILegacyMetricsV1MetricsRequest; + +/** + * Integration tests for the Legacy Metrics V1 API. + * + *

In Langfuse v4 {@code events_only} mode, legacy metrics endpoints return 404. + * + * @author Eric Deandrea + */ +abstract class LegacyMetricsV1ApiTest extends AbstractLangfuseClientTest { + + @Test + void legacyMetricsReturns404InEventsOnlyMode() { + var query = """ + { + "view": "traces", + "metrics": [{"measure": "count", "aggregation": "count"}], + "fromTimestamp": "2020-01-01T00:00:00.000Z", + "toTimestamp": "2099-01-01T00:00:00.000Z" + }"""; + + assertThatThrownBy(() -> + client.legacyMetricsV1().legacyMetricsV1Metrics( + APILegacyMetricsV1MetricsRequest.newBuilder() + .query(query) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson2Test.java new file mode 100644 index 0000000..2f6bd22 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyObservationsV1ApiAsyncJackson2Test extends LegacyObservationsV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson3Test.java new file mode 100644 index 0000000..f93df9b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyObservationsV1ApiAsyncJackson3Test extends LegacyObservationsV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncTest.java new file mode 100644 index 0000000..62452d8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiAsyncTest.java @@ -0,0 +1,40 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api.APILegacyObservationsV1GetManyRequest; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api.APILegacyObservationsV1GetRequest; + +abstract class LegacyObservationsV1ApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void legacyGetObservationReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.legacyObservationsV1().legacyObservationsV1Get( + APILegacyObservationsV1GetRequest.newBuilder() + .observationId(UUID.randomUUID().toString()) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } + + @Test + void legacyListObservationsReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.legacyObservationsV1().legacyObservationsV1GetMany( + APILegacyObservationsV1GetManyRequest.newBuilder() + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson2Test.java new file mode 100644 index 0000000..778a257 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyObservationsV1ApiJackson2Test extends LegacyObservationsV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson3Test.java new file mode 100644 index 0000000..28fc71e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyObservationsV1ApiJackson3Test extends LegacyObservationsV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiTest.java new file mode 100644 index 0000000..b276bb2 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyObservationsV1ApiTest.java @@ -0,0 +1,47 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api.APILegacyObservationsV1GetManyRequest; +import com.langfuse.api.legacyObservationsV1.LegacyObservationsV1Api.APILegacyObservationsV1GetRequest; + +/** + * Integration tests for the Legacy Observations V1 API. + * + *

In Langfuse v4 {@code events_only} mode, legacy observation endpoints return 404. + * + * @author Eric Deandrea + */ +abstract class LegacyObservationsV1ApiTest extends AbstractLangfuseClientTest { + + @Test + void legacyGetObservationReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.legacyObservationsV1().legacyObservationsV1Get( + APILegacyObservationsV1GetRequest.newBuilder() + .observationId(UUID.randomUUID().toString()) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } + + @Test + void legacyListObservationsReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.legacyObservationsV1().legacyObservationsV1GetMany( + APILegacyObservationsV1GetManyRequest.newBuilder() + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isIn(404, 400)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson2Test.java new file mode 100644 index 0000000..8a5c519 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyScoreV1ApiAsyncJackson2Test extends LegacyScoreV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson3Test.java new file mode 100644 index 0000000..6804d60 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyScoreV1ApiAsyncJackson3Test extends LegacyScoreV1ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncTest.java new file mode 100644 index 0000000..a468dd8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiAsyncTest.java @@ -0,0 +1,63 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api.APILegacyScoreV1DeleteRequest; +import com.langfuse.api.model.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class LegacyScoreV1ApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String scoreId; + + @Test + @Order(1) + void ingestTraceAndCreateScore() { + ingestTrace(TRACE_ID, SPAN_ID, "async-legacy-score-test-trace"); + + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> { + var response = client.scores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name("async-legacy-delete-test") + .value(new CreateScoreValue(1.0)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .build()) + .build()); + + assertThat(response.getId()).isNotBlank(); + scoreId = response.getId(); + }); + } + + @Test + @Order(2) + void deleteScoreViaLegacyEndpoint() { + assertThat(client.asyncLegacyScoreV1().legacyScoreV1Delete( + APILegacyScoreV1DeleteRequest.newBuilder() + .scoreId(scoreId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson2Test.java new file mode 100644 index 0000000..da9c5f3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyScoreV1ApiJackson2Test extends LegacyScoreV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson3Test.java new file mode 100644 index 0000000..3063760 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LegacyScoreV1ApiJackson3Test extends LegacyScoreV1ApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiTest.java new file mode 100644 index 0000000..8124621 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LegacyScoreV1ApiTest.java @@ -0,0 +1,69 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.legacyScoreV1.LegacyScoreV1Api.APILegacyScoreV1DeleteRequest; +import com.langfuse.api.model.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; + +/** + * Integration tests for the Legacy Score V1 API. + * + *

In v4, only the delete operation remains on the legacy endpoint. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class LegacyScoreV1ApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String scoreId; + + @Test + @Order(1) + void ingestTraceAndCreateScore() { + ingestTrace(TRACE_ID, SPAN_ID, "legacy-score-test-trace"); + + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> { + var response = client.scores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name("legacy-delete-test") + .value(new CreateScoreValue(1.0)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .build()) + .build()); + + assertThat(response.getId()).isNotBlank(); + scoreId = response.getId(); + }); + } + + @Test + @Order(2) + void deleteScoreViaLegacyEndpoint() { + client.legacyScoreV1().legacyScoreV1Delete( + APILegacyScoreV1DeleteRequest.newBuilder() + .scoreId(scoreId) + .build()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson2Test.java new file mode 100644 index 0000000..f69c2ee --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LlmConnectionsApiAsyncJackson2Test extends LlmConnectionsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson3Test.java new file mode 100644 index 0000000..5cdf428 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LlmConnectionsApiAsyncJackson3Test extends LlmConnectionsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncTest.java new file mode 100644 index 0000000..e8c0234 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiAsyncTest.java @@ -0,0 +1,65 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +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.llmConnections.LlmConnectionsApi.APILlmConnectionsDeleteRequest; +import com.langfuse.api.llmConnections.LlmConnectionsApi.APILlmConnectionsListRequest; +import com.langfuse.api.llmConnections.LlmConnectionsApi.APILlmConnectionsUpsertRequest; +import com.langfuse.api.model.LlmAdapter; +import com.langfuse.api.model.UpsertLlmConnectionRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class LlmConnectionsApiAsyncTest extends AbstractLangfuseClientTest { + + private static String connectionId; + + @Test + @Order(1) + void listLlmConnections() { + assertThat(client.asyncLlmConnections().llmConnectionsList( + APILlmConnectionsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + void upsertLlmConnection() { + assertThat(client.asyncLlmConnections().llmConnectionsUpsert( + APILlmConnectionsUpsertRequest.newBuilder() + .upsertLlmConnectionRequest(UpsertLlmConnectionRequest.builder() + .provider("async-test-provider") + .adapter(LlmAdapter.OPENAI) + .secretKey("sk-async-test-placeholder") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(connection -> { + assertThat(connection.getId()).isNotBlank(); + assertThat(connection.getProvider()).isEqualTo("async-test-provider"); + assertThat(connection.getAdapter()).isEqualTo(LlmAdapter.OPENAI.getValue()); + connectionId = connection.getId(); + }); + } + + @Test + @Order(3) + void deleteLlmConnection() { + assertThat(client.asyncLlmConnections().llmConnectionsDelete( + APILlmConnectionsDeleteRequest.newBuilder() + .id(connectionId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson2Test.java new file mode 100644 index 0000000..77293ad --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LlmConnectionsApiJackson2Test extends LlmConnectionsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson3Test.java new file mode 100644 index 0000000..0ee58fe --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class LlmConnectionsApiJackson3Test extends LlmConnectionsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiTest.java new file mode 100644 index 0000000..b6ac509 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/LlmConnectionsApiTest.java @@ -0,0 +1,65 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +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.llmConnections.LlmConnectionsApi.APILlmConnectionsDeleteRequest; +import com.langfuse.api.llmConnections.LlmConnectionsApi.APILlmConnectionsListRequest; +import com.langfuse.api.llmConnections.LlmConnectionsApi.APILlmConnectionsUpsertRequest; +import com.langfuse.api.model.LlmAdapter; +import com.langfuse.api.model.UpsertLlmConnectionRequest; + +/** + * Integration tests for the LLM Connections API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class LlmConnectionsApiTest extends AbstractLangfuseClientTest { + + private static String connectionId; + + @Test + @Order(1) + void listLlmConnections() { + assertThat(client.llmConnections().llmConnectionsList( + APILlmConnectionsListRequest.newBuilder() + .build())) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + void upsertLlmConnection() { + assertThat(client.llmConnections().llmConnectionsUpsert( + APILlmConnectionsUpsertRequest.newBuilder() + .upsertLlmConnectionRequest(UpsertLlmConnectionRequest.builder() + .provider("test-provider") + .adapter(LlmAdapter.OPENAI) + .secretKey("sk-test-placeholder") + .build()) + .build())) + .satisfies(connection -> { + assertThat(connection.getId()).isNotBlank(); + assertThat(connection.getProvider()).isEqualTo("test-provider"); + assertThat(connection.getAdapter()).isEqualTo(LlmAdapter.OPENAI.getValue()); + connectionId = connection.getId(); + }); + } + + @Test + @Order(3) + void deleteLlmConnection() { + assertThat(client.llmConnections().llmConnectionsDelete( + APILlmConnectionsDeleteRequest.newBuilder() + .id(connectionId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson2Test.java new file mode 100644 index 0000000..978ef90 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MediaApiAsyncJackson2Test extends MediaApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson3Test.java new file mode 100644 index 0000000..7ca18d5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MediaApiAsyncJackson3Test extends MediaApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncTest.java new file mode 100644 index 0000000..68892ab --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiAsyncTest.java @@ -0,0 +1,73 @@ +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.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.media.MediaApi.APIMediaGetUploadUrlRequest; +import com.langfuse.api.media.MediaApi.APIMediaPatchRequest; +import com.langfuse.api.model.GetMediaUploadUrlRequest; +import com.langfuse.api.model.MediaContentType; +import com.langfuse.api.model.PatchMediaBody; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class MediaApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String mediaId; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "async-media-test-trace"); + } + + @Test + @Order(2) + void getUploadUrl() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncMedia().mediaGetUploadUrl( + APIMediaGetUploadUrlRequest.newBuilder() + .getMediaUploadUrlRequest(GetMediaUploadUrlRequest.builder() + .traceId(TRACE_ID) + .observationId(SPAN_ID) + .contentType(MediaContentType.TEXT_PLAIN) + .contentLength(13) + .sha256Hash("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=") + .field("input") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> { + assertThat(response.getMediaId()).isNotBlank(); + mediaId = response.getMediaId(); + })); + } + + @Test + @Order(3) + void patchMedia() { + assertThat(client.asyncMedia().mediaPatch( + APIMediaPatchRequest.newBuilder() + .mediaId(mediaId) + .patchMediaBody(PatchMediaBody.builder() + .uploadedAt(OffsetDateTime.now()) + .uploadHttpStatus(200) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson2Test.java new file mode 100644 index 0000000..e0130c0 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MediaApiJackson2Test extends MediaApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson3Test.java new file mode 100644 index 0000000..ab4c920 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MediaApiJackson3Test extends MediaApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiTest.java new file mode 100644 index 0000000..f5d08a1 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MediaApiTest.java @@ -0,0 +1,75 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.media.MediaApi.APIMediaGetUploadUrlRequest; +import com.langfuse.api.media.MediaApi.APIMediaPatchRequest; +import com.langfuse.api.model.GetMediaUploadUrlRequest; +import com.langfuse.api.model.MediaContentType; +import com.langfuse.api.model.PatchMediaBody; + +/** + * Integration tests for the Media API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class MediaApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private static String mediaId; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "media-test-trace"); + } + + @Test + @Order(2) + void getUploadUrl() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.media().mediaGetUploadUrl( + APIMediaGetUploadUrlRequest.newBuilder() + .getMediaUploadUrlRequest(GetMediaUploadUrlRequest.builder() + .traceId(TRACE_ID) + .observationId(SPAN_ID) + .contentType(MediaContentType.TEXT_PLAIN) + .contentLength(13) + .sha256Hash("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=") + .field("input") + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getMediaId()).isNotBlank(); + mediaId = response.getMediaId(); + })); + } + + @Test + @Order(3) + void patchMedia() { + client.media().mediaPatch( + APIMediaPatchRequest.newBuilder() + .mediaId(mediaId) + .patchMediaBody(PatchMediaBody.builder() + .uploadedAt(java.time.OffsetDateTime.now()) + .uploadHttpStatus(200) + .build()) + .build()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson2Test.java new file mode 100644 index 0000000..3866174 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MetricsApiAsyncJackson2Test extends MetricsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson3Test.java new file mode 100644 index 0000000..974e3b9 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MetricsApiAsyncJackson3Test extends MetricsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncTest.java new file mode 100644 index 0000000..3f1e1f5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiAsyncTest.java @@ -0,0 +1,31 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.metrics.MetricsApi.APIMetricsMetricsRequest; + +abstract class MetricsApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void queryObservationCount() { + var query = """ + { + "view": "observations", + "metrics": [{"measure": "count", "aggregation": "count"}], + "fromTimestamp": "2020-01-01T00:00:00.000Z", + "toTimestamp": "2099-01-01T00:00:00.000Z" + }"""; + + assertThat(client.asyncMetrics().metricsMetrics( + APIMetricsMetricsRequest.newBuilder() + .query(query) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson2Test.java new file mode 100644 index 0000000..2bd8f22 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MetricsApiJackson2Test extends MetricsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson3Test.java new file mode 100644 index 0000000..c5d5284 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class MetricsApiJackson3Test extends MetricsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiTest.java new file mode 100644 index 0000000..6e43d18 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/MetricsApiTest.java @@ -0,0 +1,34 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.metrics.MetricsApi.APIMetricsMetricsRequest; + +/** + * Integration tests for the Metrics V2 API. + * + * @author Eric Deandrea + */ +abstract class MetricsApiTest extends AbstractLangfuseClientTest { + + @Test + void queryObservationCount() { + var query = """ + { + "view": "observations", + "metrics": [{"measure": "count", "aggregation": "count"}], + "fromTimestamp": "2020-01-01T00:00:00.000Z", + "toTimestamp": "2099-01-01T00:00:00.000Z" + }"""; + + assertThat(client.metrics().metricsMetrics( + APIMetricsMetricsRequest.newBuilder() + .query(query) + .build())) + .isNotNull() + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson2Test.java new file mode 100644 index 0000000..460a30b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ModelsApiAsyncJackson2Test extends ModelsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson3Test.java new file mode 100644 index 0000000..ecfb5df --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ModelsApiAsyncJackson3Test extends ModelsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..61db05d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiAsyncTest.java @@ -0,0 +1,89 @@ +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.APIModelsCreateRequest; +import com.langfuse.api.models.ModelsApi.APIModelsDeleteRequest; +import com.langfuse.api.models.ModelsApi.APIModelsGetRequest; +import com.langfuse.api.models.ModelsApi.APIModelsListRequest; + +/** + * Async integration tests for the Models API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ModelsApiAsyncTest extends AbstractLangfuseClientTest { + + private 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( + 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( + 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( + 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( + APIModelsDeleteRequest.newBuilder() + .id(modelId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson2Test.java new file mode 100644 index 0000000..80defc8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ModelsApiJackson2Test extends ModelsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson3Test.java new file mode 100644 index 0000000..1fb3c46 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ModelsApiJackson3Test extends ModelsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..6ba060b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ModelsApiTest.java @@ -0,0 +1,97 @@ +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.APIModelsCreateRequest; +import com.langfuse.api.models.ModelsApi.APIModelsDeleteRequest; +import com.langfuse.api.models.ModelsApi.APIModelsGetRequest; +import com.langfuse.api.models.ModelsApi.APIModelsListRequest; + +/** + * Integration tests for the Models API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ModelsApiTest extends AbstractLangfuseClientTest { + + private 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( + 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( + 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( + 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( + APIModelsDeleteRequest.newBuilder() + .id(modelId) + .build()); + + assertThat(client.models().modelsList( + 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/ObservationsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncJackson2Test.java new file mode 100644 index 0000000..010ae87 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ObservationsApiAsyncJackson2Test extends ObservationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncJackson3Test.java new file mode 100644 index 0000000..7062d1d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ObservationsApiAsyncJackson3Test extends ObservationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..1bc964a --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiAsyncTest.java @@ -0,0 +1,89 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.model.ObservationV2; +import com.langfuse.api.model.OtelSpan; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; + +/** + * Async integration tests for the Observations API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ObservationsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String ROOT_SPAN_ID = TRACE_ID.substring(0, 16); + private final String CHILD_SPAN_ID = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + private final String CHILD_SPAN_NAME = "async-observations-test-span-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSpan() { + var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); + var endNanos = String.valueOf(Long.parseLong(nowNanos) + 1_000_000_000L); + + var rootSpan = OtelSpan.builder() + .traceId(TRACE_ID) + .spanId(ROOT_SPAN_ID) + .name("root-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(endNanos) + .build(); + + var childSpan = OtelSpan.builder() + .traceId(TRACE_ID) + .spanId(CHILD_SPAN_ID) + .parentSpanId(ROOT_SPAN_ID) + .name(CHILD_SPAN_NAME) + .kind(2) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(endNanos) + .build(); + + ingestTraceWithSpans("async-observations-test-trace", Map.of(), rootSpan, childSpan); + } + + @Test + @Order(2) + void listObservationsViaV2Api() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncObservations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(observations -> { + assertThat(observations.getData()) + .isNotEmpty() + .anyMatch(o -> CHILD_SPAN_NAME.equals(o.getName())); + + var span = observations.getData().stream() + .filter(o -> CHILD_SPAN_NAME.equals(o.getName())) + .findFirst() + .orElseThrow(); + + assertThat(span) + .satisfies(s -> assertThat(s.getStartTime()).isNotNull()) + .extracting(ObservationV2::getTraceId, ObservationV2::getType) + .containsExactly(TRACE_ID, "SPAN"); + })); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson2Test.java new file mode 100644 index 0000000..f47f0c8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ObservationsApiJackson2Test extends ObservationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson3Test.java new file mode 100644 index 0000000..ca53f87 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ObservationsApiJackson3Test extends ObservationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..387aa56 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ObservationsApiTest.java @@ -0,0 +1,88 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.model.ObservationV2; +import com.langfuse.api.model.OtelSpan; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; + +/** + * Integration tests for the Observations API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ObservationsApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String ROOT_SPAN_ID = TRACE_ID.substring(0, 16); + private final String CHILD_SPAN_ID = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + private final String CHILD_SPAN_NAME = "observations-test-span-" + UUID.randomUUID(); + + @Test + @Order(1) + void ingestTraceWithSpan() { + var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); + var endNanos = String.valueOf(Long.parseLong(nowNanos) + 1_000_000_000L); + + var rootSpan = OtelSpan.builder() + .traceId(TRACE_ID) + .spanId(ROOT_SPAN_ID) + .name("root-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(endNanos) + .build(); + + var childSpan = OtelSpan.builder() + .traceId(TRACE_ID) + .spanId(CHILD_SPAN_ID) + .parentSpanId(ROOT_SPAN_ID) + .name(CHILD_SPAN_NAME) + .kind(2) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(endNanos) + .build(); + + ingestTraceWithSpans("observations-test-trace", Map.of(), rootSpan, childSpan); + } + + @Test + @Order(2) + void listObservationsViaV2Api() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.observations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(observations -> { + assertThat(observations.getData()) + .isNotEmpty() + .anyMatch(o -> CHILD_SPAN_NAME.equals(o.getName())); + + var span = observations.getData().stream() + .filter(o -> CHILD_SPAN_NAME.equals(o.getName())) + .findFirst() + .orElseThrow(); + + assertThat(span) + .satisfies(s -> assertThat(s.getStartTime()).isNotNull()) + .extracting(ObservationV2::getTraceId, ObservationV2::getType) + .containsExactly(TRACE_ID, "SPAN"); + })); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson2Test.java new file mode 100644 index 0000000..c6c35bf --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class OpentelemetryApiAsyncJackson2Test extends OpentelemetryApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson3Test.java new file mode 100644 index 0000000..094a6e2 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class OpentelemetryApiAsyncJackson3Test extends OpentelemetryApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncTest.java new file mode 100644 index 0000000..f908954 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiAsyncTest.java @@ -0,0 +1,79 @@ +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.Test; + +import com.langfuse.api.model.OpentelemetryExportTracesRequest; +import com.langfuse.api.model.OtelAttribute; +import com.langfuse.api.model.OtelAttributeValue; +import com.langfuse.api.model.OtelResource; +import com.langfuse.api.model.OtelResourceSpan; +import com.langfuse.api.model.OtelScope; +import com.langfuse.api.model.OtelScopeSpan; +import com.langfuse.api.model.OtelSpan; +import com.langfuse.api.opentelemetry.OpentelemetryApi.APIOpentelemetryExportTracesRequest; + +abstract class OpentelemetryApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void exportTraces() { + var traceId = UUID.randomUUID().toString().replace("-", ""); + var spanId = traceId.substring(0, 16); + var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); + + var span = OtelSpan.builder() + .traceId(traceId) + .spanId(spanId) + .name("async-otel-test-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(String.valueOf(Long.parseLong(nowNanos) + 1_000_000_000L)) + .attributes(List.of( + OtelAttribute.builder() + .key("test.attribute") + .value(OtelAttributeValue.builder() + .stringValue("async-test-value") + .build()) + .build())) + .build(); + + var resourceSpan = OtelResourceSpan.builder() + .resource(OtelResource.builder() + .attributes(List.of( + OtelAttribute.builder() + .key("langfuse.trace.name") + .value(OtelAttributeValue.builder() + .stringValue("async-otel-export-test") + .build()) + .build(), + OtelAttribute.builder() + .key("service.name") + .value(OtelAttributeValue.builder() + .stringValue("langfuse-java-async-test") + .build()) + .build())) + .build()) + .scopeSpans(List.of(OtelScopeSpan.builder() + .scope(OtelScope.builder() + .name("langfuse-java-async-test") + .version("1.0.0") + .build()) + .spans(List.of(span)) + .build())) + .build(); + + assertThat(client.asyncOpentelemetry().opentelemetryExportTraces( + APIOpentelemetryExportTracesRequest.newBuilder() + .opentelemetryExportTracesRequest(OpentelemetryExportTracesRequest.builder() + .resourceSpans(List.of(resourceSpan)) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson2Test.java new file mode 100644 index 0000000..7214624 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class OpentelemetryApiJackson2Test extends OpentelemetryApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson3Test.java new file mode 100644 index 0000000..84b1329 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class OpentelemetryApiJackson3Test extends OpentelemetryApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiTest.java new file mode 100644 index 0000000..144b5fa --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OpentelemetryApiTest.java @@ -0,0 +1,82 @@ +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.Test; + +import com.langfuse.api.model.OpentelemetryExportTracesRequest; +import com.langfuse.api.model.OtelAttribute; +import com.langfuse.api.model.OtelAttributeValue; +import com.langfuse.api.model.OtelResource; +import com.langfuse.api.model.OtelResourceSpan; +import com.langfuse.api.model.OtelScope; +import com.langfuse.api.model.OtelScopeSpan; +import com.langfuse.api.model.OtelSpan; +import com.langfuse.api.opentelemetry.OpentelemetryApi.APIOpentelemetryExportTracesRequest; + +/** + * Integration tests for the OpenTelemetry API. + * + * @author Eric Deandrea + */ +abstract class OpentelemetryApiTest extends AbstractLangfuseClientTest { + + @Test + void exportTraces() { + var traceId = UUID.randomUUID().toString().replace("-", ""); + var spanId = traceId.substring(0, 16); + var nowNanos = String.valueOf(System.currentTimeMillis() * 1_000_000L); + + var span = OtelSpan.builder() + .traceId(traceId) + .spanId(spanId) + .name("otel-test-span") + .kind(1) + .startTimeUnixNano(nowNanos) + .endTimeUnixNano(String.valueOf(Long.parseLong(nowNanos) + 1_000_000_000L)) + .attributes(List.of( + OtelAttribute.builder() + .key("test.attribute") + .value(OtelAttributeValue.builder() + .stringValue("test-value") + .build()) + .build())) + .build(); + + var resourceSpan = OtelResourceSpan.builder() + .resource(OtelResource.builder() + .attributes(List.of( + OtelAttribute.builder() + .key("langfuse.trace.name") + .value(OtelAttributeValue.builder() + .stringValue("otel-export-test") + .build()) + .build(), + OtelAttribute.builder() + .key("service.name") + .value(OtelAttributeValue.builder() + .stringValue("langfuse-java-test") + .build()) + .build())) + .build()) + .scopeSpans(List.of(OtelScopeSpan.builder() + .scope(OtelScope.builder() + .name("langfuse-java-test") + .version("1.0.0") + .build()) + .spans(List.of(span)) + .build())) + .build(); + + assertThat(client.opentelemetry().opentelemetryExportTraces( + APIOpentelemetryExportTracesRequest.newBuilder() + .opentelemetryExportTracesRequest(OpentelemetryExportTracesRequest.builder() + .resourceSpans(List.of(resourceSpan)) + .build()) + .build())) + .isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson2Test.java new file mode 100644 index 0000000..f7faa1b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class OrganizationsApiAsyncJackson2Test extends OrganizationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson3Test.java new file mode 100644 index 0000000..fc704b4 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class OrganizationsApiAsyncJackson3Test extends OrganizationsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncTest.java new file mode 100644 index 0000000..fc38d68 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiAsyncTest.java @@ -0,0 +1,117 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.langfuse.api.model.DeleteMembershipRequest; +import com.langfuse.api.model.MembershipRequest; +import com.langfuse.api.model.MembershipRole; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsDeleteOrganizationMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsDeleteProjectMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsGetProjectMembershipsRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsUpdateOrganizationMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsUpdateProjectMembershipRequest; + +/** + * Async integration tests for the Organizations API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +abstract class OrganizationsApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void listOrganizationApiKeys() { + assertThat(client.asyncOrganizations().organizationsGetOrganizationApiKeys()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getApiKeys()).isNotNull()); + } + + @Test + void listOrganizationMemberships() { + assertThat(client.asyncOrganizations().organizationsGetOrganizationMemberships()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMemberships()).isNotNull()); + } + + @Test + void listOrganizationProjects() { + assertThat(client.asyncOrganizations().organizationsGetOrganizationProjects()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getProjects()).isNotNull()); + } + + @Test + void getProjectMemberships() { + assertThat(client.asyncOrganizations().organizationsGetProjectMemberships( + APIOrganizationsGetProjectMembershipsRequest.newBuilder() + .projectId("langfuse-dev-project") + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMemberships()).isNotNull()); + } + + @Test + void updateOrganizationMembership() { + assertThat(client.asyncOrganizations().organizationsUpdateOrganizationMembership( + APIOrganizationsUpdateOrganizationMembershipRequest.newBuilder() + .membershipRequest(MembershipRequest.builder() + .userId("placeholder-user-id") + .role(MembershipRole.MEMBER) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getUserId()).isNotBlank()); + } + + @Test + void updateProjectMembership() { + assertThat(client.asyncOrganizations().organizationsUpdateProjectMembership( + APIOrganizationsUpdateProjectMembershipRequest.newBuilder() + .projectId("langfuse-dev-project") + .membershipRequest(MembershipRequest.builder() + .userId("placeholder-user-id") + .role(MembershipRole.MEMBER) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getUserId()).isNotBlank()); + } + + @Test + void deleteOrganizationMembership() { + assertThat(client.asyncOrganizations().organizationsDeleteOrganizationMembership( + APIOrganizationsDeleteOrganizationMembershipRequest.newBuilder() + .deleteMembershipRequest(DeleteMembershipRequest.builder() + .userId("placeholder-user-id") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } + + @Test + void deleteProjectMembership() { + assertThat(client.asyncOrganizations().organizationsDeleteProjectMembership( + APIOrganizationsDeleteProjectMembershipRequest.newBuilder() + .projectId("langfuse-dev-project") + .deleteMembershipRequest(DeleteMembershipRequest.builder() + .userId("placeholder-user-id") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson2Test.java new file mode 100644 index 0000000..78665f9 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class OrganizationsApiJackson2Test extends OrganizationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson3Test.java new file mode 100644 index 0000000..c714edf --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class OrganizationsApiJackson3Test extends OrganizationsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiTest.java new file mode 100644 index 0000000..9231318 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/OrganizationsApiTest.java @@ -0,0 +1,107 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.langfuse.api.model.DeleteMembershipRequest; +import com.langfuse.api.model.MembershipRequest; +import com.langfuse.api.model.MembershipRole; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsDeleteOrganizationMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsDeleteProjectMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsGetProjectMembershipsRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsUpdateOrganizationMembershipRequest; +import com.langfuse.api.organizations.OrganizationsApi.APIOrganizationsUpdateProjectMembershipRequest; + +/** + * Integration tests for the Organizations API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +abstract class OrganizationsApiTest extends AbstractLangfuseClientTest { + + @Test + void listOrganizationApiKeys() { + assertThat(client.organizations().organizationsGetOrganizationApiKeys()) + .satisfies(response -> + assertThat(response.getApiKeys()).isNotNull()); + } + + @Test + void listOrganizationMemberships() { + assertThat(client.organizations().organizationsGetOrganizationMemberships()) + .satisfies(response -> + assertThat(response.getMemberships()).isNotNull()); + } + + @Test + void listOrganizationProjects() { + assertThat(client.organizations().organizationsGetOrganizationProjects()) + .satisfies(response -> + assertThat(response.getProjects()).isNotNull()); + } + + @Test + void getProjectMemberships() { + assertThat(client.organizations().organizationsGetProjectMemberships( + APIOrganizationsGetProjectMembershipsRequest.newBuilder() + .projectId("langfuse-dev-project") + .build())) + .satisfies(response -> + assertThat(response.getMemberships()).isNotNull()); + } + + @Test + void updateOrganizationMembership() { + assertThat(client.organizations().organizationsUpdateOrganizationMembership( + APIOrganizationsUpdateOrganizationMembershipRequest.newBuilder() + .membershipRequest(MembershipRequest.builder() + .userId("placeholder-user-id") + .role(MembershipRole.MEMBER) + .build()) + .build())) + .satisfies(response -> + assertThat(response.getUserId()).isNotBlank()); + } + + @Test + void updateProjectMembership() { + assertThat(client.organizations().organizationsUpdateProjectMembership( + APIOrganizationsUpdateProjectMembershipRequest.newBuilder() + .projectId("langfuse-dev-project") + .membershipRequest(MembershipRequest.builder() + .userId("placeholder-user-id") + .role(MembershipRole.MEMBER) + .build()) + .build())) + .satisfies(response -> + assertThat(response.getUserId()).isNotBlank()); + } + + @Test + void deleteOrganizationMembership() { + assertThat(client.organizations().organizationsDeleteOrganizationMembership( + APIOrganizationsDeleteOrganizationMembershipRequest.newBuilder() + .deleteMembershipRequest(DeleteMembershipRequest.builder() + .userId("placeholder-user-id") + .build()) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } + + @Test + void deleteProjectMembership() { + assertThat(client.organizations().organizationsDeleteProjectMembership( + APIOrganizationsDeleteProjectMembershipRequest.newBuilder() + .projectId("langfuse-dev-project") + .deleteMembershipRequest(DeleteMembershipRequest.builder() + .userId("placeholder-user-id") + .build()) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson2Test.java new file mode 100644 index 0000000..9c61a7b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ProjectsApiAsyncJackson2Test extends ProjectsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson3Test.java new file mode 100644 index 0000000..6b62774 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ProjectsApiAsyncJackson3Test extends ProjectsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..b501f9d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiAsyncTest.java @@ -0,0 +1,54 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Async integration tests for the Projects API. + * + * @author Eric Deandrea + */ +abstract class ProjectsApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void getProjects() { + assertThat(client.asyncProjects().projectsGet()) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(projects -> + assertThat(projects.getData()).isNotEmpty()); + } + + @Test + @Disabled("Requires org-admin role") + void createProject() { + } + + @Test + @Disabled("Requires project creation in previous step") + void updateProject() { + } + + @Test + @Disabled("Requires project creation in previous step") + void createApiKey() { + } + + @Test + @Disabled("Requires API key creation in previous step") + void getApiKeys() { + } + + @Test + @Disabled("Requires API key creation in previous step") + void deleteApiKey() { + } + + @Test + @Disabled("Requires project creation in previous step") + void deleteProject() { + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson2Test.java new file mode 100644 index 0000000..9dc8d51 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ProjectsApiJackson2Test extends ProjectsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson3Test.java new file mode 100644 index 0000000..f141cf3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ProjectsApiJackson3Test extends ProjectsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..d4d116d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ProjectsApiTest.java @@ -0,0 +1,141 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.junit.jupiter.api.Disabled; +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.ProjectsCreateApiKeyRequest; +import com.langfuse.api.model.ProjectsCreateRequest; +import com.langfuse.api.model.ProjectsUpdateRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsCreateApiKeyRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsCreateRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsDeleteApiKeyRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsDeleteRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsGetApiKeysRequest; +import com.langfuse.api.projects.ProjectsApi.APIProjectsUpdateRequest; + +/** + * Integration tests for the Projects API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ProjectsApiTest extends AbstractLangfuseClientTest { + + private static String createdProjectId; + private static String apiKeyId; + + @Test + @Order(1) + 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(); + }); + }); + } + + @Test + @Order(2) + @Disabled("Requires org-admin role") + void createProject() { + assertThat(client.projects().projectsCreate( + APIProjectsCreateRequest.newBuilder() + .projectsCreateRequest(ProjectsCreateRequest.builder() + .name("test-project-" + UUID.randomUUID().toString().substring(0, 8)) + .build()) + .build())) + .satisfies(project -> { + assertThat(project.getId()).isNotBlank(); + assertThat(project.getName()).isNotBlank(); + createdProjectId = project.getId(); + }); + } + + @Test + @Order(3) + @Disabled("Requires project creation in previous step") + void updateProject() { + assertThat(client.projects().projectsUpdate( + APIProjectsUpdateRequest.newBuilder() + .projectId(createdProjectId) + .projectsUpdateRequest(ProjectsUpdateRequest.builder() + .name("updated-project-" + UUID.randomUUID().toString().substring(0, 8)) + .build()) + .build())) + .satisfies(project -> { + assertThat(project.getId()).isEqualTo(createdProjectId); + assertThat(project.getName()).startsWith("updated-project-"); + }); + } + + @Test + @Order(4) + @Disabled("Requires project creation in previous step") + void createApiKey() { + assertThat(client.projects().projectsCreateApiKey( + APIProjectsCreateApiKeyRequest.newBuilder() + .projectId(createdProjectId) + .projectsCreateApiKeyRequest(ProjectsCreateApiKeyRequest.builder() + .note("Integration test API key") + .build()) + .build())) + .satisfies(response -> { + assertThat(response.getId()).isNotBlank(); + assertThat(response.getPublicKey()).isNotBlank(); + assertThat(response.getSecretKey()).isNotBlank(); + apiKeyId = response.getId(); + }); + } + + @Test + @Order(5) + @Disabled("Requires API key creation in previous step") + void getApiKeys() { + assertThat(client.projects().projectsGetApiKeys( + APIProjectsGetApiKeysRequest.newBuilder() + .projectId(createdProjectId) + .build())) + .satisfies(keys -> + assertThat(keys.getApiKeys()) + .isNotEmpty() + .anyMatch(k -> apiKeyId.equals(k.getId()))); + } + + @Test + @Order(6) + @Disabled("Requires API key creation in previous step") + void deleteApiKey() { + assertThat(client.projects().projectsDeleteApiKey( + APIProjectsDeleteApiKeyRequest.newBuilder() + .projectId(createdProjectId) + .apiKeyId(apiKeyId) + .build())) + .satisfies(response -> + assertThat(response.getSuccess()).isTrue()); + } + + @Test + @Order(7) + @Disabled("Requires project creation in previous step") + void deleteProject() { + assertThat(client.projects().projectsDelete( + APIProjectsDeleteRequest.newBuilder() + .projectId(createdProjectId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson2Test.java new file mode 100644 index 0000000..759e484 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptVersionApiAsyncJackson2Test extends PromptVersionApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson3Test.java new file mode 100644 index 0000000..39827c6 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptVersionApiAsyncJackson3Test extends PromptVersionApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncTest.java new file mode 100644 index 0000000..a110be2 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiAsyncTest.java @@ -0,0 +1,69 @@ +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.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.APIPromptVersionUpdateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsCreateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsGetRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class PromptVersionApiAsyncTest extends AbstractLangfuseClientTest { + + private final String PROMPT_NAME = "async-test-prompt-version-" + UUID.randomUUID(); + + @Test + @Order(1) + void createPrompt() { + assertThat(client.prompts().promptsCreate( + APIPromptsCreateRequest.newBuilder() + .createPromptRequest(new CreatePromptRequest( + CreateTextPromptRequest.builder() + .name(PROMPT_NAME) + .prompt("Async Version 1: Hello {{name}}") + .type(CreateTextPromptType.TEXT) + .labels(List.of("staging")) + .build())) + .build())) + .isNotNull(); + } + + @Test + @Order(2) + void updatePromptVersion() { + assertThat(client.asyncPromptVersion().promptVersionUpdate( + APIPromptVersionUpdateRequest.newBuilder() + .name(PROMPT_NAME) + .version(1) + .promptVersionUpdateRequest(PromptVersionUpdateRequest.builder() + .newLabels(List.of("production")) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(3) + void fetchUpdatedPromptVersion() { + assertThat(client.asyncPrompts().promptsGet( + APIPromptsGetRequest.newBuilder() + .promptName(PROMPT_NAME) + .label("production") + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson2Test.java new file mode 100644 index 0000000..6d56753 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptVersionApiJackson2Test extends PromptVersionApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson3Test.java new file mode 100644 index 0000000..f6cf9b5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptVersionApiJackson3Test extends PromptVersionApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..cc417b8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptVersionApiTest.java @@ -0,0 +1,71 @@ +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.APIPromptVersionUpdateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsCreateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsGetRequest; + +/** + * Integration tests for the Prompt Version API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class PromptVersionApiTest extends AbstractLangfuseClientTest { + + private final String PROMPT_NAME = "test-prompt-version-" + UUID.randomUUID(); + + @Test + @Order(1) + void createPrompt() { + assertThat(client.prompts().promptsCreate( + 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( + 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( + APIPromptsGetRequest.newBuilder() + .promptName(PROMPT_NAME) + .label("production") + .build())) + .isNotNull(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson2Test.java new file mode 100644 index 0000000..d4e4776 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptsApiAsyncJackson2Test extends PromptsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson3Test.java new file mode 100644 index 0000000..2b30a21 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptsApiAsyncJackson3Test extends PromptsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..5a50a80 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiAsyncTest.java @@ -0,0 +1,122 @@ +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.APIPromptsCreateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsDeleteRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsGetRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsListRequest; + +/** + * Async integration tests for the Prompts API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class PromptsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TEXT_PROMPT_NAME = "async-test-text-prompt-" + UUID.randomUUID(); + private final String CHAT_PROMPT_NAME = "async-test-chat-prompt-" + UUID.randomUUID(); + + @Test + @Order(1) + void createTextPrompt() { + assertThat(client.asyncPrompts().promptsCreate( + 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( + 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( + APIPromptsGetRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(2) + void fetchChatPrompt() { + assertThat(client.asyncPrompts().promptsGet( + APIPromptsGetRequest.newBuilder() + .promptName(CHAT_PROMPT_NAME) + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(2) + void listPromptsContainsCreatedPrompts() { + assertThat(client.asyncPrompts().promptsList( + APIPromptsListRequest.newBuilder() + .build()) + ) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(prompts -> + assertThat(prompts.getData()).isNotEmpty()); + } + + @Test + @Order(3) + void deletePrompt() { + assertThat(client.asyncPrompts().promptsDelete( + APIPromptsDeleteRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson2Test.java new file mode 100644 index 0000000..6e90eab --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptsApiJackson2Test extends PromptsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson3Test.java new file mode 100644 index 0000000..5a587b1 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class PromptsApiJackson3Test extends PromptsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..a8fe41d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/PromptsApiTest.java @@ -0,0 +1,130 @@ +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.APIPromptsCreateRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsDeleteRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsGetRequest; +import com.langfuse.api.prompts.PromptsApi.APIPromptsListRequest; + +/** + * Integration tests for the Prompts API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class PromptsApiTest extends AbstractLangfuseClientTest { + + private final String TEXT_PROMPT_NAME = "test-text-prompt-" + UUID.randomUUID(); + private final String CHAT_PROMPT_NAME = "test-chat-prompt-" + UUID.randomUUID(); + + @Test + @Order(1) + void createTextPrompt() { + var prompt = client.prompts().promptsCreate( + 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( + 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( + APIPromptsGetRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(2) + void fetchChatPrompt() { + var prompt = client.prompts().promptsGet( + APIPromptsGetRequest.newBuilder() + .promptName(CHAT_PROMPT_NAME) + .build()); + + assertThat(prompt) + .isNotNull(); + + } + + @Test + @Order(2) + void listPromptsContainsCreatedPrompts() { + var prompts = client.prompts().promptsList( + 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())); + } + + @Test + @Order(3) + void deletePrompt() { + client.prompts().promptsDelete( + APIPromptsDeleteRequest.newBuilder() + .promptName(TEXT_PROMPT_NAME) + .build()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson2Test.java new file mode 100644 index 0000000..26d3040 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class ScimApiAsyncJackson2Test extends ScimApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson3Test.java new file mode 100644 index 0000000..5a9cbd7 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class ScimApiAsyncJackson3Test extends ScimApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncTest.java new file mode 100644 index 0000000..16db544 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiAsyncTest.java @@ -0,0 +1,106 @@ +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.Disabled; +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.ScimCreateUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimCreateUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimDeleteUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimGetUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimListUsersRequest; + +/** + * Async integration tests for the SCIM API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScimApiAsyncTest extends AbstractLangfuseClientTest { + + private static String userId; + + @Test + @Order(1) + void getServiceProviderConfig() { + assertThat(client.asyncScim().scimGetServiceProviderConfig()) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(1) + void getResourceTypes() { + assertThat(client.asyncScim().scimGetResourceTypes()) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(1) + void getSchemas() { + assertThat(client.asyncScim().scimGetSchemas()) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(1) + void listUsers() { + assertThat(client.asyncScim().scimListUsers( + APIScimListUsersRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getResources()).isNotNull()); + } + + @Test + @Order(2) + void createUser() { + assertThat(client.asyncScim().scimCreateUser( + APIScimCreateUserRequest.newBuilder() + .scimCreateUserRequest(ScimCreateUserRequest.builder() + .userName("scim-test-" + UUID.randomUUID().toString().substring(0, 8) + "@example.com") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(user -> { + assertThat(user.getId()).isNotBlank(); + assertThat(user.getUserName()).isNotBlank(); + userId = user.getId(); + }); + } + + @Test + @Order(3) + void getUser() { + assertThat(client.asyncScim().scimGetUser( + APIScimGetUserRequest.newBuilder() + .userId(userId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(user -> { + assertThat(user.getId()).isEqualTo(userId); + assertThat(user.getUserName()).isNotBlank(); + }); + } + + @Test + @Order(4) + void deleteUser() { + assertThat(client.asyncScim().scimDeleteUser( + APIScimDeleteUserRequest.newBuilder() + .userId(userId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson2Test.java new file mode 100644 index 0000000..fc0cfaa --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson2Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class ScimApiJackson2Test extends ScimApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson3Test.java new file mode 100644 index 0000000..bed89c7 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiJackson3Test.java @@ -0,0 +1,13 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; +import org.junit.jupiter.api.Disabled; + +@Disabled("Requires org-admin role") +class ScimApiJackson3Test extends ScimApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiTest.java new file mode 100644 index 0000000..c252e01 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScimApiTest.java @@ -0,0 +1,98 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.junit.jupiter.api.Disabled; +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.ScimCreateUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimCreateUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimDeleteUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimGetUserRequest; +import com.langfuse.api.scim.ScimApi.APIScimListUsersRequest; + +/** + * Integration tests for the SCIM API. + * + * @author Eric Deandrea + */ +@Disabled("Requires org-admin role") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScimApiTest extends AbstractLangfuseClientTest { + + private static String userId; + + @Test + @Order(1) + void getServiceProviderConfig() { + assertThat(client.scim().scimGetServiceProviderConfig()) + .isNotNull(); + } + + @Test + @Order(1) + void getResourceTypes() { + assertThat(client.scim().scimGetResourceTypes()) + .isNotNull(); + } + + @Test + @Order(1) + void getSchemas() { + assertThat(client.scim().scimGetSchemas()) + .isNotNull(); + } + + @Test + @Order(1) + void listUsers() { + assertThat(client.scim().scimListUsers( + APIScimListUsersRequest.newBuilder() + .build())) + .satisfies(response -> + assertThat(response.getResources()).isNotNull()); + } + + @Test + @Order(2) + void createUser() { + assertThat(client.scim().scimCreateUser( + APIScimCreateUserRequest.newBuilder() + .scimCreateUserRequest(ScimCreateUserRequest.builder() + .userName("scim-test-" + UUID.randomUUID().toString().substring(0, 8) + "@example.com") + .build()) + .build())) + .satisfies(user -> { + assertThat(user.getId()).isNotBlank(); + assertThat(user.getUserName()).isNotBlank(); + userId = user.getId(); + }); + } + + @Test + @Order(3) + void getUser() { + assertThat(client.scim().scimGetUser( + APIScimGetUserRequest.newBuilder() + .userId(userId) + .build())) + .satisfies(user -> { + assertThat(user.getId()).isEqualTo(userId); + assertThat(user.getUserName()).isNotBlank(); + }); + } + + @Test + @Order(4) + void deleteUser() { + client.scim().scimDeleteUser( + APIScimDeleteUserRequest.newBuilder() + .userId(userId) + .build()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson2Test.java new file mode 100644 index 0000000..e2a3db3 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoreConfigsApiAsyncJackson2Test extends ScoreConfigsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson3Test.java new file mode 100644 index 0000000..34a420c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoreConfigsApiAsyncJackson3Test extends ScoreConfigsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..d8756d8 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiAsyncTest.java @@ -0,0 +1,98 @@ +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.model.UpdateScoreConfigRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsCreateRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsGetByIdRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsGetRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsUpdateRequest; + +/** + * Async integration tests for the Score Configs API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoreConfigsApiAsyncTest extends AbstractLangfuseClientTest { + + private 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( + 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( + 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( + APIScoreConfigsGetRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(configs -> + assertThat(configs.getData()) + .isNotEmpty() + .anyMatch(c -> CONFIG_NAME.equals(c.getName()))); + } + + @Test + @Order(3) + void updateScoreConfig() { + assertThat(client.asyncScoreConfigs().scoreConfigsUpdate( + APIScoreConfigsUpdateRequest.newBuilder() + .configId(configId) + .updateScoreConfigRequest(UpdateScoreConfigRequest.builder() + .description("Updated async description") + .isArchived(true) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(config -> { + assertThat(config.getId()).isEqualTo(configId); + assertThat(config.getDescription()).isEqualTo("Updated async description"); + assertThat(config.getIsArchived()).isTrue(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson2Test.java new file mode 100644 index 0000000..fdece9e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoreConfigsApiJackson2Test extends ScoreConfigsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson3Test.java new file mode 100644 index 0000000..d9f5444 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoreConfigsApiJackson3Test extends ScoreConfigsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..73e7eee --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoreConfigsApiTest.java @@ -0,0 +1,98 @@ +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.model.UpdateScoreConfigRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsCreateRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsGetByIdRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsGetRequest; +import com.langfuse.api.scoreConfigs.ScoreConfigsApi.APIScoreConfigsUpdateRequest; + +/** + * Integration tests for the Score Configs API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoreConfigsApiTest extends AbstractLangfuseClientTest { + + private 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( + 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( + 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( + APIScoreConfigsGetRequest.newBuilder() + .build())) + .satisfies(configs -> { + assertThat(configs.getData()) + .isNotEmpty() + .anyMatch(c -> CONFIG_NAME.equals(c.getName())); + assertThat(configs.getMeta().getTotalItems()).isGreaterThan(0); + }); + } + + @Test + @Order(3) + void updateScoreConfig() { + assertThat(client.scoreConfigs().scoreConfigsUpdate( + APIScoreConfigsUpdateRequest.newBuilder() + .configId(configId) + .updateScoreConfigRequest(UpdateScoreConfigRequest.builder() + .description("Updated description") + .isArchived(true) + .build()) + .build())) + .satisfies(config -> { + assertThat(config.getId()).isEqualTo(configId); + assertThat(config.getDescription()).isEqualTo("Updated description"); + assertThat(config.getIsArchived()).isTrue(); + }); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson2Test.java new file mode 100644 index 0000000..b8dab9f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresApiAsyncJackson2Test extends ScoresApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson3Test.java new file mode 100644 index 0000000..f58bd0b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresApiAsyncJackson3Test extends ScoresApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..985747d --- /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.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.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; +import com.langfuse.api.scores.ScoresApi.APIScoresGetByIdRequest; +import com.langfuse.api.scores.ScoresApi.APIScoresGetManyRequest; +import com.langfuse.api.scoresV3.ScoresV3Api.APIScoresV3GetManyV3Request; + +/** + * Async integration tests for the Scores API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoresApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String SCORE_NAME = "async-test-score"; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "async-score-test-trace"); + } + + @Test + @Order(1) + void createScore() { + assertThat(client.asyncScores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.85)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .environment("default") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> assertThat(response.getId()).isNotBlank()); + } + + @Test + void scoresGetByIdReturns404InEventsOnlyMode() { + assertThat(client.asyncScores().scoresGetById( + APIScoresGetByIdRequest.newBuilder() + .scoreId(UUID.randomUUID().toString()) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + void scoresV2GetManyReturns404InEventsOnlyMode() { + assertThat(client.asyncScores().scoresGetMany( + APIScoresGetManyRequest.newBuilder() + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + @Order(2) + void listScoresViaV3() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(e -> e instanceof LangfuseApiException) + .untilAsserted(() -> + assertThat(client.asyncScoresV3().scoresV3GetManyV3( + APIScoresV3GetManyV3Request.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/ScoresApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiJackson2Test.java new file mode 100644 index 0000000..23f7fef --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresApiJackson2Test extends ScoresApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiJackson3Test.java new file mode 100644 index 0000000..8d1833b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresApiJackson3Test extends ScoresApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..65ec8e7 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresApiTest.java @@ -0,0 +1,101 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.model.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; +import com.langfuse.api.scores.ScoresApi.APIScoresGetByIdRequest; +import com.langfuse.api.scores.ScoresApi.APIScoresGetManyRequest; +import com.langfuse.api.scoresV3.ScoresV3Api.APIScoresV3GetManyV3Request; + +/** + * Integration tests for the Scores API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoresApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String SCORE_NAME = "test-score"; + + @Test + @Order(1) + void ingestTrace() { + ingestTrace(TRACE_ID, SPAN_ID, "score-test-trace"); + } + + @Test + @Order(1) + void createScore() { + assertThat(client.scores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.95)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .environment("default") + .build()) + .build())) + .satisfies(response -> + assertThat(response.getId()).isNotBlank()); + } + + @Test + void scoresV2GetManyReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.scores().scoresGetMany( + APIScoresGetManyRequest.newBuilder() + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + @Order(2) + void listScoresViaV3() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.scoresV3().scoresV3GetManyV3( + APIScoresV3GetManyV3Request.newBuilder() + .name(SCORE_NAME) + .traceId(TRACE_ID) + .build())) + .satisfies(scores -> + assertThat(scores.getData()).isNotEmpty())); + } + + @Test + void scoresV2GetByIdReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.scores().scoresGetById( + APIScoresGetByIdRequest.newBuilder() + .scoreId(UUID.randomUUID().toString()) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson2Test.java new file mode 100644 index 0000000..793ec45 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresV3ApiAsyncJackson2Test extends ScoresV3ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson3Test.java new file mode 100644 index 0000000..1476ccb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresV3ApiAsyncJackson3Test extends ScoresV3ApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncTest.java new file mode 100644 index 0000000..be46736 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiAsyncTest.java @@ -0,0 +1,75 @@ +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.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.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; +import com.langfuse.api.scoresV3.ScoresV3Api.APIScoresV3GetManyV3Request; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoresV3ApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String SCORE_NAME = "async-v3-score-" + UUID.randomUUID().toString().substring(0, 8); + + @Test + @Order(1) + void ingestTraceViaOtel() { + ingestTrace(TRACE_ID, SPAN_ID, "async-scores-v3-test-trace"); + } + + @Test + @Order(2) + void createScore() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncScores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.75)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getId()).isNotBlank())); + } + + @Test + @Order(3) + void listScoresViaV3() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncScoresV3().scoresV3GetManyV3( + APIScoresV3GetManyV3Request.newBuilder() + .name(SCORE_NAME) + .fromTimestamp(OffsetDateTime.parse("2020-01-01T00:00:00Z")) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()) + .hasSize(1))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson2Test.java new file mode 100644 index 0000000..c25d424 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresV3ApiJackson2Test extends ScoresV3ApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson3Test.java new file mode 100644 index 0000000..bce846f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class ScoresV3ApiJackson3Test extends ScoresV3ApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiTest.java new file mode 100644 index 0000000..d0df0ff --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/ScoresV3ApiTest.java @@ -0,0 +1,78 @@ +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.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.CreateScoreRequest; +import com.langfuse.api.model.CreateScoreSource; +import com.langfuse.api.model.CreateScoreValue; +import com.langfuse.api.model.ScoreDataType; +import com.langfuse.api.scores.ScoresApi.APIScoresCreateRequest; +import com.langfuse.api.scoresV3.ScoresV3Api.APIScoresV3GetManyV3Request; + +/** + * Integration tests for the Scores V3 API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class ScoresV3ApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String SCORE_NAME = "v3-score-" + UUID.randomUUID().toString().substring(0, 8); + + @Test + @Order(1) + void ingestTraceViaOtel() { + ingestTrace(TRACE_ID, SPAN_ID, "scores-v3-test-trace"); + } + + @Test + @Order(2) + void createScore() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.scores().scoresCreate( + APIScoresCreateRequest.newBuilder() + .createScoreRequest(CreateScoreRequest.builder() + .traceId(TRACE_ID) + .name(SCORE_NAME) + .value(new CreateScoreValue(0.75)) + .dataType(ScoreDataType.NUMERIC) + .source(CreateScoreSource.API) + .build()) + .build())) + .satisfies(response -> + assertThat(response.getId()).isNotBlank())); + } + + @Test + @Order(3) + void listScoresViaV3() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.scoresV3().scoresV3GetManyV3( + APIScoresV3GetManyV3Request.newBuilder() + .name(SCORE_NAME) + .fromTimestamp(OffsetDateTime.parse("2020-01-01T00:00:00Z")) + .build())) + .satisfies(response -> + assertThat(response.getData()) + .hasSize(1))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson2Test.java new file mode 100644 index 0000000..86dd4a5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class SessionsApiAsyncJackson2Test extends SessionsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson3Test.java new file mode 100644 index 0000000..4013c1b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class SessionsApiAsyncJackson3Test extends SessionsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..eafc009 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiAsyncTest.java @@ -0,0 +1,79 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; +import com.langfuse.api.sessions.SessionsApi.APISessionsGetRequest; +import com.langfuse.api.sessions.SessionsApi.APISessionsListRequest; + +/** + * Async integration tests for the Sessions API. + * + *

In Langfuse v4 {@code events_only} mode, legacy sessions endpoints return 404. + * This test verifies the 404 behavior and uses the v2 observations API as the replacement. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class SessionsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String SESSION_ID = "async-test-session-" + UUID.randomUUID(); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + + @Test + void sessionsGetReturns404InEventsOnlyMode() { + assertThat(client.asyncSessions().sessionsGet( + APISessionsGetRequest.newBuilder() + .sessionId("nonexistent-" + UUID.randomUUID()) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + void sessionsListReturns404InEventsOnlyMode() { + assertThat(client.asyncSessions().sessionsList( + APISessionsListRequest.newBuilder() + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + @Order(1) + void ingestTraceWithSession() { + ingestTrace(TRACE_ID, SPAN_ID, "async-sessions-test-trace", Map.of("session.id", SESSION_ID)); + } + + @Test + @Order(2) + void querySessionDataViaV2ObservationsAsync() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncObservations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()) + .isNotEmpty() + .anyMatch(o -> "root-span".equals(o.getName())))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson2Test.java new file mode 100644 index 0000000..997dfdb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class SessionsApiJackson2Test extends SessionsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson3Test.java new file mode 100644 index 0000000..01b6cdb --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class SessionsApiJackson3Test extends SessionsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..08dbae1 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/SessionsApiTest.java @@ -0,0 +1,84 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; +import com.langfuse.api.sessions.SessionsApi.APISessionsGetRequest; +import com.langfuse.api.sessions.SessionsApi.APISessionsListRequest; + +/** + * Integration tests for the Sessions API. + * + *

In Langfuse v4 {@code events_only} mode, legacy sessions endpoints return 404. + * This test verifies the 404 behavior and uses the v2 observations API with + * {@code traceId} filter as the replacement. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class SessionsApiTest extends AbstractLangfuseClientTest { + + private final String SESSION_ID = "test-session-" + UUID.randomUUID(); + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + + @Test + void sessionsGetReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.sessions().sessionsGet( + APISessionsGetRequest.newBuilder() + .sessionId("nonexistent-" + UUID.randomUUID()) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + void sessionsListReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.sessions().sessionsList( + APISessionsListRequest.newBuilder() + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + @Order(1) + void ingestTraceWithSession() { + ingestTrace(TRACE_ID, SPAN_ID, "sessions-test-trace", Map.of("session.id", SESSION_ID)); + } + + @Test + @Order(2) + void querySessionDataViaV2ObservationsWithSessionIdFilter() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.observations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(response -> + assertThat(response.getData()) + .isNotEmpty() + .anyMatch(o -> "root-span".equals(o.getName())))); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson2Test.java new file mode 100644 index 0000000..c8c41a9 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class TraceApiAsyncJackson2Test extends TraceApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson3Test.java new file mode 100644 index 0000000..11dd022 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class TraceApiAsyncJackson3Test extends TraceApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..76ed6fd --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiAsyncTest.java @@ -0,0 +1,107 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +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.LangfuseApiException; +import com.langfuse.api.model.TraceDeleteMultipleRequest; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; +import com.langfuse.api.trace.TraceApi.APITraceDeleteMultipleRequest; +import com.langfuse.api.trace.TraceApi.APITraceDeleteRequest; +import com.langfuse.api.trace.TraceApi.APITraceGetRequest; +import com.langfuse.api.trace.TraceApi.APITraceListRequest; + +/** + * Async integration tests for the Trace API. + * + *

In Langfuse v4 {@code events_only} mode, legacy trace read endpoints return 404. + * This test verifies the 404 behavior and uses the v2 observations API as the replacement. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class TraceApiAsyncTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String TRACE_NAME = "async-trace-api-test-" + UUID.randomUUID(); + + @Test + void traceGetReturns404InEventsOnlyMode() { + assertThat(client.asyncTrace().traceGet( + APITraceGetRequest.newBuilder() + .traceId(UUID.randomUUID().toString()) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + void traceListReturns404InEventsOnlyMode() { + assertThat(client.asyncTrace().traceList( + APITraceListRequest.newBuilder() + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } + + @Test + @Order(1) + void ingestTraceViaOtel() { + ingestTrace(TRACE_ID, SPAN_ID, TRACE_NAME); + } + + @Test + @Order(2) + void queryTraceDataViaV2ObservationsAsync() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.asyncObservations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()) + .isNotEmpty() + .anyMatch(o -> "root-span".equals(o.getName())))); + } + + @Test + @Order(3) + void traceDelete() { + assertThat(client.asyncTrace().traceDelete( + APITraceDeleteRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } + + @Test + void traceDeleteMultiple() { + assertThat(client.asyncTrace().traceDeleteMultiple( + APITraceDeleteMultipleRequest.newBuilder() + .traceDeleteMultipleRequest(TraceDeleteMultipleRequest.builder() + .traceIds(List.of(UUID.randomUUID().toString().replace("-", ""))) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson2Test.java new file mode 100644 index 0000000..c3539e4 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class TraceApiJackson2Test extends TraceApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson3Test.java new file mode 100644 index 0000000..0eac72b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class TraceApiJackson3Test extends TraceApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} 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..608ab8f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/TraceApiTest.java @@ -0,0 +1,110 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +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.LangfuseApiException; +import com.langfuse.api.model.TraceDeleteMultipleRequest; +import com.langfuse.api.observations.ObservationsApi.APIObservationsGetManyRequest; +import com.langfuse.api.trace.TraceApi.APITraceDeleteMultipleRequest; +import com.langfuse.api.trace.TraceApi.APITraceDeleteRequest; +import com.langfuse.api.trace.TraceApi.APITraceGetRequest; +import com.langfuse.api.trace.TraceApi.APITraceListRequest; + +/** + * Integration tests for the Trace API. + * + *

In Langfuse v4 {@code events_only} mode, legacy trace read endpoints return 404. + * This test verifies the 404 behavior and uses the v2 observations API as the replacement. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class TraceApiTest extends AbstractLangfuseClientTest { + + private final String TRACE_ID = UUID.randomUUID().toString().replace("-", ""); + private final String SPAN_ID = TRACE_ID.substring(0, 16); + private final String TRACE_NAME = "trace-api-test-" + UUID.randomUUID(); + + @Test + void traceGetReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.trace().traceGet( + APITraceGetRequest.newBuilder() + .traceId(UUID.randomUUID().toString()) + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + void traceListReturns404InEventsOnlyMode() { + assertThatThrownBy(() -> + client.trace().traceList( + APITraceListRequest.newBuilder() + .build())) + .isInstanceOf(LangfuseApiException.class) + .satisfies(e -> + assertThat(((LangfuseApiException) e).getStatusCode()) + .isEqualTo(404)); + } + + @Test + @Order(1) + void ingestTraceViaOtel() { + ingestTrace(TRACE_ID, SPAN_ID, TRACE_NAME, Map.of("user.id", "test-user")); + } + + @Test + @Order(2) + void queryTraceDataViaV2Observations() { + await().atMost(Duration.ofSeconds(15)) + .pollInterval(Duration.ofSeconds(1)) + .ignoreExceptionsMatching(LangfuseApiException.class::isInstance) + .untilAsserted(() -> + assertThat(client.observations().observationsGetMany( + APIObservationsGetManyRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(response -> + assertThat(response.getData()) + .isNotEmpty() + .anyMatch(o -> "root-span".equals(o.getName())))); + } + + @Test + @Order(3) + void traceDelete() { + assertThat(client.trace().traceDelete( + APITraceDeleteRequest.newBuilder() + .traceId(TRACE_ID) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } + + @Test + void traceDeleteMultiple() { + assertThat(client.trace().traceDeleteMultiple( + APITraceDeleteMultipleRequest.newBuilder() + .traceDeleteMultipleRequest(TraceDeleteMultipleRequest.builder() + .traceIds(List.of(UUID.randomUUID().toString().replace("-", ""))) + .build()) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson2Test.java new file mode 100644 index 0000000..21d7d9d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardWidgetsApiAsyncJackson2Test extends UnstableDashboardWidgetsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson3Test.java new file mode 100644 index 0000000..28642e1 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardWidgetsApiAsyncJackson3Test extends UnstableDashboardWidgetsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncTest.java new file mode 100644 index 0000000..408b73b --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiAsyncTest.java @@ -0,0 +1,111 @@ +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.UnstableCreateDashboardWidgetRequest; +import com.langfuse.api.model.UnstableDashboardWidget; +import com.langfuse.api.model.UnstableDashboardWidgetChartType; +import com.langfuse.api.model.UnstableDashboardWidgetMetric; +import com.langfuse.api.model.UnstableDashboardWidgetMetricAggregation; +import com.langfuse.api.model.UnstableDashboardWidgetView; +import com.langfuse.api.model.UnstableUpdateDashboardWidgetRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsCreateRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsDeleteRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsGetRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsListRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsUpdateRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableDashboardWidgetsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String WIDGET_NAME = "async-test-widget-" + UUID.randomUUID().toString().substring(0, 8); + private static String widgetId; + + @Test + @Order(1) + void createWidget() { + assertThat(client.asyncUnstableDashboardWidgets().unstableDashboardWidgetsCreate( + APIUnstableDashboardWidgetsCreateRequest.newBuilder() + .unstableCreateDashboardWidgetRequest(UnstableCreateDashboardWidgetRequest.builder() + .name(WIDGET_NAME) + .description("Async test widget") + .view(UnstableDashboardWidgetView.OBSERVATIONS) + .metrics(List.of(UnstableDashboardWidgetMetric.builder() + .measure("count") + .agg(UnstableDashboardWidgetMetricAggregation.COUNT) + .build())) + .chartType(UnstableDashboardWidgetChartType.NUMBER) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(widget -> { + assertThat(widget.getId()).isNotBlank(); + assertThat(widget.getName()).isEqualTo(WIDGET_NAME); + widgetId = widget.getId(); + }); + } + + @Test + @Order(2) + void getWidget() { + assertThat(client.asyncUnstableDashboardWidgets().unstableDashboardWidgetsGet( + APIUnstableDashboardWidgetsGetRequest.newBuilder() + .widgetId(widgetId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(UnstableDashboardWidget::getId, UnstableDashboardWidget::getName) + .containsExactly(widgetId, WIDGET_NAME); + } + + @Test + @Order(2) + void listWidgets() { + assertThat(client.asyncUnstableDashboardWidgets().unstableDashboardWidgetsList( + APIUnstableDashboardWidgetsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(widgets -> + assertThat(widgets.getData()) + .isNotEmpty() + .anyMatch(w -> WIDGET_NAME.equals(w.getName()))); + } + + @Test + @Order(3) + void updateWidget() { + assertThat(client.asyncUnstableDashboardWidgets().unstableDashboardWidgetsUpdate( + APIUnstableDashboardWidgetsUpdateRequest.newBuilder() + .widgetId(widgetId) + .unstableUpdateDashboardWidgetRequest(UnstableUpdateDashboardWidgetRequest.builder() + .name(WIDGET_NAME + "-updated") + .description("Updated async test widget") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(widget -> { + assertThat(widget.getId()).isEqualTo(widgetId); + assertThat(widget.getName()).isEqualTo(WIDGET_NAME + "-updated"); + }); + } + + @Test + @Order(4) + void deleteWidget() { + assertThat(client.asyncUnstableDashboardWidgets().unstableDashboardWidgetsDelete( + APIUnstableDashboardWidgetsDeleteRequest.newBuilder() + .widgetId(widgetId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson2Test.java new file mode 100644 index 0000000..ad7fa1c --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardWidgetsApiJackson2Test extends UnstableDashboardWidgetsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson3Test.java new file mode 100644 index 0000000..1ad300d --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardWidgetsApiJackson3Test extends UnstableDashboardWidgetsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiTest.java new file mode 100644 index 0000000..e80e797 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardWidgetsApiTest.java @@ -0,0 +1,110 @@ +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.UnstableCreateDashboardWidgetRequest; +import com.langfuse.api.model.UnstableDashboardWidget; +import com.langfuse.api.model.UnstableDashboardWidgetChartType; +import com.langfuse.api.model.UnstableDashboardWidgetMetric; +import com.langfuse.api.model.UnstableDashboardWidgetMetricAggregation; +import com.langfuse.api.model.UnstableDashboardWidgetView; +import com.langfuse.api.model.UnstableUpdateDashboardWidgetRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsCreateRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsDeleteRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsGetRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsListRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsUpdateRequest; + +/** + * Integration tests for the Unstable Dashboard Widgets API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableDashboardWidgetsApiTest extends AbstractLangfuseClientTest { + + private final String WIDGET_NAME = "test-widget-" + UUID.randomUUID().toString().substring(0, 8); + private static String widgetId; + + @Test + @Order(1) + void createWidget() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsCreate( + APIUnstableDashboardWidgetsCreateRequest.newBuilder() + .unstableCreateDashboardWidgetRequest(UnstableCreateDashboardWidgetRequest.builder() + .name(WIDGET_NAME) + .description("Test widget") + .view(UnstableDashboardWidgetView.OBSERVATIONS) + .metrics(List.of(UnstableDashboardWidgetMetric.builder() + .measure("count") + .agg(UnstableDashboardWidgetMetricAggregation.COUNT) + .build())) + .chartType(UnstableDashboardWidgetChartType.NUMBER) + .build()) + .build())) + .satisfies(widget -> { + assertThat(widget.getId()).isNotBlank(); + assertThat(widget.getName()).isEqualTo(WIDGET_NAME); + widgetId = widget.getId(); + }); + } + + @Test + @Order(2) + void getWidget() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsGet( + APIUnstableDashboardWidgetsGetRequest.newBuilder() + .widgetId(widgetId) + .build())) + .extracting(UnstableDashboardWidget::getId, UnstableDashboardWidget::getName) + .containsExactly(widgetId, WIDGET_NAME); + } + + @Test + @Order(2) + void listWidgets() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsList( + APIUnstableDashboardWidgetsListRequest.newBuilder() + .build())) + .satisfies(widgets -> + assertThat(widgets.getData()) + .isNotEmpty() + .anyMatch(w -> WIDGET_NAME.equals(w.getName()))); + } + + @Test + @Order(3) + void updateWidget() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsUpdate( + APIUnstableDashboardWidgetsUpdateRequest.newBuilder() + .widgetId(widgetId) + .unstableUpdateDashboardWidgetRequest(UnstableUpdateDashboardWidgetRequest.builder() + .name(WIDGET_NAME + "-updated") + .description("Updated test widget") + .build()) + .build())) + .satisfies(widget -> { + assertThat(widget.getId()).isEqualTo(widgetId); + assertThat(widget.getName()).isEqualTo(WIDGET_NAME + "-updated"); + }); + } + + @Test + @Order(4) + void deleteWidget() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsDelete( + APIUnstableDashboardWidgetsDeleteRequest.newBuilder() + .widgetId(widgetId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson2Test.java new file mode 100644 index 0000000..67bd8b2 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardsApiAsyncJackson2Test extends UnstableDashboardsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson3Test.java new file mode 100644 index 0000000..2dbef6e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardsApiAsyncJackson3Test extends UnstableDashboardsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncTest.java new file mode 100644 index 0000000..5c67e36 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiAsyncTest.java @@ -0,0 +1,189 @@ +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.UnstableCreateDashboardPlacementRequest; +import com.langfuse.api.model.UnstableCreateDashboardPlacementRequestOneOf; +import com.langfuse.api.model.UnstableCreateDashboardRequest; +import com.langfuse.api.model.UnstableCreateDashboardWidgetRequest; +import com.langfuse.api.model.UnstableDashboard; +import com.langfuse.api.model.UnstableDashboardWidgetChartType; +import com.langfuse.api.model.UnstableDashboardWidgetMetric; +import com.langfuse.api.model.UnstableDashboardWidgetMetricAggregation; +import com.langfuse.api.model.UnstableDashboardWidgetView; +import com.langfuse.api.model.UnstableUpdateDashboardPlacementRequest; +import com.langfuse.api.model.UnstableUpdateDashboardRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsCreateRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsAddPlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsCreateRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsDeletePlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsDeleteRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsGetRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsListRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsUpdatePlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsUpdateRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableDashboardsApiAsyncTest extends AbstractLangfuseClientTest { + + private final String DASHBOARD_NAME = "async-test-dashboard-" + UUID.randomUUID().toString().substring(0, 8); + private static String dashboardId; + private static String widgetId; + private static String placementId; + + @Test + @Order(1) + void createDashboard() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsCreate( + APIUnstableDashboardsCreateRequest.newBuilder() + .unstableCreateDashboardRequest(UnstableCreateDashboardRequest.builder() + .name(DASHBOARD_NAME) + .description("Async test dashboard for integration tests") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(dashboard -> { + assertThat(dashboard.getId()).isNotBlank(); + assertThat(dashboard.getName()).isEqualTo(DASHBOARD_NAME); + dashboardId = dashboard.getId(); + }); + } + + @Test + @Order(1) + void createWidgetForPlacement() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsCreate( + APIUnstableDashboardWidgetsCreateRequest.newBuilder() + .unstableCreateDashboardWidgetRequest(UnstableCreateDashboardWidgetRequest.builder() + .name("async-placement-widget-" + UUID.randomUUID().toString().substring(0, 8)) + .view(UnstableDashboardWidgetView.OBSERVATIONS) + .metrics(List.of(UnstableDashboardWidgetMetric.builder() + .measure("count") + .agg(UnstableDashboardWidgetMetricAggregation.COUNT) + .build())) + .chartType(UnstableDashboardWidgetChartType.NUMBER) + .build()) + .build())) + .satisfies(widget -> { + assertThat(widget.getId()).isNotBlank(); + widgetId = widget.getId(); + }); + } + + @Test + @Order(2) + void getDashboard() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsGet( + APIUnstableDashboardsGetRequest.newBuilder() + .dashboardId(dashboardId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .extracting(UnstableDashboard::getId, UnstableDashboard::getName) + .containsExactly(dashboardId, DASHBOARD_NAME); + } + + @Test + @Order(2) + void listDashboards() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsList( + APIUnstableDashboardsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(dashboards -> + assertThat(dashboards.getData()) + .isNotEmpty() + .anyMatch(d -> DASHBOARD_NAME.equals(d.getName()))); + } + + @Test + @Order(3) + void updateDashboard() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsUpdate( + APIUnstableDashboardsUpdateRequest.newBuilder() + .dashboardId(dashboardId) + .unstableUpdateDashboardRequest(UnstableUpdateDashboardRequest.builder() + .name(DASHBOARD_NAME + "-updated") + .description("Updated async description") + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(dashboard -> { + assertThat(dashboard.getId()).isEqualTo(dashboardId); + assertThat(dashboard.getName()).isEqualTo(DASHBOARD_NAME + "-updated"); + }); + } + + @Test + @Order(4) + void addPlacement() { + var placement = client.unstableDashboards().unstableDashboardsAddPlacement( + APIUnstableDashboardsAddPlacementRequest.newBuilder() + .dashboardId(dashboardId) + .unstableCreateDashboardPlacementRequest( + new UnstableCreateDashboardPlacementRequest( + UnstableCreateDashboardPlacementRequestOneOf.builder() + .widgetId(widgetId) + .type(UnstableCreateDashboardPlacementRequestOneOf.TypeEnum.WIDGET) + .x(0) + .y(0) + .width(6) + .height(6) + .build())) + .build()); + + assertThat(placement).isNotNull(); + placementId = placement.getUnstableDashboardPlacementOneOf().getId(); + assertThat(placementId).isNotBlank(); + } + + @Test + @Order(5) + void updatePlacement() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsUpdatePlacement( + APIUnstableDashboardsUpdatePlacementRequest.newBuilder() + .dashboardId(dashboardId) + .placementId(placementId) + .unstableUpdateDashboardPlacementRequest(UnstableUpdateDashboardPlacementRequest.builder() + .x(2) + .y(2) + .width(8) + .height(4) + .build()) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(6) + void deletePlacement() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsDeletePlacement( + APIUnstableDashboardsDeletePlacementRequest.newBuilder() + .dashboardId(dashboardId) + .placementId(placementId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .isNotNull(); + } + + @Test + @Order(7) + void deleteDashboard() { + assertThat(client.asyncUnstableDashboards().unstableDashboardsDelete( + APIUnstableDashboardsDeleteRequest.newBuilder() + .dashboardId(dashboardId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson2Test.java new file mode 100644 index 0000000..a5282fe --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardsApiJackson2Test extends UnstableDashboardsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson3Test.java new file mode 100644 index 0000000..c940887 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableDashboardsApiJackson3Test extends UnstableDashboardsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiTest.java new file mode 100644 index 0000000..859b7d5 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableDashboardsApiTest.java @@ -0,0 +1,187 @@ +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.UnstableCreateDashboardPlacementRequest; +import com.langfuse.api.model.UnstableCreateDashboardPlacementRequestOneOf; +import com.langfuse.api.model.UnstableCreateDashboardRequest; +import com.langfuse.api.model.UnstableCreateDashboardWidgetRequest; +import com.langfuse.api.model.UnstableDashboard; +import com.langfuse.api.model.UnstableDashboardWidgetChartType; +import com.langfuse.api.model.UnstableDashboardWidgetMetric; +import com.langfuse.api.model.UnstableDashboardWidgetMetricAggregation; +import com.langfuse.api.model.UnstableDashboardWidgetView; +import com.langfuse.api.model.UnstableUpdateDashboardPlacementRequest; +import com.langfuse.api.model.UnstableUpdateDashboardRequest; +import com.langfuse.api.unstableDashboardWidgets.UnstableDashboardWidgetsApi.APIUnstableDashboardWidgetsCreateRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsAddPlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsCreateRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsDeletePlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsDeleteRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsGetRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsListRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsUpdatePlacementRequest; +import com.langfuse.api.unstableDashboards.UnstableDashboardsApi.APIUnstableDashboardsUpdateRequest; + +/** + * Integration tests for the Unstable Dashboards API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableDashboardsApiTest extends AbstractLangfuseClientTest { + + private final String DASHBOARD_NAME = "test-dashboard-" + UUID.randomUUID().toString().substring(0, 8); + private static String dashboardId; + private static String widgetId; + private static String placementId; + + @Test + @Order(1) + void createDashboard() { + assertThat(client.unstableDashboards().unstableDashboardsCreate( + APIUnstableDashboardsCreateRequest.newBuilder() + .unstableCreateDashboardRequest(UnstableCreateDashboardRequest.builder() + .name(DASHBOARD_NAME) + .description("Test dashboard for integration tests") + .build()) + .build())) + .satisfies(dashboard -> { + assertThat(dashboard.getId()).isNotBlank(); + assertThat(dashboard.getName()).isEqualTo(DASHBOARD_NAME); + dashboardId = dashboard.getId(); + }); + } + + @Test + @Order(1) + void createWidgetForPlacement() { + assertThat(client.unstableDashboardWidgets().unstableDashboardWidgetsCreate( + APIUnstableDashboardWidgetsCreateRequest.newBuilder() + .unstableCreateDashboardWidgetRequest(UnstableCreateDashboardWidgetRequest.builder() + .name("placement-widget-" + UUID.randomUUID().toString().substring(0, 8)) + .view(UnstableDashboardWidgetView.OBSERVATIONS) + .metrics(List.of(UnstableDashboardWidgetMetric.builder() + .measure("count") + .agg(UnstableDashboardWidgetMetricAggregation.COUNT) + .build())) + .chartType(UnstableDashboardWidgetChartType.NUMBER) + .build()) + .build())) + .satisfies(widget -> { + assertThat(widget.getId()).isNotBlank(); + widgetId = widget.getId(); + }); + } + + @Test + @Order(2) + void getDashboard() { + assertThat(client.unstableDashboards().unstableDashboardsGet( + APIUnstableDashboardsGetRequest.newBuilder() + .dashboardId(dashboardId) + .build())) + .extracting(UnstableDashboard::getId, UnstableDashboard::getName) + .containsExactly(dashboardId, DASHBOARD_NAME); + } + + @Test + @Order(2) + void listDashboards() { + assertThat(client.unstableDashboards().unstableDashboardsList( + APIUnstableDashboardsListRequest.newBuilder() + .build())) + .satisfies(dashboards -> + assertThat(dashboards.getData()) + .isNotEmpty() + .anyMatch(d -> DASHBOARD_NAME.equals(d.getName()))); + } + + @Test + @Order(3) + void updateDashboard() { + assertThat(client.unstableDashboards().unstableDashboardsUpdate( + APIUnstableDashboardsUpdateRequest.newBuilder() + .dashboardId(dashboardId) + .unstableUpdateDashboardRequest(UnstableUpdateDashboardRequest.builder() + .name(DASHBOARD_NAME + "-updated") + .description("Updated description") + .build()) + .build())) + .satisfies(dashboard -> { + assertThat(dashboard.getId()).isEqualTo(dashboardId); + assertThat(dashboard.getName()).isEqualTo(DASHBOARD_NAME + "-updated"); + }); + } + + @Test + @Order(4) + void addPlacement() { + var placement = client.unstableDashboards().unstableDashboardsAddPlacement( + APIUnstableDashboardsAddPlacementRequest.newBuilder() + .dashboardId(dashboardId) + .unstableCreateDashboardPlacementRequest( + new UnstableCreateDashboardPlacementRequest( + UnstableCreateDashboardPlacementRequestOneOf.builder() + .widgetId(widgetId) + .type(UnstableCreateDashboardPlacementRequestOneOf.TypeEnum.WIDGET) + .x(0) + .y(0) + .width(6) + .height(6) + .build())) + .build()); + + assertThat(placement).isNotNull(); + placementId = placement.getUnstableDashboardPlacementOneOf().getId(); + assertThat(placementId).isNotBlank(); + } + + @Test + @Order(5) + void updatePlacement() { + var updated = client.unstableDashboards().unstableDashboardsUpdatePlacement( + APIUnstableDashboardsUpdatePlacementRequest.newBuilder() + .dashboardId(dashboardId) + .placementId(placementId) + .unstableUpdateDashboardPlacementRequest(UnstableUpdateDashboardPlacementRequest.builder() + .x(2) + .y(2) + .width(8) + .height(4) + .build()) + .build()); + + assertThat(updated).isNotNull(); + } + + @Test + @Order(6) + void deletePlacement() { + assertThat(client.unstableDashboards().unstableDashboardsDeletePlacement( + APIUnstableDashboardsDeletePlacementRequest.newBuilder() + .dashboardId(dashboardId) + .placementId(placementId) + .build())) + .isNotNull(); + } + + @Test + @Order(7) + void deleteDashboard() { + assertThat(client.unstableDashboards().unstableDashboardsDelete( + APIUnstableDashboardsDeleteRequest.newBuilder() + .dashboardId(dashboardId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson2Test.java new file mode 100644 index 0000000..4a76189 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluationRulesApiAsyncJackson2Test extends UnstableEvaluationRulesApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson3Test.java new file mode 100644 index 0000000..cdfb15e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluationRulesApiAsyncJackson3Test extends UnstableEvaluationRulesApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncTest.java new file mode 100644 index 0000000..2dd15dd --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiAsyncTest.java @@ -0,0 +1,74 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.jupiter.api.Disabled; +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.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesDeleteRequest; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesGetRequest; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesListRequest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableEvaluationRulesApiAsyncTest extends AbstractLangfuseClientTest { + + private static String evaluationRuleId; + + @Test + @Order(1) + void listEvaluationRules() { + assertThat(client.asyncUnstableEvaluationRules().unstableEvaluationRulesList( + APIUnstableEvaluationRulesListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + @Disabled("Requires an evaluator to be created first via UnstableEvaluatorsApi") + void unstableEvaluationRulesCreate() { + // Creating an evaluation rule requires an evaluator reference (evaluatorId). + // The create request is a oneOf: unstableCreateLlmAsJudgeEvaluationRuleRequest + // or unstableCreateCodeEvaluationRuleRequest. Both require an existing evaluator. + } + + @Test + @Order(3) + @Disabled("Requires evaluation rule creation in previous step") + void unstableEvaluationRulesUpdate() { + // Updating an evaluation rule requires an existing rule ID. + } + + @Test + @Order(3) + @Disabled("Requires evaluation rule creation in previous step") + void getEvaluationRule() { + assertThat(client.asyncUnstableEvaluationRules().unstableEvaluationRulesGet( + APIUnstableEvaluationRulesGetRequest.newBuilder() + .evaluationRuleId(evaluationRuleId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(rule -> + assertThat(rule.getActualInstance()).isNotNull()); + } + + @Test + @Order(4) + @Disabled("Requires evaluation rule creation in previous step") + void deleteEvaluationRule() { + assertThat(client.asyncUnstableEvaluationRules().unstableEvaluationRulesDelete( + APIUnstableEvaluationRulesDeleteRequest.newBuilder() + .evaluationRuleId(evaluationRuleId) + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson2Test.java new file mode 100644 index 0000000..a26a35e --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluationRulesApiJackson2Test extends UnstableEvaluationRulesApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson3Test.java new file mode 100644 index 0000000..5d54c79 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluationRulesApiJackson3Test extends UnstableEvaluationRulesApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiTest.java new file mode 100644 index 0000000..4f7e036 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluationRulesApiTest.java @@ -0,0 +1,74 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Disabled; +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.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesDeleteRequest; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesGetRequest; +import com.langfuse.api.unstableEvaluationRules.UnstableEvaluationRulesApi.APIUnstableEvaluationRulesListRequest; + +/** + * Integration tests for the Unstable Evaluation Rules API. + * + * @author Eric Deandrea + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +abstract class UnstableEvaluationRulesApiTest extends AbstractLangfuseClientTest { + + private static String evaluationRuleId; + + @Test + @Order(1) + void listEvaluationRules() { + assertThat(client.unstableEvaluationRules().unstableEvaluationRulesList( + APIUnstableEvaluationRulesListRequest.newBuilder() + .build())) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + @Order(2) + @Disabled("Requires an evaluator to be created first via UnstableEvaluatorsApi") + void unstableEvaluationRulesCreate() { + // Creating an evaluation rule requires an evaluator reference (evaluatorId). + // The create request is a oneOf: unstableCreateLlmAsJudgeEvaluationRuleRequest + // or unstableCreateCodeEvaluationRuleRequest. Both require an existing evaluator. + } + + @Test + @Order(3) + @Disabled("Requires evaluation rule creation in previous step") + void unstableEvaluationRulesUpdate() { + // Updating an evaluation rule requires an existing rule ID. + } + + @Test + @Order(3) + @Disabled("Requires evaluation rule creation in previous step") + void getEvaluationRule() { + assertThat(client.unstableEvaluationRules().unstableEvaluationRulesGet( + APIUnstableEvaluationRulesGetRequest.newBuilder() + .evaluationRuleId(evaluationRuleId) + .build())) + .satisfies(rule -> + assertThat(rule.getActualInstance()).isNotNull()); + } + + @Test + @Order(4) + @Disabled("Requires evaluation rule creation in previous step") + void deleteEvaluationRule() { + assertThat(client.unstableEvaluationRules().unstableEvaluationRulesDelete( + APIUnstableEvaluationRulesDeleteRequest.newBuilder() + .evaluationRuleId(evaluationRuleId) + .build())) + .satisfies(response -> + assertThat(response.getMessage()).isNotBlank()); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson2Test.java new file mode 100644 index 0000000..5a57922 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluatorsApiAsyncJackson2Test extends UnstableEvaluatorsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson3Test.java new file mode 100644 index 0000000..a19154f --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluatorsApiAsyncJackson3Test extends UnstableEvaluatorsApiAsyncTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncTest.java new file mode 100644 index 0000000..b6f4505 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiAsyncTest.java @@ -0,0 +1,52 @@ +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.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.model.UnstableCodeEvaluatorSourceCodeLanguage; +import com.langfuse.api.model.UnstableCreateEvaluatorRequest; +import com.langfuse.api.model.UnstableCreateEvaluatorRequestOneOf1; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi.APIUnstableEvaluatorsCreateRequest; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi.APIUnstableEvaluatorsListRequest; + +/** + * Async integration tests for the Unstable Evaluators API. + * + * @author Eric Deandrea + */ +abstract class UnstableEvaluatorsApiAsyncTest extends AbstractLangfuseClientTest { + + @Test + void listEvaluators() { + assertThat(client.asyncUnstableEvaluators().unstableEvaluatorsList( + APIUnstableEvaluatorsListRequest.newBuilder() + .build())) + .succeedsWithin(Duration.ofSeconds(5)) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + void createCodeEvaluatorRequiresEnterprisePlan() { + var createRequest = new UnstableCreateEvaluatorRequest( + UnstableCreateEvaluatorRequestOneOf1.builder() + .name("async-test-evaluator-" + UUID.randomUUID().toString().substring(0, 8)) + .sourceCode("def evaluate(output, expected_output, input, metadata):\n return 1.0") + .sourceCodeLanguage(UnstableCodeEvaluatorSourceCodeLanguage.PYTHON) + .type(UnstableCreateEvaluatorRequestOneOf1.TypeEnum.CODE) + .build()); + + assertThat(client.asyncUnstableEvaluators().unstableEvaluatorsCreate( + APIUnstableEvaluatorsCreateRequest.newBuilder() + .unstableCreateEvaluatorRequest(createRequest) + .build())) + .failsWithin(Duration.ofSeconds(5)) + .withThrowableThat() + .withCauseInstanceOf(LangfuseApiException.class); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson2Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson2Test.java new file mode 100644 index 0000000..73c6745 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson2Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluatorsApiJackson2Test extends UnstableEvaluatorsApiTest { + + @Override + LangfuseApi createClient() { + return jackson2Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson3Test.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson3Test.java new file mode 100644 index 0000000..1fddb69 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiJackson3Test.java @@ -0,0 +1,11 @@ +package com.langfuse.client; + +import com.langfuse.api.LangfuseApi; + +class UnstableEvaluatorsApiJackson3Test extends UnstableEvaluatorsApiTest { + + @Override + LangfuseApi createClient() { + return jackson3Client(); + } +} diff --git a/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiTest.java b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiTest.java new file mode 100644 index 0000000..bd69b33 --- /dev/null +++ b/langfuse-java-client/src/test/java/com/langfuse/client/UnstableEvaluatorsApiTest.java @@ -0,0 +1,49 @@ +package com.langfuse.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.langfuse.api.LangfuseApiException; +import com.langfuse.api.model.UnstableCodeEvaluatorSourceCodeLanguage; +import com.langfuse.api.model.UnstableCreateEvaluatorRequest; +import com.langfuse.api.model.UnstableCreateEvaluatorRequestOneOf1; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi.APIUnstableEvaluatorsCreateRequest; +import com.langfuse.api.unstableEvaluators.UnstableEvaluatorsApi.APIUnstableEvaluatorsListRequest; + +/** + * Integration tests for the Unstable Evaluators API. + * + * @author Eric Deandrea + */ +abstract class UnstableEvaluatorsApiTest extends AbstractLangfuseClientTest { + + @Test + void listEvaluators() { + assertThat(client.unstableEvaluators().unstableEvaluatorsList( + APIUnstableEvaluatorsListRequest.newBuilder() + .build())) + .satisfies(response -> + assertThat(response.getData()).isNotNull()); + } + + @Test + void createCodeEvaluatorRequiresEnterprisePlan() { + var createRequest = new UnstableCreateEvaluatorRequest( + UnstableCreateEvaluatorRequestOneOf1.builder() + .name("test-evaluator-" + UUID.randomUUID().toString().substring(0, 8)) + .sourceCode("def evaluate(output, expected_output, input, metadata):\n return 1.0") + .sourceCodeLanguage(UnstableCodeEvaluatorSourceCodeLanguage.PYTHON) + .type(UnstableCreateEvaluatorRequestOneOf1.TypeEnum.CODE) + .build()); + + assertThatThrownBy(() -> client.unstableEvaluators().unstableEvaluatorsCreate( + APIUnstableEvaluatorsCreateRequest.newBuilder() + .unstableCreateEvaluatorRequest(createRequest) + .build())) + .isInstanceOf(LangfuseApiException.class); + } +} 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..898bd70 --- /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:25.12` | Analytics data store | +| Redis | `redis:7` | Queue and cache | +| MinIO | `cgr.dev/chainguard/minio` | S3-compatible object storage | +| Langfuse Web | `langfuse/langfuse:4` | API server | +| Langfuse Worker | `langfuse/langfuse-worker:4` | 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:4") + .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:4` | +| 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:25.12`, 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..ef0fb5f --- /dev/null +++ b/langfuse-java-testcontainers/src/main/java/com/langfuse/testcontainers/LangfuseContainer.java @@ -0,0 +1,361 @@ +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( + DockerImageName.parse(chConfig.image()) + .asCompatibleSubstituteFor("clickhouse/clickhouse-server")) + .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"); + withEnv("LANGFUSE_OBSERVATION_FIELD_OVERFLOW_ENABLED", "false"); + withEnv("LANGFUSE_OBSERVATION_FIELD_SIZE_LIMIT_BYTES", "2097152"); + withEnv("LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK", "false"); + 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_MIGRATION_V4_WRITE_MODE", "events_only"); + + 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"); + worker.withEnv("LANGFUSE_OBSERVATION_FIELD_OVERFLOW_ENABLED", "false"); + worker.withEnv("LANGFUSE_OBSERVATION_FIELD_SIZE_LIMIT_BYTES", "2097152"); + worker.withEnv("LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK", "false"); + 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"); + worker.withEnv("LANGFUSE_MIGRATION_V4_WRITE_MODE", "events_only"); + } + + 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..63da662 --- /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 = "docker.io/clickhouse/clickhouse-server:25.12"; + + /** + * @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..a1d8375 --- /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:4"; + + /** + * 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(5); + + /** + * @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..7b7327e --- /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:4"; + + /** + * @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..347afcb --- /dev/null +++ b/openapi.yml @@ -0,0 +1,16768 @@ +openapi: 3.0.1 +info: + title: server + 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: &ref_0 + - 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + /api/public/experiments: + get: + description: |- + List experiments with cursor-based pagination. Results are ordered by + latest experiment activity descending. + operationId: experiments_list + tags: + - Experiments + parameters: + - name: fields + in: query + description: |- + Comma-separated list of field groups to include. Available groups: + `core`, `metadata`, `scores`. If omitted, `core` is returned. + required: false + schema: + type: string + nullable: true + - name: limit + in: query + description: Number of experiments to return per page. Maximum 100, default 50. + required: false + schema: + type: integer + nullable: true + - name: scoreLimit + in: query + description: >- + Number of scores to return per experiment when `fields=scores` is + requested. Maximum 50, default 50. + required: false + schema: + type: integer + nullable: true + - name: cursor + in: query + description: Versioned base64url cursor from the previous response page. + required: false + schema: + type: string + nullable: true + - name: fromStartTime + in: query + description: Retrieve only experiments on or after this datetime. + required: true + schema: + type: string + format: date-time + - name: toStartTime + in: query + description: Retrieve only experiments before this datetime. + required: false + schema: + type: string + format: date-time + nullable: true + - name: id + in: query + description: Comma-separated list of experiment IDs. + required: false + schema: + type: string + nullable: true + - name: name + in: query + description: Comma-separated list of experiment names. + required: false + schema: + type: string + nullable: true + - name: datasetId + in: query + description: Comma-separated list of dataset IDs. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: |- + JSON string containing an array of structured filter conditions. + Supported columns are `id`, `name`, and `datasetId`. + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ExperimentsResponse' + '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: *ref_0 + /api/public/experiment-items: + get: + description: |- + List experiment items with cursor-based pagination. Use this endpoint + to export experiment item inputs, outputs, expected outputs, metadata, + and optionally item/trace scores. Results are ordered by time + descending. + operationId: experiments_listItems + tags: + - Experiments + parameters: + - name: fields + in: query + description: |- + Comma-separated list of field groups to include. Available groups: + `core`, `dataset`, `io`, `metadata`, `itemMetadata`, + `experimentMetadata`, `scores`. If omitted, `core,dataset` is + returned. + required: false + schema: + type: string + nullable: true + - name: limit + in: query + description: >- + Number of experiment items to return per page. Maximum 100, default + 50. + required: false + schema: + type: integer + nullable: true + - name: scoreLimit + in: query + description: >- + Number of scores to return per experiment item when `fields=scores` + is requested. Maximum 50, default 50. + required: false + schema: + type: integer + nullable: true + - name: cursor + in: query + description: Versioned base64url cursor from the previous response page. + required: false + schema: + type: string + nullable: true + - name: fromStartTime + in: query + description: Retrieve only experiment items started on or after this datetime. + required: true + schema: + type: string + format: date-time + - name: toStartTime + in: query + description: Retrieve only experiment items started before this datetime. + required: false + schema: + type: string + format: date-time + nullable: true + - name: experimentId + in: query + description: Comma-separated list of experiment IDs. + required: false + schema: + type: string + nullable: true + - name: experimentName + in: query + description: Comma-separated list of experiment names. + required: false + schema: + type: string + nullable: true + - name: experimentItemId + in: query + description: Comma-separated list of experiment item IDs. + required: false + schema: + type: string + nullable: true + - name: datasetId + in: query + description: Comma-separated list of dataset IDs. + required: false + schema: + type: string + nullable: true + - name: filter + in: query + description: |- + JSON string containing an array of structured filter conditions. + Supported columns are `experimentId`, `experimentName`, + `experimentItemId`, and `datasetId`. + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ExperimentItemsResponse' + '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: *ref_0 + /api/public/feedback: + post: + description: >- + Submit explicit user-approved feedback about Langfuse skills, MCP tools, + CLI, docs, or public API. Do not include secrets, credentials, customer + data, trace payloads, or unrelated use-case details. + operationId: feedback_submit + tags: + - Feedback + parameters: [] + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitFeedbackResponse' + '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: '' + security: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitFeedbackRequest' + /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' + '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: *ref_0 + 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 + /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-boolean", "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: *ref_0 + /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/ObservationsViewSingle' + '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: *ref_0 + /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: *ref_0 + /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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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`, `scores-boolean`, and + `scores-categorical` views only (traces view not supported) + + - Direct access to tags and release fields on observations + + - Semantic-root filtering and grouping through the v2-only + `isRootObservation` dimension + + - 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 + + - `isRootObservation` - Boolean semantic-root status. `true` includes + physical roots and app roots whose SDK parent is external (so + `parentObservationId` may be non-null). + + - `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-boolean + + Query boolean score data. It has the same score and parent + trace/observation dimensions as scores-numeric, plus: + + + **Dimensions:** + + - `booleanValue` - Boolean value for true/false grouping and filtering + + + **Measures:** + + - `count` - Total number of boolean scores + + - `value` - Numeric 0/1 score value; `avg` returns the true-rate + + + ### 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-boolean / 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-boolean", "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 + } + } + + ``` + + + For example, to count semantic roots (including app roots with a + non-null external parent), use a boolean filter: + + ```json + + { + "view": "observations", + "metrics": [{"measure": "count", "aggregation": "count"}], + "filters": [{"column": "isRootObservation", "operator": "=", "value": true, "type": "boolean"}], + "fromTimestamp": "2025-01-01T00:00:00.000Z", + "toTimestamp": "2025-02-01T00:00:00.000Z" + } + + ``` + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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, isRootObservation + + - `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: sessionId + in: query + description: Filter by session ID. + 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 + description: >- + Filter by the physical parent observation ID. + + An empty value matches only observations without a physical parent. + Use `isRootObservation` to include observations marked as app roots + by the SDK, which may retain a non-null `parentObservationId`. + required: false + schema: + type: string + nullable: true + - name: isRootObservation + in: query + description: >- + Filter by whether an observation is a logical root. + + Root observations include observations without a physical parent and + observations marked as app roots by the SDK. + + An app-root observation may have `isRootObservation=true` and a + non-null `parentObservationId`. + required: false + schema: + type: boolean + 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", "matches" + // - 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", "matches" + // - 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 + + - `isRootObservation` (boolean) - Whether the observation is a + logical root. Observations marked as app roots by the SDK may retain + a non-null parentObservationId. + + + ### 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 + + - `input` (string) - Observation input. Supports accelerated indexed + literal search with the `matches` operator. + + - `output` (string) - Observation output. Supports accelerated + indexed literal search with the `matches` operator. + + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata + key-value pairs. Use `key` parameter to filter on specific metadata + keys. + + + The `matches` operator is only supported for `input`, `output`, and + stringObject `metadata` filters. It performs indexed literal search + with token-boundary pruning using the events table text indexes. + Case sensitivity differs by target: `input` and `output` matches are + case-insensitive, while metadata value matches are case-sensitive. + Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. + Use `contains` for legacy substring semantics where the API allows + it. Any v2 `input` or `output` filter must be accompanied by at + least one `=` or `matches` filter on `input` or `output`; standalone + `contains`, `starts with`, `ends with`, and `does not contain` + filters on these columns are rejected. + + + ## 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" + }, + { + "type": "string", + "column": "output", + "operator": "matches", + "value": "needle" + }, + { + "type": "boolean", + "column": "isRootObservation", + "operator": "=", + "value": true + } + ] + + ``` + 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: *ref_0 + /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' + '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: *ref_0 + 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 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + /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: *ref_0 + /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: *ref_0 + 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: *ref_0 + 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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScoreConfigRequest' + /api/public/v3/scores: + get: + description: |- + Get a list of scores with a polymorphic `value` field (v3). + + The `value` field type depends on `dataType`: + - `NUMERIC` β†’ number + - `BOOLEAN` β†’ boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` β†’ string + + The response always includes the core fields: id, projectId, name, + value, dataType, source, timestamp, environment, createdAt, updatedAt. + + Additional field groups can be requested via the `fields` parameter: + - `details` β€” adds comment, configId, metadata + - `subject` β€” adds the subject object describing the entity the score + is attached to: kind (trace, observation, session, or experiment), + id, and traceId for observation-level scores + - `annotation` β€” adds authorUserId, queueId + + Unknown group names return HTTP 400. + operationId: scoresV3_getManyV3 + tags: + - ScoresV3 + parameters: + - name: limit + in: query + description: >- + Number of items per page. Maximum 100, default 50. Requests with a + limit greater than 100 return HTTP 400. + required: false + schema: + type: integer + nullable: true + - name: cursor + in: query + description: >- + URL-safe base64 (base64url) cursor for pagination. Use the cursor + from the previous response to get the next page. Absent on the final + page. + required: false + schema: + type: string + nullable: true + - name: fields + in: query + description: >- + Comma-separated field groups to include in addition to the + always-returned core fields. Allowed: details, subject, annotation β€” + see the endpoint description for the fields each group adds. Unknown + names return HTTP 400. + required: false + schema: + type: string + nullable: true + - name: id + in: query + description: >- + Comma-separated list of score IDs to filter by (OR within, AND + across filters). + required: false + schema: + type: string + nullable: true + - name: name + in: query + description: Comma-separated list of score names to filter by. + required: false + schema: + type: string + nullable: true + - name: source + in: query + description: >- + Comma-separated list of score sources to filter by (e.g. API, + ANNOTATION, EVAL). Case-insensitive β€” `api` and `API` are + equivalent. + required: false + schema: + type: string + nullable: true + - name: dataType + in: query + description: >- + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, + CATEGORICAL, TEXT, CORRECTION). Case-insensitive β€” `numeric` and + `NUMERIC` are equivalent. Must be a single value when used with + value, valueMin, or valueMax; otherwise the request returns HTTP + 400. Must be NUMERIC when used with valueMin or valueMax. + required: false + schema: + type: string + nullable: true + - name: environment + in: query + description: Comma-separated list of environments to filter by. + required: false + schema: + type: string + nullable: true + - name: configId + in: query + description: Comma-separated list of score config IDs to filter by. + required: false + schema: + type: string + nullable: true + - name: queueId + in: query + description: Comma-separated list of annotation queue IDs to filter by. + required: false + schema: + type: string + nullable: true + - name: authorUserId + in: query + description: Comma-separated list of author user IDs to filter by. + required: false + schema: + type: string + nullable: true + - name: value + in: query + description: >- + Comma-separated list of exact values to filter by. Requires a single + dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, + multiple dataTypes, or omitting dataType returns HTTP 400. For + BOOLEAN, each value must be "true" or "false"; for NUMERIC, each + value must be a finite number. Otherwise the request returns HTTP + 400. + required: false + schema: + type: string + nullable: true + - name: valueMin + in: query + description: >- + Inclusive lower bound on the numeric value. Requires + dataType=NUMERIC as a single value; otherwise the request returns + HTTP 400. + required: false + schema: + type: number + format: double + nullable: true + - name: valueMax + in: query + description: >- + Inclusive upper bound on the numeric value. Requires + dataType=NUMERIC as a single value; otherwise the request returns + HTTP 400. + required: false + schema: + type: number + format: double + nullable: true + - name: traceId + in: query + description: >- + Comma-separated list of trace IDs to filter by. Mutually exclusive + with sessionId, experimentId. May be combined with observationId to + scope the observation lookup to a specific trace. + required: false + schema: + type: string + nullable: true + - name: sessionId + in: query + description: >- + Comma-separated list of session IDs to filter by. Mutually exclusive + with traceId, observationId, experimentId. + required: false + schema: + type: string + nullable: true + - name: observationId + in: query + description: >- + Comma-separated list of observation IDs to filter by. Requires + traceId to be specified, because observation IDs are scoped to a + trace. Mutually exclusive with sessionId, experimentId. Returns HTTP + 400 when used without traceId. + required: false + schema: + type: string + nullable: true + - name: experimentId + in: query + description: >- + Comma-separated list of dataset run IDs (experiment IDs) to filter + by. Mutually exclusive with traceId, sessionId, observationId. + required: false + schema: + type: string + nullable: true + - name: fromTimestamp + in: query + description: Inclusive lower bound on the score timestamp. + required: false + schema: + type: string + format: date-time + nullable: true + - name: toTimestamp + in: query + description: Exclusive upper bound on the score timestamp. + required: false + schema: + type: string + format: date-time + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/GetScoresV3Response' + '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: *ref_0 + /api/public/scores: + post: + description: >- + Create a score (supports trace, observation, session, and dataset run + scores) + operationId: scores_create + tags: + - Scores + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScoreResponse' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScoreRequest' + /api/public/v2/scores: + get: + description: |- + **Deprecated.** Use `GET /api/public/v3/scores` instead. This endpoint + is no longer available on Langfuse v4 and later. + + 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: *ref_0 + /api/public/v2/scores/{scoreId}: + get: + description: |- + **Deprecated.** Use `GET /api/public/v3/scores` with the `id` filter + instead. This endpoint is no longer available on Langfuse v4 and later. + + 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: *ref_0 + /api/public/sessions: + get: + description: >- + Get sessions. + + + This legacy endpoint is not recommended for new data extraction + workflows. + + Use the v2 observations endpoint with a bounded time range and group + rows by + + `sessionId` instead: + + `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. + 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: *ref_0 + /api/public/sessions/{sessionId}: + get: + description: >- + Get a session. + + + Please note that `traces` on this endpoint are not paginated. For large + + sessions or new data extraction workflows, use the v2 observations + endpoint + + with a URL-encoded `sessionId` filter and a bounded time range: + + `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. + 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: *ref_0 + /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: *ref_0 + 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: *ref_0 + /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", "booleanObject", "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: "=", ">", "<", ">=", "<=" + // - booleanObject: "=", "<>" + // - 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, booleanObject, and categoryOptions types when filtering on nested fields like metadata or score names + } + ] + + ``` + + + ## 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 + + - `score_booleans` (booleanObject) - Boolean score values. Use `key` + for the score name and a boolean `value`, e.g. `{"type": + "booleanObject", "column": "score_booleans", "key": "is_correct", + "operator": "=", "value": true}`. The `<>` operator also matches + traces without a score of that name. + + + ## 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: *ref_0 + 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: *ref_0 + 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/dashboard-widgets: + get: + description: |- + List dashboard widgets in the project, ordered by most recently + updated first. + + Responses may include legacy `traces` widgets created before this + API existed. New widgets cannot be created with `view: traces`. + operationId: unstable_dashboardWidgets_list + tags: + - UnstableDashboardWidgets + 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/unstableDashboardWidgetList' + '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: *ref_0 + post: + description: >- + Create a dashboard widget (a standalone chart definition you place on + + any dashboard). + + + This endpoint creates the widget only; place it on a dashboard via + + `POST /dashboards/{dashboardId}/placements`. + + + Supported views are `observations`, `scores-numeric`, `scores-boolean`, + and `scores-categorical`. + + The legacy `traces` view is not supported by this unstable API. + + Widgets are created as v2 internally. + + + `chartConfig` is optional and defaults to the plain config for + + `chartType`; when `chartConfig.type` is given it must match + + `chartType`. + + + Unstable API note: + + - This surface may evolve while dashboard/widget APIs are being + finalized. + operationId: unstable_dashboardWidgets_create + tags: + - UnstableDashboardWidgets + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboardWidget' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateDashboardWidgetRequest' + /api/public/unstable/dashboard-widgets/{widgetId}: + get: + description: |- + Get a dashboard widget by id. + + The response may use `view: traces` for legacy widgets. + operationId: unstable_dashboardWidgets_get + tags: + - UnstableDashboardWidgets + parameters: + - name: widgetId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboardWidget' + '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: *ref_0 + patch: + description: |- + Update a dashboard widget. + + All fields are optional; at least one field is required. + Changing `chartType` without sending `chartConfig` resets the config + to the new chart type's defaults. When `chartConfig.type` is given + it must match the widget's (possibly updated) `chartType`. + + `view` cannot be changed to the legacy `traces` value. Existing + `traces` widgets may be updated on other fields. + operationId: unstable_dashboardWidgets_update + tags: + - UnstableDashboardWidgets + parameters: + - name: widgetId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboardWidget' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableUpdateDashboardWidgetRequest' + delete: + description: |- + Delete a dashboard widget. + + The API returns `409` while the widget is still placed on a dashboard. + Remove those placements first. + operationId: unstable_dashboardWidgets_delete + tags: + - UnstableDashboardWidgets + parameters: + - name: widgetId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDeleteDashboardWidgetResponse' + '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' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: *ref_0 + /api/public/unstable/dashboards: + get: + description: |- + List dashboards in the project, ordered by most recently updated + first. + operationId: unstable_dashboards_list + tags: + - UnstableDashboards + 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/unstableDashboardList' + '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: *ref_0 + post: + description: Create a dashboard. + operationId: unstable_dashboards_create + tags: + - UnstableDashboards + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboard' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateDashboardRequest' + /api/public/unstable/dashboards/{dashboardId}: + get: + description: Get a dashboard by id. + operationId: unstable_dashboards_get + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboard' + '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: *ref_0 + patch: + description: Update a dashboard's name, description, definition, or filters. + operationId: unstable_dashboards_update + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboard' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableUpdateDashboardRequest' + delete: + description: Delete a dashboard. + operationId: unstable_dashboards_delete + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDeleteDashboardResponse' + '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: *ref_0 + /api/public/unstable/dashboards/{dashboardId}/placements: + post: + description: |- + Add a placement to a dashboard grid (see `DashboardPlacement` for + grid semantics). + + `id` and the position fields are optional: when omitted, the + placement gets a server-generated id and is appended below all + existing tiles as a 6x6 tile. Returns the created placement. + + The referenced widget must exist in the same project or be a + Langfuse-managed widget. The API returns `409` if a placement with + the same `id` already exists on the dashboard. + operationId: unstable_dashboards_addPlacement + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboardPlacement' + '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' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateDashboardPlacementRequest' + /api/public/unstable/dashboards/{dashboardId}/placements/{placementId}: + patch: + description: |- + Move or resize a placement. All fields are optional; at least one is + required. Omitted fields keep their current value. The placement's + content (widget/preset reference) and id cannot change β€” delete and + re-add the placement to swap content. Returns the updated placement. + operationId: unstable_dashboards_updatePlacement + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + - name: placementId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDashboardPlacement' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableUpdateDashboardPlacementRequest' + delete: + description: >- + Remove a placement from a dashboard grid without deleting the referenced + widget. + operationId: unstable_dashboards_deletePlacement + tags: + - UnstableDashboards + parameters: + - name: dashboardId + in: path + required: true + schema: + type: string + - name: placementId + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstableDeleteDashboardPlacementResponse' + '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: *ref_0 + /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` + + - for `llm_as_judge` evaluators, every evaluator prompt variable must be + mapped exactly once + + - for `code` evaluators, Langfuse uses the fixed code runtime mapping; + omit `mapping` in create and update requests + + - for user-provided `llm_as_judge` mappings, `expected_output` and + `experiment_item_metadata` 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`: for `llm_as_judge` evaluators, 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`: for `llm_as_judge`, 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' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateEvaluationRuleRequest' + get: + description: >- + List evaluation rules in the authenticated project. + + + This includes legacy `trace` and `dataset` rules so they can be + inspected and migrated to v4 rules. Legacy rules are read-only through + this API; create, update, and delete continue to support only + `observation` and `experiment` rules. + 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: *ref_0 + /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, execution timing, and effective runtime status. Legacy `trace` + and `dataset` rules are returned for migration and are read-only through + this API. + 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/unstableReadableEvaluationRule' + '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: *ref_0 + patch: + description: >- + Update an evaluation rule. + + + Typical uses: + + - enable or disable live execution + + - switch to another evaluator + + - adjust sampling + + - change filters + + - update LLM-as-judge 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 an LLM-as-judge `mapping` must still + produce a valid target-specific configuration + + - if you change `target` for an LLM-as-judge rule, also send a + compatible `filter` and `mapping` in the same request unless the + existing ones are still valid for the new target + + - for `code` evaluator rules, omit `mapping`; Langfuse stores the fixed + code runtime mapping automatically + + - 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 an LLM-as-judge 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: *ref_0 + 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: *ref_0 + /api/public/unstable/evaluators: + post: + description: >- + Create an evaluator in the authenticated project. + + + Use evaluators to define **how** Langfuse should score data. + + LLM-as-a-judge evaluators define a prompt, expected structured output, + and optional model configuration. + + Code evaluators define source code and a runtime language. + + + 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`. + + + Code evaluator validation: + + - At creation, Langfuse only validates the request shape + + - The `sourceCode` itself is not executed here. It is first run + (preflight-tested against a sample observation) when you link the + evaluator to an evaluation rule, so runtime errors in the code surface + at evaluation-rule creation, not at evaluator creation. + + + 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`: for + `type=llm_as_judge`, send `dataType`, `reasoning.description`, and + `score.description`. Do not send `version`; it is not part of the public + request shape. + + - If `type` is omitted, Langfuse treats the request as + `type=llm_as_judge` for backwards compatibility. New clients should send + `type` explicitly. + + + 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' + '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: *ref_0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/unstableCreateEvaluatorRequest' + 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: *ref_0 + /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: *ref_0 + delete: + description: >- + Delete an evaluator. + + + Important behavior: + + - This deletes the evaluator including all of its stored versions; + `evaluatorId` may reference any version. + + - The API returns `409` while evaluation rules still reference the + evaluator. Delete those evaluation rules first. + + - Langfuse-managed evaluators (`scope=managed`) cannot be deleted; the + API returns `403`. + + - Scores already produced by the evaluator are not deleted. + operationId: unstable_evaluators_delete + 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/unstableDeleteEvaluatorResponse' + '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' + '429': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/unstablePublicApiError' + security: *ref_0 +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 + - description + - 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 + - PARQUET + description: >- + File format for exported data. `PARQUET` is a columnar binary format + encoded and compressed by the storage engine; gzip compression does not + apply to it. Note that the model-price columns (`input_price`, + `output_price`, `total_price`) are not included in Parquet observation + exports. + BlobStorageIntegrationFileTypeResponse: + title: BlobStorageIntegrationFileTypeResponse + type: string + enum: + - JSON + - CSV + - JSONL + - PARQUET + description: File type reported for an existing integration. + 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. + Observation columns are controlled by `exportFieldGroups`; field groups + without a counterpart in this data model (e.g. `trace_context`) are + omitted. + + - `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. Observation + columns of both portions 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 selecting which observation columns are included in the + export. Applies to all export sources; groups without a counterpart in + the legacy data model (e.g. `trace_context`) are omitted from the legacy + 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). Must not be in the future (27 h tolerance for + timezone differences). + 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: integrations on Langfuse Cloud + default to `OBSERVATIONS_V2`; self-hosted deployments fall back to + `LEGACY_TRACES_OBSERVATIONS`. Required when `exportFieldGroups` is + provided. + + + **Cloud-only project 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. Use `OBSERVATIONS_V2` for all new integrations. Self-hosted + deployments are unaffected. + + + **Cloud-only integration deprecation gate (effective 2026-06-22):** + On Langfuse Cloud, legacy export sources are only accepted for blob + storage integrations created before 2026-06-22, regardless of + project age. Requests that would create a new integration with + `LEGACY_TRACES_OBSERVATIONS` or + `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP + 400. Use `OBSERVATIONS_V2` instead. Self-hosted deployments are + unaffected. + exportFieldGroups: + type: array + items: + $ref: '#/components/schemas/BlobStorageExportFieldGroup' + nullable: true + description: >- + Field groups to include in each exported observation row. Applies to + all export sources; must include `core` if provided. When omitted on + create, the column default (all groups) applies. When omitted on + update, the existing value is preserved. + + + `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/BlobStorageIntegrationFileTypeResponse' + 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 observation row. An empty + list is treated as all groups during export. + 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 + - endpoint + - region + - accessKeyId + - prefix + - exportFrequency + - enabled + - forcePathStyle + - fileType + - exportMode + - exportStartDate + - compressed + - exportSource + - exportFieldGroups + - nextSyncAt + - lastSyncAt + - lastError + - lastErrorAt + - 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 + - running + - 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) + + - `running` β€” an export job is currently being processed + + - `queued` β€” next export is overdue (`nextSyncAt` is in the past) and + waiting to be picked up by the worker + + - `idle` β€” enabled but has never exported yet and no export is queued + + - `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 + - lastSyncAt + - nextSyncAt + - lastError + - lastErrorAt + 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. Must be a member of the + organization that owns the project, otherwise an error will be + thrown. + 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 + Deprecation: + title: Deprecation + type: object + description: >- + Migration signal returned by deprecated endpoints. Optional fields are + omitted when they have no value. + properties: + message: + type: string + description: >- + Human- and agent-readable summary of the deprecation and its + replacement. + replacement: + type: string + nullable: true + description: >- + The replacement endpoint, e.g. "GET /api/public/v2/observations". + Omitted when the endpoint is being removed without a direct + replacement. + docsUrl: + type: string + nullable: true + description: Link to the migration documentation (markdown), when available. + sunsetAt: + type: string + nullable: true + description: >- + ISO date after which the endpoint may stop working, when a removal + date is committed. + required: + - message + 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 + - name + - sessionId + - release + - version + - userId + - 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 + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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 + - traceId + - type + - name + - startTime + - endTime + - completionStartTime + - model + - modelParameters + - input + - version + - metadata + - output + - usage + - level + - statusMessage + - parentObservationId + - promptId + - 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 + required: + - promptName + - promptVersion + - modelId + - inputPrice + - outputPrice + - totalPrice + - calculatedInputCost + - calculatedOutputCost + - calculatedTotalCost + - latency + - timeToFirstToken + allOf: + - $ref: '#/components/schemas/Observation' + ObservationsViewSingle: + title: ObservationsViewSingle + type: object + properties: + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + allOf: + - $ref: '#/components/schemas/ObservationsView' + 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 physical parent observation ID, if present. + + Observations marked as app roots by the SDK may retain a non-null + parent ID. + type: + type: string + description: The type of the observation (e.g. GENERATION, SPAN, EVENT) + isRootObservation: + type: boolean + nullable: true + description: >- + Whether this observation is a logical root. + + This is true for observations without a physical parent and + observations marked as app roots by the SDK. + 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 + - traceId + - startTime + - endTime + - projectId + - parentObservationId + - type + - modelId + - inputPrice + - outputPrice + - totalPrice + 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 + - unit + 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 + - authorUserId + - comment + - metadata + - configId + - queueId + - 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 + - authorUserId + - comment + - metadata + - configId + - queueId + - 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 + properties: + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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 + - description + - metadata + - inputSchema + - expectedOutputSchema + - 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 + mediaReferences: + type: array + items: + $ref: '#/components/schemas/DatasetItemMediaReference' + description: >- + Resolved Langfuse media references found in input, expectedOutput, + and metadata. + required: + - id + - status + - input + - expectedOutput + - metadata + - sourceTraceId + - sourceObservationId + - datasetId + - datasetName + - createdAt + - updatedAt + - mediaReferences + DatasetItemMediaReference: + title: DatasetItemMediaReference + type: object + properties: + field: + $ref: '#/components/schemas/DatasetItemMediaReferenceField' + description: The dataset item field containing the reference + referenceString: + type: string + description: >- + The Langfuse media reference string, e.g. + `@@@langfuseMedia:type=image/png|id=...|source=bytes@@@` + jsonPath: + type: string + description: >- + JSONPath of the string holding the reference within the field, e.g. + `$['image']` + media: + $ref: '#/components/schemas/DatasetItemMediaReferenceMedia' + description: The resolved media record. + required: + - field + - referenceString + - jsonPath + - media + DatasetItemMediaReferenceField: + title: DatasetItemMediaReferenceField + type: string + enum: + - input + - expectedOutput + - metadata + DatasetItemMediaReferenceMedia: + title: DatasetItemMediaReferenceMedia + type: object + properties: + mediaId: + type: string + description: The unique langfuse identifier of the 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 + url: + type: string + description: The signed download URL of the media record + urlExpiry: + type: string + description: The expiry date and time of the download URL + required: + - mediaId + - contentType + - contentLength + - url + - urlExpiry + 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 + - observationId + - 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 + - description + - metadata + - datasetId + - datasetName + - createdAt + - updatedAt + DatasetRunWithItems: + title: DatasetRunWithItems + type: object + properties: + datasetRunItems: + type: array + items: + $ref: '#/components/schemas/DatasetRunItem' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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 + - startDate + - inputPrice + - outputPrice + - totalPrice + - tokenizerId + - 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: number + 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), cannot be reused across datasets, and must be at + most 255 characters. + 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' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + required: + - data + - meta + DeleteDatasetRunResponse: + title: DeleteDatasetRunResponse + type: object + properties: + message: + type: string + required: + - message + ExperimentsResponse: + title: ExperimentsResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Experiment' + meta: + $ref: '#/components/schemas/ExperimentsResponseMeta' + required: + - data + - meta + ExperimentsResponseMeta: + title: ExperimentsResponseMeta + type: object + properties: + cursor: + type: string + nullable: true + description: >- + Versioned base64url cursor for retrieving the next page. Absent when + there are no more results. + Experiment: + title: Experiment + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + nullable: true + startTime: + type: string + format: date-time + description: |- + Start of the experiment, i.e. the earliest event within the + requested time range. Clipped to `fromStartTime` when the + experiment started before the requested range. + endTime: + type: string + format: date-time + description: |- + End of the experiment, i.e. the latest event end within the + requested time range. + itemCount: + type: integer + description: Number of experiment items within the requested time range. + datasetId: + type: string + nullable: true + description: Null when the experiment is not associated with a dataset. + metadata: + type: object + additionalProperties: true + nullable: true + description: Included only when `fields=metadata` is requested. + scores: + type: array + items: + $ref: '#/components/schemas/ScoreV3' + nullable: true + description: >- + Included only when `fields=scores` is requested. Contains scores + directly attached to the experiment. + required: + - id + - name + - description + - startTime + - endTime + - itemCount + - datasetId + ExperimentItemsResponse: + title: ExperimentItemsResponse + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ExperimentItem' + meta: + $ref: '#/components/schemas/ExperimentsResponseMeta' + required: + - data + - meta + ExperimentItem: + title: ExperimentItem + type: object + properties: + id: + type: string + traceId: + type: string + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + nullable: true + level: + $ref: '#/components/schemas/ObservationLevel' + environment: + type: string + experimentId: + type: string + experimentName: + type: string + experimentItemId: + type: string + experimentDatasetId: + type: string + nullable: true + description: Included when `fields=dataset` is requested. + experimentItemVersion: + type: string + format: date-time + nullable: true + description: Included when `fields=dataset` is requested. + input: + nullable: true + description: Included when `fields=io` is requested. + output: + nullable: true + description: Included when `fields=io` is requested. + expectedOutput: + nullable: true + description: Included when `fields=io` is requested. + metadata: + type: object + additionalProperties: true + nullable: true + description: Included when `fields=metadata` is requested. + experimentItemMetadata: + type: object + additionalProperties: true + nullable: true + description: Included when `fields=itemMetadata` is requested. + experimentMetadata: + type: object + additionalProperties: true + nullable: true + description: Included when `fields=experimentMetadata` is requested. + experimentDescription: + type: string + nullable: true + description: Included when `fields=experimentMetadata` is requested. + scores: + type: array + items: + $ref: '#/components/schemas/ScoreV3' + nullable: true + description: >- + Included only when `fields=scores` is requested. Contains item and + trace scores only; experiment-level scores are returned by the + experiments endpoint. + required: + - id + - traceId + - startTime + - endTime + - level + - environment + - experimentId + - experimentName + - experimentItemId + FeedbackTargetType: + title: FeedbackTargetType + type: string + enum: + - skill + - mcp-tool + - cli + - docs + - public-api + - other + SubmitFeedbackRequest: + title: SubmitFeedbackRequest + type: object + properties: + targetType: + $ref: '#/components/schemas/FeedbackTargetType' + description: Category of the thing the feedback is about. + target: + type: string + description: >- + The specific instance within targetType: the skill name, MCP tool + name, CLI command, API endpoint path, or docs page path (e.g. + 'queryMetrics', '/docs/mcp'). An identifier, not a sentence. Must be + between 1 and 200 characters. + feedback: + type: string + description: >- + Concise feedback text approved by the user. Must be between 1 and + 3000 characters. + goal: + type: string + nullable: true + description: >- + Optional user-approved goal or use case they were trying to achieve. + Must be between 1 and 1500 characters when provided. Do not include + secrets, customer data, trace payloads, or broad unrelated context. + referenceUrl: + type: string + nullable: true + description: >- + Optional HTTP(S) reference URL. Langfuse stores it as text for + triage and does not fetch it. + required: + - targetType + - target + - feedback + SubmitFeedbackResponse: + title: SubmitFeedbackResponse + type: object + properties: + id: + type: string + description: Correlation ID for the submitted feedback. + required: + - id + HealthResponse: + title: HealthResponse + type: object + properties: + version: + type: string + description: Langfuse server version + status: + type: string + 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. + 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. + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + required: + - data + - meta + 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 OpenAI + (`{"useResponsesApi":true}`), 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"}`) - **OpenAI**: Optional. If provided, must + be `{"useResponsesApi": }` to control whether Langfuse + routes calls through OpenAI's Responses API. - **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 + description: >- + Request a presigned media upload URL. Provide exactly one context: a + trace (traceId, optionally observationId) or a dataset item (datasetId + + datasetItemId). field is required and must match the chosen context. + properties: + traceId: + type: string + nullable: true + description: >- + The trace the media is associated with. Null for dataset item media + uploads. + 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. + datasetId: + type: string + nullable: true + description: >- + The dataset the media belongs to. Null for trace/observation media + uploads. + datasetItemId: + type: string + nullable: true + description: >- + The dataset item the media is associated with (need not exist yet). + Null for trace/observation media uploads. + 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 item field the media is in: `input`/`output`/`metadata` (trace) + or `input`/`expectedOutput`/`metadata` (dataset item). + required: + - 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 + ScoreSubjectTraceV3: + title: ScoreSubjectTraceV3 + type: object + properties: + id: + type: string + description: The trace ID. + required: + - id + ScoreSubjectObservationV3: + title: ScoreSubjectObservationV3 + type: object + properties: + id: + type: string + description: The observation ID. + traceId: + type: string + nullable: true + description: The parent trace ID, if available. + required: + - id + ScoreSubjectSessionV3: + title: ScoreSubjectSessionV3 + type: object + properties: + id: + type: string + description: The session ID. + required: + - id + ScoreSubjectExperimentV3: + title: ScoreSubjectExperimentV3 + type: object + properties: + id: + type: string + description: The dataset run ID (experiment ID). + required: + - id + ScoreSubjectV3: + title: ScoreSubjectV3 + oneOf: + - type: object + allOf: + - type: object + properties: + kind: + type: string + enum: + - trace + - $ref: '#/components/schemas/ScoreSubjectTraceV3' + required: + - kind + - type: object + allOf: + - type: object + properties: + kind: + type: string + enum: + - observation + - $ref: '#/components/schemas/ScoreSubjectObservationV3' + required: + - kind + - type: object + allOf: + - type: object + properties: + kind: + type: string + enum: + - session + - $ref: '#/components/schemas/ScoreSubjectSessionV3' + required: + - kind + - type: object + allOf: + - type: object + properties: + kind: + type: string + enum: + - experiment + - $ref: '#/components/schemas/ScoreSubjectExperimentV3' + required: + - kind + description: >- + A reference to the entity this score is attached to. Discriminated by + "kind" β€” one of trace, observation, session, or experiment. + BaseScoreV3: + title: BaseScoreV3 + type: object + properties: + id: + type: string + projectId: + type: string + name: + type: string + source: + $ref: '#/components/schemas/ScoreSource' + timestamp: + type: string + format: date-time + environment: + type: string + description: The environment from which this score originated. + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + comment: + type: string + nullable: true + description: >- + Optional comment attached to the score. Present when "details" is + included in the fields parameter. + configId: + type: string + nullable: true + description: >- + The score config ID, if this score was created from a config. + Present when "details" is included in the fields parameter. + metadata: + type: object + additionalProperties: true + nullable: true + description: >- + Arbitrary metadata attached to the score. Present when "details" is + included in the fields parameter. + authorUserId: + type: string + nullable: true + description: >- + The user who created this score, if available. Present when + "annotation" is included in the fields parameter. + queueId: + type: string + nullable: true + description: >- + The annotation queue this score belongs to, if any. Present when + "annotation" is included in the fields parameter. + subject: + $ref: '#/components/schemas/ScoreSubjectV3' + nullable: true + description: >- + The entity this score is attached to (trace, observation, session, + or experiment). Present when "subject" is included in the fields + parameter. + required: + - id + - projectId + - name + - source + - timestamp + - environment + - createdAt + - updatedAt + NumericScoreV3: + title: NumericScoreV3 + type: object + properties: + value: + type: number + format: double + description: The numeric value of the score. + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV3' + BooleanScoreV3: + title: BooleanScoreV3 + type: object + properties: + value: + type: boolean + description: The boolean value of the score. + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV3' + CategoricalScoreV3: + title: CategoricalScoreV3 + type: object + properties: + value: + type: string + description: The string category value of the score. + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV3' + TextScoreV3: + title: TextScoreV3 + type: object + properties: + value: + type: string + description: The text content of the score. + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV3' + CorrectionScoreV3: + title: CorrectionScoreV3 + type: object + properties: + value: + type: string + description: The correction content of the score. Empty string if not set. + required: + - value + allOf: + - $ref: '#/components/schemas/BaseScoreV3' + ScoreV3: + title: ScoreV3 + oneOf: + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - NUMERIC + - $ref: '#/components/schemas/NumericScoreV3' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - BOOLEAN + - $ref: '#/components/schemas/BooleanScoreV3' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CATEGORICAL + - $ref: '#/components/schemas/CategoricalScoreV3' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - TEXT + - $ref: '#/components/schemas/TextScoreV3' + required: + - dataType + - type: object + allOf: + - type: object + properties: + dataType: + type: string + enum: + - CORRECTION + - $ref: '#/components/schemas/CorrectionScoreV3' + required: + - dataType + GetScoresV3Meta: + title: GetScoresV3Meta + type: object + properties: + limit: + type: integer + cursor: + type: string + nullable: true + description: >- + URL-safe base64 (base64url) cursor for the next page. Absent when + there are no more results. + required: + - limit + GetScoresV3Response: + title: GetScoresV3Response + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ScoreV3' + meta: + $ref: '#/components/schemas/GetScoresV3Meta' + required: + - data + - meta + CreateScoreRequest: + title: CreateScoreRequest + 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 + 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/CreateScoreSource' + 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 + CreateScoreSource: + title: CreateScoreSource + 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. + CreateScoreResponse: + title: CreateScoreResponse + type: object + properties: + id: + type: string + description: The id of the created object in Langfuse + required: + - id + 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' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + required: + - data + - meta + PaginatedSessions: + title: PaginatedSessions + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Session' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + required: + - data + - meta + Traces: + title: Traces + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/TraceWithDetails' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + _deprecation: + $ref: '#/components/schemas/Deprecation' + nullable: true + 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 + - code + description: |- + The evaluator engine type. + + The unstable public API supports LLM-as-a-judge and code evaluators. + unstableCodeEvaluatorSourceCodeLanguage: + title: unstableCodeEvaluatorSourceCodeLanguage + type: string + enum: + - PYTHON + - TYPESCRIPT + description: Code evaluator runtime language. + 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`, `metadata`, and `tool_calls`. + - `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 + - tool_calls + - 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`, `metadata`, and + `tool_calls` + + - `target=experiment` supports `input`, `output`, `metadata`, + `tool_calls`, `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. + + - `tool_calls`: the tool calls recorded on the observation, as an array + of `{id, name, arguments, type, index}` objects in the order the model + emitted them. Combine with `jsonPath` (for example `$[*].name`) to + select parts of each call. + + - `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. + model: + type: string + description: >- + Model identifier exposed by the provider, for 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 variable to one source field from the target object. + + + Manual mappings are used for `llm_as_judge` evaluators. `code` + evaluators use a fixed runtime mapping managed by Langfuse. + + + 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 LLM-as-judge 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`. + source: + $ref: '#/components/schemas/unstableEvaluationRuleMappingSource' + description: >- + Source field that should populate the prompt variable. + + + Quick reference: + + - `target=observation`: `input`, `output`, `metadata`, `tool_calls` + + - `target=experiment`: `input`, `output`, `metadata`, `tool_calls`, + `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` + + - `boolean`: `"="`, `"<>"` + + - `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` + - `isRootObservation`: `boolean`, operators `=` / `<>`; true when the observation has no parent or is explicitly marked as an application root + - `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. + unstableDashboardWidgetView: + title: unstableDashboardWidgetView + type: string + enum: + - observations + - scores-numeric + - scores-boolean + - scores-categorical + unstableDashboardWidgetViewWithLegacy: + title: unstableDashboardWidgetViewWithLegacy + type: string + enum: + - observations + - scores-numeric + - scores-boolean + - scores-categorical + - traces + description: |- + Widget data view. Responses may include the legacy `traces` value for + widgets created before this API existed. + unstableDashboardWidgetChartType: + title: unstableDashboardWidgetChartType + type: string + enum: + - LINE_TIME_SERIES + - AREA_TIME_SERIES + - BAR_TIME_SERIES + - HORIZONTAL_BAR + - VERTICAL_BAR + - PIE + - NUMBER + - HISTOGRAM + - PIVOT_TABLE + unstableDashboardWidgetMetricAggregation: + title: unstableDashboardWidgetMetricAggregation + type: string + enum: + - sum + - avg + - count + - max + - min + - p50 + - p75 + - p90 + - p95 + - p99 + - histogram + - uniq + unstableDashboardWidgetDimension: + title: unstableDashboardWidgetDimension + type: object + properties: + field: + type: string + required: + - field + unstableDashboardWidgetMetric: + title: unstableDashboardWidgetMetric + type: object + properties: + measure: + type: string + agg: + $ref: '#/components/schemas/unstableDashboardWidgetMetricAggregation' + required: + - measure + - agg + unstableDashboardWidgetFilter: + title: unstableDashboardWidgetFilter + type: object + description: >- + A filter in Langfuse filter-state shape. The `value` shape and the + + allowed operators depend on `type`: + + + | `type` | `value` | operators | + + |---|---|---| + + | `string` | string | `=`, `contains`, `does not contain`, `starts + with`, `ends with` | + + | `number` | number | `=`, `>`, `<`, `>=`, `<=` | + + | `datetime` | ISO datetime string | `>`, `<`, `>=`, `<=` | + + | `boolean` | boolean | `=`, `<>` | + + | `null` | `""` | `is null`, `is not null` | + + | `stringOptions` | list of strings | `any of`, `none of` | + + | `arrayOptions` | list of strings | `any of`, `none of`, `all of` | + + | `categoryOptions` | list of strings (requires `key`) | `any of`, `none + of` | + + | `stringObject` | string (requires `key`, e.g. a metadata key) | same + as `string` | + + | `numberObject` | number (requires `key`, e.g. a score name) | same as + `number` | + + | `booleanObject` | boolean (requires `key`) | `=`, `<>` | + properties: + column: + type: string + operator: + type: string + type: + type: string + value: + nullable: true + key: + type: string + nullable: true + required: + - column + - operator + - type + unstableDashboardWidgetChartConfig: + title: unstableDashboardWidgetChartConfig + type: object + description: |- + Chart-specific widget configuration. + + `type` must match the top-level `chartType`. + `row_limit` applies to total-value charts and pivot tables. + `bins` applies to histograms. + `defaultSort` applies to pivot tables. + properties: + type: + $ref: '#/components/schemas/unstableDashboardWidgetChartType' + row_limit: + type: integer + nullable: true + show_value_labels: + type: boolean + nullable: true + bins: + type: integer + nullable: true + defaultSort: + $ref: '#/components/schemas/unstableDashboardWidgetDefaultSort' + nullable: true + required: + - type + unstableDashboardWidgetDefaultSort: + title: unstableDashboardWidgetDefaultSort + type: object + properties: + column: + type: string + order: + $ref: '#/components/schemas/unstableDashboardWidgetSortOrder' + required: + - column + - order + unstableDashboardWidgetSortOrder: + title: unstableDashboardWidgetSortOrder + type: string + enum: + - ASC + - DESC + unstableDashboardWidgetChartConfigInput: + title: unstableDashboardWidgetChartConfigInput + type: object + description: |- + Input-side chart config. `type` is optional and defaults to the + widget's `chartType`; when given it must match. + properties: + type: + $ref: '#/components/schemas/unstableDashboardWidgetChartType' + nullable: true + row_limit: + type: integer + nullable: true + show_value_labels: + type: boolean + nullable: true + bins: + type: integer + nullable: true + defaultSort: + $ref: '#/components/schemas/unstableDashboardWidgetDefaultSort' + nullable: true + unstableCreateDashboardWidgetRequest: + title: unstableCreateDashboardWidgetRequest + type: object + properties: + name: + type: string + description: + type: string + nullable: true + description: Defaults to an empty string. + view: + $ref: '#/components/schemas/unstableDashboardWidgetView' + dimensions: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetDimension' + metrics: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetMetric' + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + chartType: + $ref: '#/components/schemas/unstableDashboardWidgetChartType' + chartConfig: + $ref: '#/components/schemas/unstableDashboardWidgetChartConfigInput' + nullable: true + description: Defaults to the plain config for `chartType`. + required: + - name + - view + - dimensions + - metrics + - filters + - chartType + unstableUpdateDashboardWidgetRequest: + title: unstableUpdateDashboardWidgetRequest + type: object + properties: + name: + type: string + nullable: true + description: + type: string + nullable: true + view: + $ref: '#/components/schemas/unstableDashboardWidgetView' + nullable: true + dimensions: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetDimension' + nullable: true + metrics: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetMetric' + nullable: true + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + nullable: true + chartType: + $ref: '#/components/schemas/unstableDashboardWidgetChartType' + nullable: true + chartConfig: + $ref: '#/components/schemas/unstableDashboardWidgetChartConfigInput' + nullable: true + unstableDashboardWidgetList: + title: unstableDashboardWidgetList + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidget' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + unstableDeleteDashboardWidgetResponse: + title: unstableDeleteDashboardWidgetResponse + type: object + properties: + message: + type: string + required: + - message + unstableDashboardWidget: + title: unstableDashboardWidget + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + name: + type: string + description: + type: string + view: + $ref: '#/components/schemas/unstableDashboardWidgetViewWithLegacy' + dimensions: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetDimension' + metrics: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetMetric' + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + chartType: + $ref: '#/components/schemas/unstableDashboardWidgetChartType' + chartConfig: + $ref: '#/components/schemas/unstableDashboardWidgetChartConfig' + required: + - id + - createdAt + - updatedAt + - name + - description + - view + - dimensions + - metrics + - filters + - chartType + - chartConfig + unstableDashboardPlacement: + title: unstableDashboardPlacement + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - widget + - $ref: '#/components/schemas/unstableWidgetPlacement' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - preset + - $ref: '#/components/schemas/unstablePresetPlacement' + required: + - type + description: |- + A tile on the dashboard's 12-column grid. `x`/`y` are the tile's + top-left cell (0-based; `y` grows downward), `width`/`height` its size + in cells. The UI default tile is 6x6 (half width). Overlapping tiles + are not rejected; prefer appending below existing tiles (or omit the + position on create to let the server do it). + unstableWidgetPlacement: + title: unstableWidgetPlacement + type: object + properties: + id: + type: string + widgetId: + type: string + x: + type: integer + 'y': + type: integer + width: + type: integer + height: + type: integer + required: + - id + - widgetId + - x + - 'y' + - width + - height + unstablePresetPlacement: + title: unstablePresetPlacement + type: object + properties: + id: + type: string + presetId: + type: string + x: + type: integer + 'y': + type: integer + width: + type: integer + height: + type: integer + required: + - id + - presetId + - x + - 'y' + - width + - height + unstableCreateDashboardPlacementRequest: + title: unstableCreateDashboardPlacementRequest + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - widget + - $ref: '#/components/schemas/unstableCreateWidgetPlacement' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - preset + - $ref: '#/components/schemas/unstableCreatePresetPlacement' + required: + - type + unstableCreateWidgetPlacement: + title: unstableCreateWidgetPlacement + type: object + properties: + id: + type: string + nullable: true + description: Server-generated when omitted. + widgetId: + type: string + x: + type: integer + nullable: true + description: Grid column (12-column grid). Defaults to `0`. + 'y': + type: integer + nullable: true + description: Grid row. Defaults to the first row below all existing tiles. + width: + type: integer + nullable: true + description: Width in grid columns. Defaults to `6`. + height: + type: integer + nullable: true + description: Height in grid rows. Defaults to `6`. + required: + - widgetId + unstableCreatePresetPlacement: + title: unstableCreatePresetPlacement + type: object + properties: + id: + type: string + nullable: true + description: Server-generated when omitted. + presetId: + type: string + x: + type: integer + nullable: true + description: Grid column (12-column grid). Defaults to `0`. + 'y': + type: integer + nullable: true + description: Grid row. Defaults to the first row below all existing tiles. + width: + type: integer + nullable: true + description: Width in grid columns. Defaults to `6`. + height: + type: integer + nullable: true + description: Height in grid rows. Defaults to `6`. + required: + - presetId + unstableUpdateDashboardPlacementRequest: + title: unstableUpdateDashboardPlacementRequest + type: object + properties: + x: + type: integer + nullable: true + description: Grid column (12-column grid). + 'y': + type: integer + nullable: true + description: Grid row. + width: + type: integer + nullable: true + description: Width in grid columns. + height: + type: integer + nullable: true + description: Height in grid rows. + unstableDeleteDashboardPlacementResponse: + title: unstableDeleteDashboardPlacementResponse + type: object + properties: + message: + type: string + required: + - message + unstableDashboardDefinition: + title: unstableDashboardDefinition + type: object + properties: + widgets: + type: array + items: + $ref: '#/components/schemas/unstableDashboardPlacement' + required: + - widgets + unstableDashboard: + title: unstableDashboard + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + name: + type: string + description: + type: string + definition: + $ref: '#/components/schemas/unstableDashboardDefinition' + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + description: Dashboard-level filters applied to all widgets on the dashboard. + required: + - id + - createdAt + - updatedAt + - name + - description + - definition + - filters + unstableDashboardList: + title: unstableDashboardList + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/unstableDashboard' + meta: + $ref: '#/components/schemas/utilsMetaResponse' + required: + - data + - meta + unstableCreateDashboardRequest: + title: unstableCreateDashboardRequest + type: object + properties: + name: + type: string + description: + type: string + nullable: true + definition: + $ref: '#/components/schemas/unstableDashboardDefinition' + nullable: true + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + nullable: true + required: + - name + unstableUpdateDashboardRequest: + title: unstableUpdateDashboardRequest + type: object + properties: + name: + type: string + nullable: true + description: + type: string + nullable: true + definition: + $ref: '#/components/schemas/unstableDashboardDefinition' + nullable: true + filters: + type: array + items: + $ref: '#/components/schemas/unstableDashboardWidgetFilter' + nullable: true + unstableDeleteDashboardResponse: + title: unstableDeleteDashboardResponse + type: object + properties: + message: + type: string + required: + - message + 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. + 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 + unstableEvaluationRuleBase: + title: unstableEvaluationRuleBase + 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. + name: + type: string + description: >- + Human-readable deployment name. This is independent from the + evaluator name. + 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. + enabled: + type: boolean + description: Desired enabled state configured by the client. + 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. + createdAt: + type: string + format: date-time + description: Timestamp when the evaluation rule was created. + updatedAt: + type: string + format: date-time + description: Timestamp when the evaluation rule was last updated. + required: + - id + - name + - evaluator + - enabled + - status + - pausedReason + - pausedMessage + - sampling + - createdAt + - updatedAt + unstableEvaluationRule: + title: unstableEvaluationRule + type: object + properties: + target: + $ref: '#/components/schemas/unstableEvaluationRuleTarget' + description: Target object type that should trigger scoring. + 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 evaluator runtime variables from + the live target object. + required: + - target + - filter + - mapping + allOf: + - $ref: '#/components/schemas/unstableEvaluationRuleBase' + unstableLegacyEvaluationRule: + title: unstableLegacyEvaluationRule + type: object + description: >- + Legacy trace- or dataset-level evaluation rule returned by list and get + for migration. + + + This resource is read-only through the unstable public API. Its mapping + preserves the trace, dataset item, or named observation that each + evaluator variable previously read from. Its filters use the persisted + legacy filter format so migration clients can read the configuration + without losing information. + properties: + target: + $ref: '#/components/schemas/unstableLegacyEvaluationRuleTarget' + delay: + type: integer + description: Delay in milliseconds before the legacy evaluation job runs. + timeScope: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleTimeScope' + description: >- + Whether the legacy rule evaluates newly ingested data, existing + data, or both. + filter: + type: array + items: + $ref: '#/components/schemas/unstableEvaluationRuleFilter' + description: Stored filters used by the legacy trace or dataset rule. + mapping: + type: array + items: + $ref: '#/components/schemas/unstableLegacyEvaluationRuleMapping' + description: >- + Stored variable mappings, including the trace, dataset item, or + named observation selected for each variable. + required: + - target + - delay + - timeScope + - filter + - mapping + allOf: + - $ref: '#/components/schemas/unstableEvaluationRuleBase' + unstableReadableEvaluationRule: + title: unstableReadableEvaluationRule + oneOf: + - $ref: '#/components/schemas/unstableEvaluationRule' + - $ref: '#/components/schemas/unstableLegacyEvaluationRule' + description: >- + Evaluation rule returned by list and get, including read-only legacy + trace and dataset rules. + unstableEvaluationRules: + title: unstableEvaluationRules + type: object + description: Paginated list of evaluation rules. + properties: + data: + type: array + items: + $ref: '#/components/schemas/unstableReadableEvaluationRule' + description: Evaluation rules in the current page. + meta: + $ref: '#/components/schemas/utilsMetaResponse' + description: Standard pagination metadata. + required: + - data + - meta + unstableCreateEvaluationRuleRequest: + title: unstableCreateEvaluationRuleRequest + oneOf: + - $ref: '#/components/schemas/unstableCreateLlmAsJudgeEvaluationRuleRequest' + - $ref: '#/components/schemas/unstableCreateCodeEvaluationRuleRequest' + 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` + + - for `llm_as_judge`, fetch or inspect the evaluator first and provide a + complete variable mapping for every evaluator variable + + - for `code`, do not send variables or mappings; Langfuse stores the + fixed code runtime mapping automatically + + - optionally narrow execution with `filter` + + - set `enabled=true` only when you want live execution immediately + unstableCreateLlmAsJudgeEvaluationRuleRequest: + title: unstableCreateLlmAsJudgeEvaluationRuleRequest + type: object + properties: + name: + type: string + description: Human-readable deployment name. + evaluator: + $ref: >- + #/components/schemas/unstableLlmAsJudgeEvaluationRuleEvaluatorReference + description: >- + LLM-as-judge evaluator family to use. + + + Use `name`, `scope`, and `type` from the evaluator endpoints. If + `type` is omitted, Langfuse defaults it to `llm_as_judge` for + backwards compatibility. + + 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. + 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: >- + LLM-as-judge 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 + unstableCreateCodeEvaluationRuleRequest: + title: unstableCreateCodeEvaluationRuleRequest + type: object + properties: + name: + type: string + description: Human-readable deployment name. + evaluator: + $ref: '#/components/schemas/unstableCodeEvaluationRuleEvaluatorReference' + description: >- + Code evaluator family to use. + + + Use `name`, `scope`, and `type` 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. + 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. + required: + - name + - evaluator + - target + - enabled + 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 to an LLM-as-judge `evaluator`, send a fresh `mapping` + unless you are certain the existing mapping still matches the evaluator + variables. + + - If you change `target` for an LLM-as-judge rule, usually send both + `filter` and `mapping` in the same request. + + - For code evaluator rules, omit `mapping`; Langfuse stores the fixed + code runtime mapping automatically. + + - 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. + + A rule's evaluator type cannot be changed: provide `name` and + `scope` for an evaluator family of the rule's current type. To use a + different evaluator type, create a new 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 LLM-as-judge variable mappings. + + + Do not send this field for code evaluator rules. Langfuse stores the + fixed code runtime mapping automatically and returns it in the + response. + 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 updating an evaluation rule. + + + `name` and `scope` identify the evaluator family in the authenticated + project context. + + A rule's evaluator type cannot be changed, so this reference does not + accept a `type`; the family must match the rule's current evaluator + type. + 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 + unstableLlmAsJudgeEvaluationRuleEvaluatorReference: + title: unstableLlmAsJudgeEvaluationRuleEvaluatorReference + type: object + description: >- + LLM-as-judge evaluator family reference used when creating an evaluation + rule. + properties: + name: + type: string + description: Evaluator family name. + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: Whether the evaluator family is project-owned or Langfuse-managed. + type: + $ref: '#/components/schemas/unstableLlmAsJudgeEvaluatorType' + nullable: true + description: Evaluator engine type. Defaults to `llm_as_judge` when omitted. + required: + - name + - scope + unstableCodeEvaluationRuleEvaluatorReference: + title: unstableCodeEvaluationRuleEvaluatorReference + type: object + description: Code evaluator family reference used when creating an evaluation rule. + properties: + name: + type: string + description: Evaluator family name. + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: Whether the evaluator family is project-owned or Langfuse-managed. + type: + type: string + const: code + description: Must be `code`. + required: + - name + - scope + - type + unstableLlmAsJudgeEvaluatorType: + title: unstableLlmAsJudgeEvaluatorType + type: string + enum: + - llm_as_judge + unstableEvaluationRuleTimeScope: + title: unstableEvaluationRuleTimeScope + type: string + enum: + - NEW + - EXISTING + unstableLegacyEvaluationRuleTarget: + title: unstableLegacyEvaluationRuleTarget + type: string + enum: + - trace + - dataset + unstableLegacyEvaluationRuleMapping: + title: unstableLegacyEvaluationRuleMapping + type: object + description: >- + Maps one evaluator variable to a trace, dataset item, or field on a + named observation in a legacy rule. + properties: + variable: + type: string + description: Evaluator prompt variable populated by this mapping. + langfuseObject: + $ref: '#/components/schemas/unstableLegacyEvaluationObject' + description: >- + Trace, dataset item, or observation type from which the value is + read. + objectName: + type: string + nullable: true + description: >- + Observation name to match, or `null` when `langfuseObject` is + `trace` or `dataset_item`. + source: + type: string + description: Stored field selected from the trace, dataset item, or observation. + jsonPath: + type: string + nullable: true + description: Optional JSONPath selector applied to the selected field. + required: + - variable + - langfuseObject + - objectName + - source + unstableLegacyEvaluationObject: + title: unstableLegacyEvaluationObject + type: string + enum: + - trace + - span + - generation + - event + - agent + - tool + - chain + - retriever + - evaluator + - embedding + - guardrail + - dataset_item + unstableEvaluationRuleEvaluator: + title: unstableEvaluationRuleEvaluator + type: object + description: |- + Resolved evaluator currently used by the evaluation rule. + + `id` is the exact active evaluator version. + `name`, `scope`, and `type` 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. + type: + $ref: '#/components/schemas/unstableEvaluatorType' + description: Evaluator engine type. + required: + - id + - name + - scope + - type + unstableDeleteEvaluatorResponse: + title: unstableDeleteEvaluatorResponse + type: object + description: Confirmation response returned after successful deletion. + properties: + message: + type: string + description: Always `Evaluator successfully deleted`. + required: + - message + unstableEvaluator: + title: unstableEvaluator + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - llm_as_judge + - $ref: '#/components/schemas/unstableLlmAsJudgeEvaluator' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - code + - $ref: '#/components/schemas/unstableCodeEvaluator' + required: + - type + description: >- + One evaluator that can be used for scoring. + + + An evaluator describes **how** to score data. + + + It does not define **which** live objects are evaluated. That is the job + of `evaluation-rules`. + + + For agent clients, the most important fields are: + + - `type`: determines which evaluator fields are present + + - `variables`: for LLM evaluators, use these exact names when building + the evaluation-rule `mapping` array. LLM evaluators require every + variable to be mapped. Code evaluators always expose the fixed runtime + payload fields and Langfuse maps them automatically. + + + 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`). + unstableEvaluatorBase: + title: unstableEvaluatorBase + type: object + properties: + id: + type: string + description: Identifier of this evaluator. + name: + type: string + description: Evaluator name. + version: + type: integer + description: Version number of this evaluator. + scope: + $ref: '#/components/schemas/unstableEvaluatorScope' + description: >- + Where this evaluator comes from: your project or Langfuse-managed + defaults. + variables: + type: array + items: + type: string + description: >- + Variables that can be mapped when creating an evaluation rule. + + + LLM evaluators require every variable to be mapped exactly once. + Code evaluators always expose the fixed runtime payload fields and + Langfuse maps them automatically. + evaluationRuleCount: + type: integer + description: >- + Number of evaluation rules in the project that currently use this + evaluator version. + createdAt: + type: string + format: date-time + description: Timestamp when this evaluator was created. + updatedAt: + type: string + format: date-time + description: Timestamp when this evaluator was last updated. + required: + - id + - name + - version + - scope + - variables + - evaluationRuleCount + - createdAt + - updatedAt + unstableLlmAsJudgeEvaluator: + title: unstableLlmAsJudgeEvaluator + type: object + properties: + prompt: + type: string + description: Prompt template used during evaluation. + 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. + required: + - prompt + - outputDefinition + - modelConfig + allOf: + - $ref: '#/components/schemas/unstableEvaluatorBase' + unstableCodeEvaluator: + title: unstableCodeEvaluator + type: object + properties: + sourceCode: + type: string + description: Source code executed for each matched observation. + sourceCodeLanguage: + $ref: '#/components/schemas/unstableCodeEvaluatorSourceCodeLanguage' + description: Runtime language for `sourceCode`. + required: + - sourceCode + - sourceCodeLanguage + allOf: + - $ref: '#/components/schemas/unstableEvaluatorBase' + 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 + oneOf: + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - llm_as_judge + - $ref: '#/components/schemas/unstableCreateLlmAsJudgeEvaluatorRequest' + required: + - type + - type: object + allOf: + - type: object + properties: + type: + type: string + enum: + - code + - $ref: '#/components/schemas/unstableCreateCodeEvaluatorRequest' + required: + - type + 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. + + If `type` is omitted, Langfuse defaults it to `llm_as_judge` for + backwards compatibility. + unstableCreateLlmAsJudgeEvaluatorRequest: + title: unstableCreateLlmAsJudgeEvaluatorRequest + type: object + properties: + name: + type: string + description: Evaluator name within the authenticated project. + prompt: + type: string + description: Prompt template used by the evaluator. + 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 + unstableCreateCodeEvaluatorRequest: + title: unstableCreateCodeEvaluatorRequest + type: object + properties: + name: + type: string + description: Evaluator name within the authenticated project. + sourceCode: + type: string + description: Code executed for each matched observation. + sourceCodeLanguage: + $ref: '#/components/schemas/unstableCodeEvaluatorSourceCodeLanguage' + description: Runtime language for `sourceCode`. + required: + - name + - sourceCode + - sourceCodeLanguage + 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..a3e5095 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.22.1 + 3.2.1 4.11.0 - 5.9.3 + 6.1.2 + 2.0.18 + 7.24.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); + } + } + } +}