Compare commits

...

396 Commits

Author SHA1 Message Date
KiewanVillatel 2775de9d25 release: 0.1.0-beta.19 (#195) 2026-07-23 12:31:02 +02:00
github-actions[bot] 2ab482c2d3 release: 0.1.0-beta.19 2026-07-23 10:21:10 +00:00
KiewanVillatel b1649d975e Release SDK updates 2026-07-23 12:20:48 +02:00
stlc-bot aece72d035 refactor(runs): remove SmithDB v2 endpoint flag [LSO-3391]
Stainless-Generated-From: eaf6eeeffa6089b29a6ed890ffccada85c435446
2026-07-23 00:21:12 +00:00
stlc-bot aa4be05c11 feat(backend): update CRUD endpoint to allow retrieving / storing chart series metadata
Stainless-Generated-From: d6a6962cef9066a8de2683d0a48553057474595b
2026-07-22 19:30:16 +00:00
stlc-bot 8aded40800 feat(backend): Allow storing grid layout to custom dashboards
Stainless-Generated-From: 235750773cea09074a5f805563dba096b19f5c6a
2026-07-22 18:27:41 +00:00
stlc-bot 6165434d21 feat(sandboxes): free-form labels on sandboxes and snapshots
Stainless-Generated-From: 5a8ad04cc2a2a4afaa4f310a862d9701806a1662
2026-07-22 14:07:11 +00:00
stlc-bot 95da1e9b94 revert(runs): require session in POST /runs/stats
Stainless-Generated-From: 76e29824c8418e6e3596ff4fde3f2abec3fd34f9
2026-07-22 12:49:59 +00:00
stlc-bot eace8c3f99 feat(sandboxes): let proxy rules contribute sandbox env vars
Stainless-Generated-From: 3d4147438f77389052680dd6bf85a9c90ab4d373
2026-07-22 10:42:41 +00:00
stlc-bot 056dbd3673 fix(runs): require session in POST /runs/stats
Stainless-Generated-From: b24d4f534d67323bbf8765d82bb0843c24030da5
2026-07-22 10:25:34 +00:00
stlc-bot e7fe407f74 fix: support last_queued_at in v2 runs query
Stainless-Generated-From: da0d737eda0ece655bc0779537309b706951bb74
2026-07-21 23:20:53 +00:00
stlc-bot f6608288d4 feat(host): p50/p99 run queue wait-time chart in project monitoring [LSD-1547]
Stainless-Generated-From: 17fb5d12f51ab1746f996098443e5b69b023ca82
2026-07-21 20:22:16 +00:00
stlc-bot b072b2e2fc fix(evaluators): persist and wire through playground_settings_id for llm-as-judge oauth models [part of ENT-1402]
Stainless-Generated-From: 809ff1ccff334354d5ce4e55c50e5f4974bcf13f
2026-07-21 15:57:35 +00:00
stlc-bot 391b75ca3b Build SDK
Stainless-Generated-From: df7d0b0d0f4e763f9a168447fe5c95f5efdf25fa
2026-07-20 13:51:31 +00:00
KiewanVillatel 42326ef486 Merge branch 'main' into stlc/from-prod 2026-07-20 15:04:28 +02:00
KiewanVillatel b6d079b991 ci: sync from prod now targets main instead of next (#48)
## Summary

- Updates `stlc-sync-from-production.yml` to check out `main` (instead
of `next`) and open back-sync PRs targeting `main`
- Aligns with the removal of the `next` branch and the shift to a
direct-to-main flow

## Test plan

- [ ] Trigger `stlc: sync from production` via workflow_dispatch and
confirm the PR it creates targets `main`

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-20 14:39:17 +02:00
KiewanVillatel 804022edf1 release: 0.1.0-beta.18 (#193) 2026-07-20 12:55:54 +02:00
github-actions[bot] feb140e343 release: 0.1.0-beta.18 2026-07-20 10:26:58 +00:00
KiewanVillatel 0d0a8f5ffb Release SDK updates (#191) 2026-07-20 12:26:41 +02:00
stlc-bot 57da309643 feat(runs): add GET /v2/runs/{run_id}/url endpoint and SDK runs.get_url method
Stainless-Generated-From: 12ef224c44ef834db59a6216eb1010f87947b165
2026-07-20 10:12:06 +00:00
stlc-bot 388984c005 fix: build, lint, and test failures (feat(gateway): rate limit policy CRUD, validation, and materialization [ENT-1138]) 2026-07-17 21:55:39 +00:00
KiewanVillatel 283c4c2bcf release: 0.1.0-beta.17 (#190) 2026-07-17 20:47:23 +02:00
github-actions[bot] b8f1caebe1 release: 0.1.0-beta.17 2026-07-17 18:39:52 +00:00
KiewanVillatel b64cfc2ca8 Release SDK updates (#189) 2026-07-17 20:39:32 +02:00
stlc-bot 101472f4ce feat(runs/v2): make start_time optional in GET /v2/runs/:id
Stainless-Generated-From: fc698ee51e0e6b4b1cb0389217ce572774ac2dba
2026-07-17 17:39:28 +00:00
KiewanVillatel df8f95af97 chore: remove stlc-promote-to-main workflow (#45)
## Summary
- Removes `.github/workflows/stlc-promote-to-main.yml`, which triggered
a `next → main` ff-only merge in `langchainplus` whenever the next→main
PR in this repo was approved or CI passed on `next`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 10:56:47 +00:00
KiewanVillatel 6720c1f0e4 ci: run stainless file check on PRs targeting main (#46)
## Summary

- Changes the `check-stainless-file-modification` workflow trigger from
PRs targeting `next` to PRs targeting `main`

## Why

The check was not running on PRs that target `main` directly (e.g.
[#45](https://github.com/langchain-ai/langsmith-java-staging/pull/45)),
leaving Stainless-generated file modifications undetected on that path.

## Test plan

- [ ] Verify the check runs on a future PR targeting `main`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 12:56:13 +02:00
stlc-bot 2f1ca870dd docs: minor rewording of List Commits endpoint description
Stainless-Generated-From: cfaab39cb776a2dff32d8108ebd8c219682c0d92
2026-07-17 09:29:49 +00:00
Kiewan Villatel 7ba0bad56f Build SDK
Stainless-Generated-From: d378b7c5267d135b1741402378738a3d01eeb862
2026-07-17 10:42:39 +02:00
KiewanVillatel 68707629d2 release: 0.1.0-beta.16 (#188) 2026-07-17 09:40:57 +02:00
github-actions[bot] 3880cb324a release: 0.1.0-beta.16 2026-07-17 07:31:20 +00:00
KiewanVillatel 37d650a362 Release SDK updates (#187) 2026-07-17 09:30:59 +02:00
KiewanVillatel cff262179d Merge branch 'main' into stainless/release 2026-07-17 09:23:19 +02:00
KiewanVillatel a19f9aae0b stlc: use main branch, remove next→main promotion
## Summary

This PR is part of migrating stlc to land SDK commits directly on `main`
instead of going through the `next` buffer branch.

- Update `stlc-seal-custom-code.yml` trigger from `branches: [next]` to
`branches: [main]` so the seal workflow fires on pushes to `main`
- Delete `stlc-promote-to-main.yml` entirely — this workflow promoted
sealed commits from `next` into `main`, which is no longer needed once
`next` is removed from the flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 09:19:55 +02:00
stlc-bot 1409dd89d8 fix: remove July 2026 premier plan
Stainless-Generated-From: 09c5b50a463efb9be1cd10176e2bc88fd9e2fc4a
2026-07-16 22:11:59 +00:00
stlc-bot 839107858c chore: new unshare endpoint that doesn't require lookup
Stainless-Generated-From: 3c2f57d95968959d58e0bafbea6a5d0b24077e75
2026-07-16 21:51:34 +00:00
stlc-bot 8eb067064e fix(datasets): serve public dataset sessions via Go, fixing SmithDB-only 501
Stainless-Generated-From: cbd1d7df666a4e821e8ac118fd84eaa8e5acdb00
2026-07-16 16:24:08 +00:00
stlc-bot 306a079479 chore: Stainless SDK methods for V2 public-run APIs
Stainless-Generated-From: 0c767292f9a7937b0d09cb8261ae2c873a41ce2d
2026-07-16 15:15:00 +00:00
John Kennedy 8b1dff9a1e fix: patch 6 high-severity security alerts (#42)
## Security Alert Patch

Resolves all 6 open high-severity Dependabot alerts in the Maven
dependency graph.

### Packages Updated

| Package | Resolved change | Strategy | Scope | CVEs resolved |
|---|---|---|---|---|
| com.fasterxml.jackson.core:jackson-databind | 2.15.3 -> 2.18.8; 2.21.0
-> 2.21.4 | Parent bump for Palantir plus constrained Dokka classpaths |
Build/dev only | CVE-2026-54512, CVE-2026-54513 |
| org.bouncycastle:bcpg-jdk18on | 1.80 -> 1.84 | Constraint on Kotlin
publishing validation | Build/dev only | CVE-2026-3505 |
| org.codehaus.plexus:plexus-utils | 4.0.2 -> 4.0.3 | Constraint on
Shadow plugin classpath | Test/build only | CVE-2025-67030 |

### Strategy Notes

- Palantir Java Format moves from 2.89.0 to 2.93.0, the first release
that declares patched Jackson 2.21.4.
- Dokka 2.2.0 is the latest release and still declares Jackson 2.15.3,
so its buildscript and worker configurations are constrained to 2.18.8.
Remove the constraint when Dokka publishes a patched dependency graph.
- Kotlin publishing validation selects Bouncy Castle 1.80 internally, so
that isolated configuration is constrained to 1.84.
- Shadow 8.3.8 still declares Plexus Utils 4.0.2. Shadow 9.4.3 was
assessed but failed the repository R8 compatibility test, so the plugin
remains at 8.3.8 with a buildscript-only Plexus constraint.

### Advisory Details

- CVE-2026-54512 / GHSA-j3rv-43j4-c7qm: Jackson polymorphic type
validator bypass via generic type parameters.
- CVE-2026-54513 / GHSA-rmj7-2vxq-3g9f: Jackson array subtype allowlist
bypass.
- CVE-2026-3505 / GHSA-cj8j-37rh-8475: Bouncy Castle uncontrolled
resource consumption.
- CVE-2025-67030 / GHSA-6fmv-xxpf-w3cw: Plexus Utils directory traversal
in extractFile.

### Linear Tickets

No matching open Linear tickets found.

### Verification

- [x] All project dependency reports contain no affected vulnerable
versions
- [x] Kotlin and Java lint pass
- [x] Full Gradle test suite passes
- [x] ProGuard and R8 compatibility checks pass
- [x] Dokka Javadoc generation passes
- [x] git diff --check passes
2026-07-16 11:55:45 +02:00
stlc-bot 50525423cc feat(feedback): support GET /feedback for thread AQs
Stainless-Generated-From: 50cfa58b8547d1cd99da2b22d9081517d3cbbf92
2026-07-15 18:50:28 +00:00
stlc-bot d53da42ffa feat: add run rule evaluator tracing flag [LSO-3323]
Stainless-Generated-From: df568b7188e650a63d7521cb0b0321fc4332db75
2026-07-15 17:01:02 +00:00
stlc-bot cbc00807ee feat(engine): issue lifecycle backend + webhook status filter (2/8)
Stainless-Generated-From: c6ff8a64c58e6a3d43bf068dd92639cc39e2aa0b
2026-07-15 16:42:11 +00:00
KiewanVillatel 2cf0ff6e08 release: 0.1.0-beta.15 (#186) 2026-07-15 10:03:57 +02:00
github-actions[bot] 8e2a0583bf release: 0.1.0-beta.15 2026-07-15 07:53:08 +00:00
KiewanVillatel 63ca829e51 Release SDK updates (#185) 2026-07-15 09:52:50 +02:00
stlc-bot d35ada22c7 chore: update smithdb proto generated code
Stainless-Generated-From: 2945c07633971e09ac433ae6fac86ff1d3695ed1
2026-07-14 23:17:38 +00:00
stlc-bot 2c9704f3c0 feat(context-hub): fire commit webhook and deliver async
Stainless-Generated-From: c28c9c538349263a012361131369c1a2892b246b
2026-07-14 17:46:20 +00:00
stlc-bot b2ed882d0e feat: add July 2026 startup and premier plan tiers
Stainless-Generated-From: 5cd04e44b56fb36e6ec5b7d431198df237532e00
2026-07-14 17:04:24 +00:00
stlc-bot 99fe7dbe33 feat(sandboxes): add preserve_memory_on_stop create attribute
Stainless-Generated-From: c005594f452b7d91aad0d624bc9ebbbef0d8d692
2026-07-14 12:58:43 +00:00
KiewanVillatel c3764a059c release: 0.1.0-beta.14 (#184) 2026-07-14 11:37:58 +02:00
github-actions[bot] 3f086d2302 release: 0.1.0-beta.14 2026-07-14 09:30:30 +00:00
KiewanVillatel ca210b2c66 Release SDK updates (#183) 2026-07-14 11:30:12 +02:00
John Kennedy e639046090 fix: patch remaining Java security alerts (#38)
## Summary

Patch the remaining open Java/Maven Dependabot alerts on the Stainless
next branch.

## Changes

- Upgrade Jackson modules from 2.22.0 to 2.22.1.
- Pin vulnerable test dependencies to patched versions:
  - Bouncy Castle modules: 1.84
  - Log4j Core: 2.25.4
  - Plexus Utils: 4.0.3
  - Commons Lang: 3.18.0
- Use explicit versions for the test dependencies so the dependency
graph recognizes the fixes.

## Security Alerts

Resolves the affected alerts for Jackson Databind/Core, Bouncy Castle,
Log4j Core, Plexus Utils, and Commons Lang, including:

- GHSA-5jmj-h7xm-6q6v
- GHSA-5hh8-q8hv-fr38
- GHSA-9fxm-vc8v-hj55
- GHSA-hgj6-7826-r7m5
- GHSA-rmj7-2vxq-3g9f
- GHSA-j3rv-43j4-c7qm
- GHSA-rcqc-6cw3-h962
- GHSA-cj8j-37rh-8475
- GHSA-c3fc-8qff-9hwx
- GHSA-wg6q-6289-32hp
- GHSA-445c-vh5m-36rj
- GHSA-6hg6-v5c8-fphq
- GHSA-3pxv-7cmr-fjr4
- GHSA-vc5p-v9hr-52mj
- GHSA-6fmv-xxpf-w3cw
- GHSA-72hv-8253-57qq

## Verification

- Core test suite passed.
- Kotlin formatting passed.
- Gradle dependency resolution confirms Jackson Databind 2.22.1.
- Diff check passed.
2026-07-14 01:56:47 -07:00
stlc-bot 1893f83cf2 fix(smith-sdks): name thread stats response model ThreadStats
Stainless-Generated-From: 08528562024b8bc13c762ccb8a943824136e51ab
2026-07-14 08:05:19 +00:00
stlc-bot 63bde3c6e4 fix(smith-go): default time bounds for POST /v2/threads/query
Stainless-Generated-From: 0fe434729c46373eaa6f2c4fcba16fa6b674e9c6
2026-07-13 16:27:40 +00:00
stlc-bot 9cfd26bef9 refactor(sandboxes): make context hub sync a generic mount
Stainless-Generated-From: 8d75a97626cfea68319b3656b34756d2a0100b30
2026-07-12 22:00:12 +00:00
stlc-bot 17551decf7 feat(evaluators): expose is_managed on Go evaluator reads [LSE-2532]
Stainless-Generated-From: 4fdcf56badac7a5800d4d83afc3fdbde1fe3d09e
2026-07-10 19:00:10 +00:00
stlc-bot 0a29041897 feat(dashboards): convert prebuilts to v2 charts on clone [LSO-3306, LSO-3322]
Stainless-Generated-From: 9a6d3abdfd5cd0109e650ac59f56cbd95ff275f2
2026-07-10 16:03:05 +00:00
stlc-bot 12185beb4c feat: forward filter param to thread stats API
Stainless-Generated-From: b4b2f4867f918b220c363931b43145dbeeea5b07
2026-07-09 22:28:22 +00:00
stlc-bot 18d7867206 feat(sdk): wire annotation-queue runs/by-key into stainless [LSAQ-141]
Stainless-Generated-From: a2b46fa9a70c37b435ed42d063f79de82478c6ed
2026-07-09 21:49:37 +00:00
KiewanVillatel a80e55d510 release: 0.1.0-beta.13 (#182) 2026-07-08 22:19:49 +02:00
github-actions[bot] b8f033e3d9 release: 0.1.0-beta.13 2026-07-08 20:10:44 +00:00
KiewanVillatel 38646cb1fa fix: re-emit commits missed by release-please due to timestamp ordering (#181)
refactor(sandboxes): resolve default sandbox via snapshot tag instead of blueprint

feat(backend): extend_trace_retention field for annotation queue adds

feat: Adds attachment support to Java multipart requests (#33)
2026-07-08 22:10:26 +02:00
stlc-bot 59d3e33e71 docs(runs): document that limit does not apply when querying by trace ID
Stainless-Generated-From: d8187437e161077c4005caca7dc16759de02a5e0
2026-07-08 18:40:44 +00:00
Kiewan Villatel c35c92289b Fix release please 2026-07-08 20:17:12 +02:00
Jacob Lee c22f69aac3 Release SDK updates (#180) 2026-07-08 09:26:12 -07:00
Quentin Brosse 57f86d2e60 release: 0.1.0-beta.12 (#178) 2026-07-08 14:43:20 +02:00
github-actions[bot] 141e3e5603 release: 0.1.0-beta.12 2026-07-08 12:01:47 +00:00
Quentin Brosse 5d5112753c Release SDK updates (#179) 2026-07-08 14:01:25 +02:00
Jacob Lee 13b36e3f11 feat: Adds attachment support to Java multipart requests (#33)
Adds multipart run attachment support to the Java SDK.

- Added `RunAttachment` for binary run attachments.
- Added per-run attachments on `Run`.
- Added direct multipart ingest APIs:
  - `client.runs().multipartIngest(createRuns, updateRuns)`
  - async/raw response variants
- Multipart serialization now sends:
  - run payloads as `post.{runId}` / `patch.{runId}`
  - large fields separately, e.g. `post.{runId}.inputs`
  - attachments as `attachment.{runId}.{attachmentName}`
- `InputStream` attachments are treated as non-repeatable so retry
behavior is safe.

Java:
```java
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.models.runs.Run;
import com.langchain.smith.models.runs.RunAttachment;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;

public class MultipartRunAttachmentExample {
    public static void main(String[] args) {
        var client = LangsmithOkHttpClient.builder()
            .apiKey(System.getenv("LANGSMITH_API_KEY"))
            .build();

        var runId = UUID.randomUUID().toString();

        var run = Run.builder()
            .id(runId)
            .traceId(runId)
            .dottedOrder("20250101T000000000Z" + runId)
            .name("run-with-attachment")
            .runType(Run.RunType.CHAIN)
            .putAttachment(
                "input-file",
                RunAttachment.of(
                    "text/plain",
                    "hello from an attachment".getBytes(StandardCharsets.UTF_8)
                )
            )
            .build();

        client.runs().multipartIngest(List.of(run), List.of());

        client.close();
    }
}
```

Kotlin:

```kotlin
var runId = UUID.randomUUID().toString();

var run = Run.builder()
    .id(runId)
    .traceId(runId)
    .dottedOrder("20250101T000000000Z" + runId)
    .name("run-with-attachment")
    .runType(Run.RunType.CHAIN)
    .putAttachment(
        "input-file",
        RunAttachment.of(
            "text/plain",
            "hello from an attachment".getBytes(StandardCharsets.UTF_8)
        )
    )
    .build();

client.runs().multipartIngest(List.of(run), List.of());
```
2026-07-07 15:56:59 -07:00
stlc-bot fcc7ab733b feat(backend): extend_trace_retention field for annotation queue adds
Stainless-Generated-From: 6e5f8b8939ccbe3b430dcd2e24d162f1a2a9cd1c
2026-07-07 17:54:22 +00:00
stlc-bot a07babf48b refactor(sandboxes): resolve default sandbox via snapshot tag instead of blueprint
Stainless-Generated-From: 543ad09db4e939e4c6fac45a471bdbf33106b7f5
2026-07-07 17:32:58 +00:00
KiewanVillatel fdba8e6123 fix: remove duplicate threads/traces declarations in client impls (#35)
## Summary

- Removes duplicate `private val threads` and `private val traces`
property declarations in `LangsmithClientImpl` and
`LangsmithClientAsyncImpl`
- Removes duplicate `override fun threads()` and `override fun traces()`
method overrides in both classes
- The commit `feat(smith-sdks): expose threads and traces v2 endpoints
in public SDK` accidentally added each declaration twice, causing Kotlin
compiler error: `Conflicting declarations`

Fixes the build failure in [PR
#34](https://github.com/langchain-ai/langsmith-java-staging/pull/34).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 14:25:40 +02:00
KiewanVillatel bf2443da84 Release SDK updates (#176) 2026-07-07 13:28:27 +02:00
stlc-bot 3bc6173328 feat(smith-sdks): expose threads and traces v2 endpoints in public SDK
Stainless-Generated-From: 2c83167dfb83a332ca7f2df2729a3a69f237b0f0
2026-07-07 10:30:49 +00:00
stlc-bot d71cff9f59 feat(stainless)!: skip sessions resource for python and typescript
Stainless-Generated-From: 43e94bbe9420618dc2c99ff41a91606f550319b6
2026-07-07 10:16:41 +00:00
stlc-bot e2fc8a84dd Build SDK
Stainless-Generated-From: 105d7387fdada55b829c002388f740a5a8a88f39
2026-07-06 18:50:20 +00:00
stlc-bot 8e17124b68 feat(managed-eval): expose is_managed_evaluator on run-rule read/write paths
Stainless-Generated-From: c961e1af0cc0c71a209a4948f8fd37d4d2c86149
2026-07-06 17:58:20 +00:00
stlc-bot 6ab6fa281f chore(stainless): skip all datasets endpoints in Python/TS SDKs except experiment-runs
Stainless-Generated-From: c8dbb4a59b1515fe90aa6ea4868afc57b24565c4
2026-07-06 17:42:51 +00:00
stlc-bot 585b12dd26 fix(smith-sdks): remove dead retrieve_thread_preview SDK mapping
Stainless-Generated-From: a18af293d2eef2eab3c41770c9ff79c56e5a29c1
2026-07-06 17:04:09 +00:00
stlc-bot 4513b3ffb0 fix(smith-sdks): rename dataset runs POST methods from create to query
Stainless-Generated-From: 9c0c2fbf221fc0ca137a78a5d855f1c52fb0e89d
2026-07-05 23:51:35 +00:00
KiewanVillatel b00f843bc8 ci: retrigger next -> main promotion when checks finish (#29)
## Summary
- Approving the `next -> main` PR before CI checks finish never
re-dispatched the promotion once checks passed, since
`pull_request_review` only fires once on submission.
- Adds a `check_suite: [completed]` trigger to
`stlc-promote-to-main.yml`. On that event, a new gate step looks up the
open `next -> main` PR and checks `reviewDecision == APPROVED` via `gh
pr view`; only then does it proceed to dispatch the same
`git-ff-only-merge` workflow as before.
- Existing `pull_request_review` trigger behavior is unchanged.

## Test plan
- [ ] Approve a `next -> main` PR while checks are still running,
confirm the promotion workflow fires once checks complete successfully.
- [ ] Confirm approving after checks already passed still dispatches
promotion immediately (existing path).
- [ ] Confirm no dispatch happens if checks complete but the PR isn't
approved yet.
2026-07-03 10:40:40 +02:00
KiewanVillatel 5b3f2edfce release: 0.1.0-beta.11 (#174) 2026-07-02 20:26:47 +02:00
github-actions[bot] f6ea8673f6 release: 0.1.0-beta.11 2026-07-02 17:35:22 +00:00
KiewanVillatel 539f2893ea Release SDK updates (#175) 2026-07-02 19:35:00 +02:00
stlc-bot e3b478437e fix: restore title/additionalProperties on v2 RunResponse JSON fields for Stainless
Stainless-Generated-From: 945d660b43c31978b891013d15740bacec141f9b
2026-07-02 15:56:31 +00:00
KiewanVillatel c01f895e57 Release SDK updates (#173) 2026-07-02 14:00:48 +02:00
John Kennedy 6b52583c7c Bump vulnerable dependency versions (#27)
## Summary
- bump Jackson, Spring Boot example, WireMock/Jetty test dependencies,
and vulnerable transitive constraints
- update Spring Boot 3 annotation import and dependency security notes

## Testing
- mise x java@temurin-21.0.11+10.0.LTS -- sfw ./gradlew test
2026-07-02 09:28:27 +02:00
sineha-mani 384c001825 feat(evaluation): remaining python-style evaluate entry points (#18)
## Summary
This change adds the remaining Python-style evaluate entry points on top
of #17.

Feature | New in #18?
-- | --
EvaluateTarget (Runnable-style target) | 
evaluate(client, EvaluateTarget, params) | 
evaluateExisting() / evaluateExistingAsync() | 
evaluate(client, experimentId, params) convenience overload | 
evaluateComparative() | 
evaluate(client, Pair(expA, expB), params) convenience overload | 
ComparativeEvaluator / comparativeEvaluator() | 
EvaluateExistingParams | 
EvaluateComparativeParams | 
ComparativeExperimentResults | 

References
[LSDK-241](https://linear.app/langchain/issue/LSDK-241/add-evaluate-to-langsmith-java-sdk-example-release)

<img width="1173" height="508" alt="Screenshot 2026-06-25 at 3 01 38 PM"
src="https://github.com/user-attachments/assets/a86df925-3657-4ad2-ba21-e250b9f5b4b7"
/>

## Test plan
- [x] `./gradlew :langsmith-java-core:test --tests
"com.langchain.smith.evaluation.*"` passes
- [X] Ran locally with a script

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:27:36 -07:00
stlc-bot a00b1f18ca refactor!: disallow run_count session sort
Stainless-Generated-From: 8c7f6e5f06da792243633325c29fe1a621869ce0
2026-07-01 21:51:05 +00:00
stlc-bot 48b3b08984 ci(stlc): show a clear message when SDK preview build hits a code conflict
Stainless-Generated-From: 8ddeeb1a988a3c864df094292e3a925f3bcccea6
2026-07-01 16:36:24 +00:00
KiewanVillatel 7a1922239b release: 0.1.0-beta.10 (#172) 2026-07-01 10:40:36 +02:00
github-actions[bot] 8b752b109b release: 0.1.0-beta.10 2026-07-01 08:31:15 +00:00
KiewanVillatel 636b67ce06 Release SDK updates (#171) 2026-07-01 10:30:56 +02:00
stlc-bot 2d4900b16b chore(examples): dual-write source run lookup coordinates in Python
Stainless-Generated-From: e59106795c2f894f41350588bd5052cba69938f9
2026-06-30 23:25:18 +00:00
KiewanVillatel 4d45358e0e fix: update hand-written code to use RunIngest after stainless rename (#25)
## Summary

- Updates all hand-written source files to use `RunIngest` instead of
`Run` following the stainless rename in `chore(stainless): rename run
models in runs resource`
- Applies ktfmt formatting to changed files
- Updates test files and example module that were also broken by the
rename

This is a cherry-pick of the fixes from [PR
#24](https://github.com/langchain-ai/langsmith-java-staging/pull/24)
targeting `next` so that the `next → main` promotion PR (#22) can build
successfully.

## Test plan

- [x] `./scripts/build` passes locally
- [x] CI passes on this PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 13:41:58 -07:00
KiewanVillatel 10a1f9bcbb fix(ci): add GH_REPO to release-please trigger publish step (#24)
## Summary

- The `release-please.yml` workflow's "Trigger publish" step calls `gh
workflow run` but the workflow has no `actions/checkout` step, so `gh`
cannot detect the repository from git context and fails with: `fatal:
not a git repository`
- This caused `v0.1.0-beta.9` (and `beta.8`) publish to not auto-trigger
— `beta.8` required a manual `workflow_dispatch`, and `beta.9` was never
published
- The retry of the release-please run (which GitHub auto-triggered) then
skipped the step entirely because `release_created` was no longer set
(the release already existed)
- Fix: add `GH_REPO: ${{ github.repository }}` so `gh` CLI knows the
target repo without needing a checkout

## Root cause trace

1. PR merged → release-please action ran, created the release, set
`release_created: true`
2. "Trigger publish" step executed `gh workflow run publish-sonatype.yml
--ref <tag>` → **failed** with `fatal: not a git repository`
3. Workflow auto-retried ~10.5 hours later → release-please found the
release already exists → `release_created` not set → "Trigger publish"
**skipped**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 13:38:11 -07:00
stlc-bot 2f0ce40b86 chore(stainless): rename run models in runs resource
Stainless-Generated-From: e622f9e97b62df51f4dee52b289ed6857f615f26
2026-06-30 13:05:37 +00:00
Kiewan Villatel bbd15dab2c Build SDK
Stainless-Generated-From: 36442af6b17ea627fed196a08ea9ab79b9570abb
2026-06-30 14:27:33 +02:00
KiewanVillatel 2c08fe0ecb ci: prevent modification of Stainless-generated files on next branch (#23)
## Summary

- Adds a new GitHub Actions workflow
(`.github/workflows/check-stainless-file-modification.yml`) that runs on
every PR targeting the `next` branch.
- The workflow scans all changed files and fails if any contains the
header comment `File generated from our OpenAPI spec by Stainless.`
- When the check fails, it clearly guides the author toward one of three
remedies:
  1. Refactor to avoid touching the generated file
  2. Use Stainless configuration (OpenAPI extensions or transforms)
3. Add the label `skip-stainless-generated-file-modification-check` as a
last resort to bypass the check
- Re-triggers on label add/remove so adding the skip label immediately
clears the failure.
- Scoped to this repository only (won't run on forks).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 14:00:25 +02:00
sineha-mani 7d9a2fafd4 feat(evaluation): add core evaluate() param parity (#17)
**Description**
This change makes Java evaluate() faster and adds the main Python
evaluate() options that were still missing.

By default, nothing changes: examples run one at a time, in order, and
results upload to LangSmith as a new experiment.

What’s new

maxConcurrency — run multiple examples in parallel (default 0 =
sequential). Helps when each row is slow (LLM calls, network I/O).
numRepetitions — evaluate each row multiple times in one experiment.
Summary evaluators — dataset-level scores (e.g. precision/recall) after
all rows finish.
experimentId — add runs to an existing experiment instead of creating a
new one.
uploadResults(false) — local-only evaluation, no session or feedback
upload (dry runs, CI).
errorHandling — LOG (default) or IGNORE when the target fails on a row.
evaluateAsync() — non-blocking evaluation via CompletableFuture.


Feature | New in #17?
-- | --
maxConcurrency | 
summaryEvaluators | 
numRepetitions | 
experimentId (extend existing) | 
uploadResults | 
errorHandling | 
evaluateAsync() / blocking | 

<img width="1190" height="452" alt="Screenshot 2026-06-25 at 3 10 50 PM"
src="https://github.com/user-attachments/assets/1eb91d1d-abc9-4d9f-bb72-4f42e5aacef6"
/>

References
[LSDK-241](https://linear.app/langchain/issue/LSDK-241/add-evaluate-to-langsmith-java-sdk-example-release)

Sequential (max concurrency = 0)
<img width="1197" height="366" alt="Screenshot 2026-06-25 at 12 36
48 PM"
src="https://github.com/user-attachments/assets/f363b18e-059e-48da-8088-fe1d00e88d99"
/>

Parallel (max concurrency = 3)
<img width="1186" height="354" alt="Screenshot 2026-06-25 at 12 40
48 PM"
src="https://github.com/user-attachments/assets/05701745-27b2-4bd2-a2a5-5e30531b88c7"
/>

**Test Plan**
[X] ./gradlew :langsmith-java-core:test --tests
"com.langchain.smith.evaluation.EvaluateRunnerTest"
[X] Manual smoke: run evaluate() on a dataset with maxConcurrency(4) and
confirm runs appear in LangSmith with feedback scores

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 17:43:36 -05:00
github-actions[bot] ab6c153520 release: 0.1.0-beta.9 (#167)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 23:01:35 +02:00
KiewanVillatel 34f1c52fc1 Release SDK updates (#166)
fix: trigger publish-sonatype workflow after release-please creates a
release (#13)

feat(sandboxes): expose registry CRUD and generate Python/TypeScript SDK
clients

fix(backend)!: accept float value for percentile metric [LS-000]

feat(api)!: document v2 experiment-runs and de-publicize legacy dataset
endpoints

feat(sandbox-host): per-VM cgroups for CPU bursting (2x), fractional
vCPU, and sustained-burst limiting

feat: add GET endpoint to retrieve single workspace by id [closes
ENT-508]

feat(evaluation): add Java evaluate orchestrator (#8)
2026-06-29 22:47:46 +02:00
sineha-mani d1420efd7a feat(evaluation): add Java evaluate orchestrator (#8)
**Description**
Adds the Java evaluate() orchestrator on top of PR 1 (foundation). We
can now run an app against a dataset, trace each prediction, score it
with evaluators, and upload feedback as a LangSmith experiment. We are
aiming for parity with Python's client.evaluate().

End-to-end flow:

- Load test cases from a dataset (by name or ID), or pass in a list
directly
- Start an experiment in LangSmith to group the run
- For each test case: run your app, score the result, save feedback to
LangSmith
- Return a summary with each row’s run and scores


File | Purpose
-- | --
Evaluate.kt | Public entry point — Evaluation.evaluate(client, target,
params) (+ Java Function overload)
EvaluateParams.kt | Builder config: dataset (name/ID or examples),
evaluators, experiment name/prefix, description, metadata
EvaluateRunner.kt | Core loop: resolve dataset → list examples → create
session → trace app per row → run evaluators → collect results.
Resolves dataset name → UUID before listing examples (required by local
API)
EvaluateFeedback.kt | Maps EvaluationResult → FeedbackCreateSchema and
posts via client.feedback().create() with ModelFeedbackSource
ExperimentResults.kt | Return types: ExperimentResults (experiment
metadata + rows) and ExperimentResultRow (run, example, scores)
EvaluateRunnerTest.kt | Unit test with mocked client: tracing, scoring,
and feedback posting
AutoBatchQueueTest.kt (fix) | Fixes flaky CI test — waits on latch
instead of Thread.sleep(100) for async batch flush

**Follow Up** 
Support parallel

**Test Plan**
Verified locally against local LangSmith via the new example.


<img width="859" height="401" alt="Screenshot 2026-06-23 at 4 38 48 PM"
src="https://github.com/user-attachments/assets/862c5a9d-d198-4c83-a6b5-8c0751086ff1"
/>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 14:12:36 -05:00
stlc-bot cdbd36936f feat: add GET endpoint to retrieve single workspace by id [closes ENT-508]
Stainless-Generated-From: c6359ba3a207af8363ad5a4c2209270fccf84acb
2026-06-29 15:55:00 +00:00
stlc-bot 49bf68d747 feat(sandbox-host): per-VM cgroups for CPU bursting (2x), fractional vCPU, and sustained-burst limiting
Stainless-Generated-From: 81dece96e6afea57d658b0401e421c7bc3904003
2026-06-28 19:11:51 +00:00
stlc-bot 387526fd3a feat(api)!: document v2 experiment-runs and de-publicize legacy dataset endpoints
Stainless-Generated-From: 30e72d154a104230281840db94c0c4553ecdb293
2026-06-26 22:34:17 +00:00
stlc-bot e88560861c fix(backend)!: accept float value for percentile metric [LS-000]
Stainless-Generated-From: daeffb8cae1466669df7ea9d06949df815082dcf
2026-06-26 01:31:05 +00:00
stlc-bot cfed196979 feat(sandboxes): expose registry CRUD and generate Python/TypeScript SDK clients
Stainless-Generated-From: d36ac9040eeab46766b7560ab5afebcf988add42
2026-06-26 00:11:01 +00:00
KiewanVillatel 70edc4514c fix: trigger publish-sonatype workflow after release-please creates a release (#13)
## Problem

`publish-sonatype.yml` is triggered by `release: types: [published]`,
but the release created by the `release-please` action uses
`GITHUB_TOKEN` (`github-actions[bot]`). GitHub explicitly prevents
events created by `GITHUB_TOKEN` from triggering other workflows, so the
publish step was silently skipped for v0.1.0-beta.8.

Prior releases (beta.7 and earlier) worked because they were created by
`stainless-app[bot]` (a GitHub App token), which does not have this
restriction.

## Fix

Add a `gh workflow run` step inside the release-please job that fires
`publish-sonatype.yml` explicitly after the release is created,
conditioned on `steps.release.outputs.release_created`.

## Test plan

- [ ] Merge this PR and confirm the next release-please PR merge
triggers a `publish-sonatype.yml` run
- [ ] Manually trigger `publish-sonatype.yml` via `workflow_dispatch`
for v0.1.0-beta.8 to publish the missed release

🤖 Generated with [Claude Code](https://claude.ai/claude-code)
2026-06-25 11:47:21 +02:00
github-actions[bot] 6f1ae304fa release: 0.1.0-beta.8 (#165)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-25 11:17:49 +02:00
KiewanVillatel 84e02f96b4 Merge pull request #164 from langchain-ai/stainless/release
Release SDK updates
2026-06-25 10:57:06 +02:00
KiewanVillatel 65b5b9ae40 chore: add release-please workflow (#11)
## Description

Adds `.github/workflows/release-please.yml` to automate release PR
creation whenever commits land on `main`.

Closes:
https://linear.app/langchain/issue/LIN-400/chore-add-release-please-workflow-to-langsmith-java

- Adds the release-please workflow, guarded by `github.repository ==
'langchain-ai/langsmith-java'` so it only fires on the production repo.
- The existing `release-please-config.json` (release-type `simple`,
Gradle extra files) and `.release-please-manifest.json` (current version
`0.1.0-beta.7`) are already in place — no additional config needed.
- `publish-sonatype.yml` will continue to fire automatically via its
`release: published` trigger once release-please merges a release PR and
creates the GitHub Release.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 09:55:02 +02:00
sineha-mani a82b5c88fc feat(evaluation): Java evaluate() primitives (#7)
## Description

First PR in the Java `evaluate()` rollout based on the high priority
request by PayPal.

Python evaluate() needs three things: (1) run each dataset example
through a traced target, (2) score runs with evaluators, (3) post
feedback to LangSmith. This branch adds the shared primitives those
steps depend on — mirroring Python’s RunEvaluator, EvaluationResult,
evaluator coercion, and experiment/example linkage on runs.

We use generated types when possible. 

References
[LSDK-239](https://linear.app/langchain/issue/LSDK-239/add-evaluate-to-langsmith-java-sdk-tracing-types)

**_No breaking changes expected. All evaluate primitives are new public
surface (RunEvaluator, runEvaluator, EvaluationResult, etc.). The full
Client.evaluate() orchestrator is not included yet._**

What each file is doing:

**Evaluation types (com.langchain.smith.evaluation)**

EvaluationResult.kt — Defines what a single score looks like: name,
score, comment, and related fields. Uses generated
FeedbackCreateSchema.FeedbackConfig when a judge specifies how a score
should be interpreted.
EvaluationResults.kt — Holds multiple scores when one judge returns more
than one grade.
RunEvaluator.kt — The contract for something that judges one attempt
against the expected answer.
DynamicRunEvaluator.kt — Turns a custom judge function into something
the SDK can call consistently. Supports three input shapes (run +
example, outputs vs reference, inputs + outputs + reference) and
Java-friendly interfaces (RunExampleEvaluator,
OutputReferenceEvaluator).
EvaluatorUtils.kt — Unpacks run/example data into plain maps and
normalizes whatever a judge returns (bool, number, map, list, etc.) into
structured EvaluationResult(s).

**Tracing updates**

Traceable.kt — Lets traced runs remember which dataset row and
experiment they belong to (referenceExampleId, sessionId).
RunTree.kt — Carries that link through the run record sent to LangSmith.

**Tests**

EvaluationResultTest.kt — Verifies score objects and generated feedback
config are built correctly.
DynamicRunEvaluatorTest.kt — Verifies custom judges work in the
different ways users might write them.
TraceableEvaluateContextTest.kt — Verifies predictions are tagged with
the right example and experiment when traced.

## What's intentionally out of scope

- No `evaluate()` entry point yet (PR 2)
- No experiment session management or feedback upload loop
- No examples / release docs (PR 3)

## Test plan

- [X] `./gradlew :langsmith-java-core:test --tests
"com.langchain.smith.evaluation.*" --rerun`
- [X] `./gradlew :langsmith-java-core:test --tests
"com.langchain.smith.tracing.TraceableEvaluateContextTest" --rerun`
- [X] `./gradlew :langsmith-java-core:formatKotlin lintKotlin`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 16:34:47 -07:00
KiewanVillatel e097147ff3 Merge pull request #163 from langchain-ai/stainless/release
Release SDK updates
2026-06-24 15:50:26 +02:00
KiewanVillatel ba419de13e fix(tests): remove duplicate retrieve() method in RunServiceTest (#10)
## Summary

- Removes a duplicate `fun retrieve()` method from `RunServiceTest.kt`
that caused a `Conflicting overloads` Kotlin compile error, breaking
`gradlew build`

## Root cause

**Stainless custom-code boundary mis-detection, not a codegen bug.**

Before commit `48c283c` (`feat(runs/v2): expose runs v2 endpoints
publicly`), the file already had one `fun retrieve()` at line ~453 — a
**previously generated** test that happened to sit immediately after the
hand-written batch tests.

When `48c283c` ran Stainless to add `retrieveV1`, `retrieveV2`, and the
`retrieve` alias, Stainless had to merge the new generated output with
the existing file. It:
1. Correctly preserved the hand-written batch tests as custom code
2. **Incorrectly** treated the old `retrieve()` test as custom code too
(it was adjacent to the hand-written section), freezing it in place
3. Then emitted the newly generated `retrieve()` alias test at its
correct config-order position → **duplicate**

Confirmed by running `stlc generate` without custom code: the raw
generated output has exactly one `retrieve()`, correctly positioned
after `retrieveV1`/`retrieveV2`.

The removed occurrence (line 454) was the stale generated test
mis-classified as custom code during the `48c283c` merge. The kept
occurrence (now ~line 700) is the freshly generated alias test at its
proper position.

## Test plan

- [x] CI passes on this branch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:20:36 +02:00
stlc-bot 17bb73a006 chore(smith-sdks): update Java SDK stainless custom-code tracking file
Stainless-Generated-From: 2062f5beb2ee44bbc4337d84567cb8aafff7986a
2026-06-24 10:26:05 +00:00
Kiewan Villatel 48c283c7a7 feat(runs/v2): expose runs v2 endpoints publicly with stainless config
Stainless-Generated-From: 2698695f4f6a7b8162cd28e9066480ff3001ed79
2026-06-24 11:14:42 +02:00
KiewanVillatel 3c36fa9f58 Merge pull request #162 from langchain-ai/stainless/release
Release SDK updates
2026-06-24 10:48:33 +02:00
stlc-bot 49039f2770 feat(stats): add include_details param to /runs/stats for backwards compat
Stainless-Generated-From: ac7d88b132b0ec8c2fb75b773f5748a19a8ae478
2026-06-23 18:23:48 +00:00
stlc-bot 54d105ef91 fix(sandboxes)!: default S3 mount endpoint
Stainless-Generated-From: 3761a9c27af396f6b87676441431bbc645bfa0a1
2026-06-23 12:46:33 +00:00
stlc-bot b963cab6c6 refactor: rename field to match prev trace retention fields
Stainless-Generated-From: dd221815bcfc4a9bb610709484d60c2ca7c69e11
2026-06-22 19:45:43 +00:00
stlc-bot 734a8285cd feat(sandboxes): move mounts under mount_config
Stainless-Generated-From: 474ffec1d88a86c444dfcd8cd93176a196650e30
2026-06-19 11:12:28 +00:00
KiewanVillatel 152169f4e1 ci: add workflow to promote stlc/from-prod → next on approval (#6)
## Summary

- Adds `stlc-promote-from-prod-to-next.yml`: on an approving review of a
`stlc/from-prod → next` PR, dispatches `git-ff-only-merge.yml` in
langchainplus with `src_branch=stlc/from-prod` and `target_branch=next`
- Updates `stlc-promote-to-main.yml` to dispatch `git-ff-only-merge.yml`
(renamed from `stlc_promote.yml`) and use the new single-repo `repo`
field

Depends on: https://github.com/langchain-ai/langchainplus/pull/27932

## Test plan

- [ ] Approve the `stlc/from-prod → next` PR in this repo and verify the
workflow dispatches and fast-forwards `next` to `stlc/from-prod`
- [ ] Confirm existing `next → main` promotion still works via
`stlc-promote-to-main.yml`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 09:03:58 +02:00
stlc-bot ac8da0f77c fix(sandboxes): use built-in gcp proxy host matching
Stainless-Generated-From: 98e1d224d4e639c875df7395a46cdba8c84961f0
2026-06-18 13:41:33 +00:00
stlc-bot 7f24878418 feat(group-stats): back thread stats with SmithDB
Stainless-Generated-From: 8bed75a505269e08bfcbd524db27819bd139e83c
2026-06-16 18:58:33 +00:00
stlc-bot 3c218e98bc feat(run-rules): per-action trace-retention control for automations [LSO-2749]
Stainless-Generated-From: 820e434600b1e6b444e26fb94d7d35cd4ba52f25
2026-06-16 17:31:22 +00:00
stlc-bot 2ddc687610 feat(sandboxes)!: add ArtifactFS git mounts
Stainless-Generated-From: 699e98adc655572eead9632f161868b2d938f4e6
2026-06-16 12:13:59 +00:00
KiewanVillatel f00fef565e chore: add stlc sync and promotion workflows (#5)
Replicates the four STLC lifecycle workflows from
`langsmith-go-staging`, adapted for the Java SDK.

## Workflows added

| File | Trigger | Purpose |
|------|---------|---------|
| `stlc-sync-from-production.yml` | Cron (every 6h) + manual | Opens a
PR syncing `langsmith-java` (production) `main` back onto staging `next`
after a release |
| `stlc-promote-to-production.yml` | Push to `main` | Pushes staging
`main` to `stainless/release` on `langsmith-java` and opens/updates the
release PR |
| `stlc-promote-to-main.yml` | PR review submitted | When the `next →
main` PR is approved, dispatches `stlc_promote.yml` in `langchainplus` |
| `stlc-seal-custom-code.yml` | Push to `next` | Dispatches
`stlc_seal_custom_code.yml` in `langchainplus` scoped to
`langsmith-java-staging` |

All workflow filenames use hyphens and the `stlc: ...` name prefix
consistently. Logic is otherwise identical to the Go staging repo with
mechanical `go` → `java` substitutions.
2026-06-15 15:08:20 +02:00
stlc-bot 87e3acbb7b feat(sandboxes)!: add gcs bucket mounts
Stainless-Generated-From: d653643420667367faf788d3093269783eb0b9e4
2026-06-15 12:42:39 +00:00
stlc-bot 280a37489d feat(abac): add tag_value_ids to dataset creation endpoints
Stainless-Generated-From: 78633c683d6095befbc2e23f75c6c6e1aaf05084
2026-06-12 20:42:52 +00:00
stlc-bot 77f12e3a6e feat: end-to-end OAuth bearer for Playground [part of ENT-760]
Stainless-Generated-From: aae0770e95c27ab3ffcaae4e98706844a460bdc3
2026-06-12 17:31:19 +00:00
stlc-bot b86e854687 feat(backend): allow retrieving and storing V2 charts in database [LSO-2799]
Stainless-Generated-From: a6e52cd3ade297b9840fb679d25dc80e62a1eece
2026-06-12 16:15:49 +00:00
stlc-bot fd2980ce69 feat(sandboxes): add GCP proxy auth flow
Stainless-Generated-From: e19ec4f409a11b384e2bc5bdb260f025a53186b2
2026-06-12 11:36:58 +00:00
stlc-bot e480cd4104 feat: make online evaluator retention opt-in (backend)
Stainless-Generated-From: 833d850dc17eca03babbdf20bfd08e0b346084b8
2026-06-12 04:52:51 +00:00
stlc-bot 3f3a0c9c30 fix(evaluators): show code evaluator trace counts [LSE-2359]
Stainless-Generated-From: b73e13aeea10d57c3ea8fb62d922c34169736192
2026-06-11 22:17:29 +00:00
stlc-bot 77d19d3241 feat: add online evaluator endpoints to stainless config
Stainless-Generated-From: 6cc6df8d86158cf1cf863f95233a383508ee383f
2026-06-11 16:03:03 +00:00
stlc-bot 5a6bddd241 feat: adding time to first token for experiment metrics
Stainless-Generated-From: 53b0a374d8994730eeb629dd5b89d7e1dc0aa251
2026-06-10 18:07:42 +00:00
stlc-bot 10f1d65063 feat(hub): add include_owners to repos list for Fleet [closes AB-2537]
Stainless-Generated-From: d3f29792ae8ba2102d6583e32ddfe369c77beb43
2026-06-10 01:16:09 +00:00
stlc-bot 79647d1579 feat(sandboxes): snapshot memory from stopped boxes + honor restore_memory in v2 [INF-0000]
Stainless-Generated-From: 6bcb8e63cc367450c7ddf3dd646bf9932d7c49fb
2026-06-09 19:29:44 +00:00
stlc-bot f3ce273a02 feat(abac): add tag_value_ids to prompt creation endpoints [ENT-1176]
Stainless-Generated-From: b130dec878cfb300bf3156c9ef00f4fa57c3533b
2026-06-08 13:50:16 +00:00
stlc-bot bbbcf7bf40 feat(fleet): frontend passes typed fields (closes AB-000)
Stainless-Generated-From: d3df6f0341a9641e0c5353f5f3350b96b7038973
2026-06-08 09:22:40 +00:00
stlc-bot f859585592 feat(abac): add tag_value_ids to tracer session creation
Stainless-Generated-From: e9fdc72fa5c1fa28ec45a7fb08bd4078aaf732f3
2026-06-05 14:27:28 +00:00
stlc-bot 370b940a1c refactor(playground): remove legacy experiment endpoints [LSO-2230]
Stainless-Generated-From: 9953c40859655979614cd3c43c7a0755c45ed45e
2026-06-04 21:29:54 +00:00
Kiewan Villatel ad6daecac9 Revert "Build SDK"
This reverts commit 6b25ea38a8.
2026-06-03 21:20:47 +02:00
Kiewan Villatel 6b25ea38a8 Build SDK
Stainless-Generated-From: 3a0cd98377fb09f5d85dba3ea691f3849ba02263
2026-06-03 21:19:39 +02:00
stlc-bot 8b85e16472 chore(fleet): add param to list threads [closes AB-2522]
Stainless-Generated-From: c8d2efa256195ac10d872571d2e2139e1f7b0ff4
2026-06-02 18:50:54 +00:00
stlc-bot 7cb70fc3e3 feat(sandboxes): proxy runtime routes through platform backend [INF-0000]
Stainless-Generated-From: d30b7b81813354ec1ca1ae1094bbc1107017ac45
2026-06-02 17:23:16 +00:00
stlc-bot cb519f93d4 feat(sandboxes): resolve AWS proxy rule secrets
Stainless-Generated-From: cf35f3ba977200bfaef8361b423147be29b16c81
2026-06-02 12:55:51 +00:00
stlc-bot 6bbe522f0a feat(sandboxes): set AWS proxy compatibility env
Stainless-Generated-From: 9a9800e9de6b6498fe555f10a8961b322dd1ab1a
2026-06-01 14:43:32 +00:00
stlc-bot bb9f973f41 feat(sandboxes): filter lists by creator [INF-1492]
Stainless-Generated-From: 6521e9d00bd87c684594407f5a093ad54a2a0941
2026-06-01 12:11:47 +00:00
stlc-bot da0fc7b507 feat(sandboxes): add sandbox env var support
Stainless-Generated-From: e928201c44a02a9ced8002607677081d66ea4778
2026-06-01 10:17:32 +00:00
Kiewan Villatel 9c85b0cfd1 Build SDK
Stainless-Generated-From: 45836ee9e735d7588029f65d656d810f719b07f5
2026-05-30 14:28:28 +02:00
Kiewan Villatel 25b9aacfda Build SDK
Stainless-Generated-From: f669148e795626b9d422ea415951fdf934d9c9d5
2026-05-28 14:04:14 +02:00
Kiewan Villatel 3bd596cefa Resolve merge conflicts in ci, stats, and example build 2026-05-28 10:52:13 +02:00
Kiewan Villatel 334fe4ef56 Build SDK
Stainless-Generated-From: b3f3c130ed43c672c54694b4121e90305c6c58c7
2026-05-28 10:14:00 +02:00
stainless-app[bot] 744987978d release: 0.1.0-beta.7 (#156)
* chore(internal): renormalize gradlew.bat line endings

* fix(go): avoid panic when http.DefaultTransport is wrapped

defaultHTTPClient performed an unchecked type assertion on
http.DefaultTransport, which panicked for any caller that had wrapped
the global transport (e.g. otelhttp.NewTransport for distributed
tracing). When the assertion fails, fall back to the wrapped
RoundTripper as-is — preserving the caller's wrapping at the cost of
ResponseHeaderTimeout, which is strictly better than panicking.

* feat(api): api update

* docs: clarify forwards compat behavior

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* chore: remove duplicated dokka setup

* codegen metadata

* codegen metadata

* codegen metadata

* perf(client): create one json mapper

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(client): support proxy authentication

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(sdk): add hub directories endpoints to Stainless config

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* fix(go): avoid panic when http.DefaultTransport is wrapped

defaultHTTPClient performed an unchecked type assertion on
http.DefaultTransport, which panicked for any caller that had wrapped
the global transport (e.g. otelhttp.NewTransport for distributed
tracing). When the assertion fails, fall back to the wrapped
RoundTripper as-is — preserving the caller's wrapping at the cost of
ResponseHeaderTimeout, which is strictly better than panicking.

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): codegen related update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* fix: restore dokka-javadoc plugin application to subprojects (#157)

* fix: restore dokka-javadoc plugin application to subprojects

Commit b1af8d8 ("remove duplicated dokka setup") removed
`apply(plugin = "org.jetbrains.dokka-javadoc")` from the subprojects
block, thinking the root-level `plugins {}` declaration covered it.

The root `plugins {}` block applies the plugin to the root project only.
Each subproject needs its own application to register the
`dokkaGeneratePublicationJavadoc` task, which
`langchain.publish.gradle.kts` depends on via
`JavadocJar.Dokka("dokkaGeneratePublicationJavadoc")`. Without it,
configuring any subproject that applies `langchain.publish` fails with:

  Could not create task ':langsmith-java:dokkaJavadocJar'.
  > Task with name 'dokkaGeneratePublicationJavadoc' not found in
    project ':langsmith-java'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: fix double blank lines flagged by ktfmt lint

Remove spurious extra blank lines between `logLevel` and `apiKey` in
three Builder classes. ktfmt (--kotlinlang-style) disallows consecutive
blank lines inside a class body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* release: 0.1.0-beta.7

---------

Co-authored-by: meorphis <eric@stainless.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Kiewan Villatel <kiewan@langchain.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 14:32:52 +00:00
stainless-app[bot] c904c2d278 release: 0.1.0-beta.6 (#154)
* fix: patch security alerts — OTel, Tomcat, Jetty, Logback, Spring, SnakeYAML (#153)

Resolves open Dependabot security alerts across critical+high severity tiers.

Key change: opentelemetry-api/sdk/exporter-otlp 1.32.0 → 1.62.0 (CVE-2026-45292,
published dep). Semconv 1.23.1-alpha → 1.41.1 (stable GA); migrates
ResourceAttributes.SERVICE_* → ServiceAttributes.SERVICE_* (class removed in 1.30+).

Other fixes (test/example scope, non-published):
- Jetty 9.4.57 → 9.4.58.v20250814 (CVE-2025-5115 in http2-common)
- Logback 1.2.13 → 1.5.32 (CVE-2025-11226, CVE-2024-12798, CVE-2024-12801, CVE-2026-1225;
  the 1.2.13 pin was overly conservative — CVE-2023-6481/6378 are fixed since 1.3.12)
- Tomcat 9.0.115 → 9.0.118 in example module (14 alerts, 4 critical)
- Spring 5.3.34 → 5.3.39 + spring-expression constraint (CVE-2024-38809, CVE-2024-38808)
- SnakeYAML 1.31 → 1.32 (CVE-2022-38752, CVE-2022-41854)

Adds SECURITY_FIX_PLAN.md tracking open/stale/no-fix-available alert status.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* release: 0.1.0-beta.6

---------

Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
2026-05-20 11:19:28 -07:00
stainless-app[bot] 1540e83048 release: 0.1.0-beta.5 (#151)
* codegen metadata

* chore(deps): bump the minor-and-patch group across 1 directory with 3 updates (#145)

Bumps the minor-and-patch group with 3 updates in the / directory: [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin), [jvm](https://github.com/JetBrains/kotlin) and [gradle-wrapper](https://github.com/gradle/gradle).


Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.3.0 to 2.3.21
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.0...v2.3.21)

Updates `jvm` from 2.3.0 to 2.3.21
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.0...v2.3.21)

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v9.4.1...v9.5.0)

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 9.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: jvm
  dependency-version: 2.3.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
  dependency-version: 2.3.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump actions/github-script from 8 to 9 (#144)

Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump gradle/actions (#143)

Bumps the minor-and-patch group with 1 update in the / directory: [gradle/actions](https://github.com/gradle/actions).


Updates `gradle/actions` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f...50e97c2cd7a37755bbfafc9c5b7cafaece252f6e)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(sandbox): make snapshot optional (#152)

* feat(sandbox): default create to server runtime

* fix(sandbox): restore generated helper import

* release: 0.1.0-beta.5

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
2026-05-13 22:19:30 +00:00
stainless-app[bot] 513d207b80 release: 0.1.0-beta.4 (#148)
* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* Refresh profile auth before requests (#149)

* Refresh profile auth before requests

* Use profile URL for OAuth refresh

* release: 0.1.0-beta.4

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Mukil Loganathan <mukil@langchain.dev>
2026-05-04 14:44:29 +00:00
stainless-app[bot] 48a0841016 release: 0.1.0-beta.3 (#147)
* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* Add profile loading (#146)

* codegen metadata

* release: 0.1.0-beta.3

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Mukil Loganathan <mukil@langchain.dev>
2026-05-01 14:01:53 +00:00
stainless-app[bot] 090c06bbaa release: 0.1.0-beta.2 (#142)
* codegen metadata

* fix: Fix another build issue (#141)

* release: 0.1.0-beta.2

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
2026-04-30 19:00:56 +00:00
stainless-app[bot] ea5f34e155 release: 0.1.0-beta.1 (#140)
* chore: Fix build (#139)

* Fix build

* Comment

* release: 0.1.0-beta.1

---------

Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
2026-04-30 18:07:06 +00:00
stainless-app[bot] 14634be733 release: 0.1.0-beta.0 (#132)
* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): sandbox apis

add sandbox apis

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* chore(deps): bump the minor-and-patch group across 1 directory with 5 updates (#123)

* chore(deps): bump the minor-and-patch group with 5 updates

Bumps the minor-and-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [org.jetbrains.dokka](https://github.com/Kotlin/dokka) | `2.0.0` | `2.2.0` |
| [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) | `2.0.21` | `2.3.20` |
| [com.vanniktech:gradle-maven-publish-plugin](https://github.com/vanniktech/gradle-maven-publish-plugin) | `0.28.0` | `0.36.0` |
| [jvm](https://github.com/JetBrains/kotlin) | `2.0.21` | `2.3.20` |
| [com.vanniktech.maven.publish](https://github.com/vanniktech/gradle-maven-publish-plugin) | `0.28.0` | `0.36.0` |

Updates `org.jetbrains.dokka` from 2.0.0 to 2.2.0
- [Release notes](https://github.com/Kotlin/dokka/releases)
- [Commits](https://github.com/Kotlin/dokka/compare/v2.0.0...v2.2.0)

Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.0.21 to 2.3.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.0.21...v2.3.20)

Updates `com.vanniktech:gradle-maven-publish-plugin` from 0.28.0 to 0.36.0
- [Release notes](https://github.com/vanniktech/gradle-maven-publish-plugin/releases)
- [Changelog](https://github.com/vanniktech/gradle-maven-publish-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vanniktech/gradle-maven-publish-plugin/compare/0.28.0...0.36.0)

Updates `jvm` from 2.0.21 to 2.3.20
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.0.21...v2.3.20)

Updates `com.vanniktech.maven.publish` from 0.28.0 to 0.36.0
- [Release notes](https://github.com/vanniktech/gradle-maven-publish-plugin/releases)
- [Changelog](https://github.com/vanniktech/gradle-maven-publish-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vanniktech/gradle-maven-publish-plugin/compare/0.28.0...0.36.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.dokka
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
  dependency-version: 2.3.20
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: com.vanniktech:gradle-maven-publish-plugin
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: jvm
  dependency-version: 2.3.20
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: com.vanniktech.maven.publish
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix build failures from dependency bumps

Remove SonatypeHost reference removed in gradle-maven-publish-plugin
0.34.0+ (publishToMavenCentral() defaults to Central Portal).
Bump proguard-gradle 7.6.0 → 7.9.0 to support Kotlin 2.3.x metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert Kotlin to 2.3.0 to match Gradle 9.4.1 embedded version

Kotlin 2.3.20 is not yet supported by CodeQL and conflicts with
the kotlin-dsl plugin's embedded Kotlin 2.3.0. Keeping the Kotlin
version aligned with the Gradle wrapper avoids both issues.

Bump proguard-gradle to 7.8.0 (from 7.6.0) to support Kotlin 2.3.0
metadata. 7.9.0 has a null pointer regression; 7.8.x is the sweet spot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(evaluators): add list evaluators (GET /api/v1/runs/rules)

Exposes `GET /api/v1/runs/rules`.

Will result in `client.evaluators.list()` in the Go SDK.

The endpoint returns online evaluators (internally called "run rules") — automated rules that trigger on incoming runs to apply LLM/code evaluators, add runs to annotation queues, trigger webhooks, etc. The `runs/rules` URL path is an implementation detail; `evaluators` matches the user-facing branding.

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat: support setting headers via env

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat: Add autobatch tracing mode (#134)

* Add autobatch mode

* Fix

* Update signature

* Add async impl

* Fix optin

* Fix

* Fix

* Fix tests

* Add test

* Refactor to be more idiomatic

* Feedback

* Use explicit lock, add to AGENTS.md

* Remove test script

* codegen metadata

* feat: Merge enqueued posts and patches to optimize batching (#135)

* Merge enqueued posts and patches to optimize batching

* Exclude patched inputs by default

* Logs

* Fix import order

* Feedback

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat: Make autobatch queue denote batches by payload size, add aggregation delay (#137)

* Make autobatch queue denote batches by payload size, add aggregation delay

* Fully drain queue when batch delay is hit

* Feedback on locks

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat: Add multipart batching (#138)

* Add multipart batching

* Handle 404s better

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): add workspaces

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* codegen metadata

* codegen metadata

* feat: Add zstd compression to runs service (#136)

* Add zstd compression

* Merge

* Fix

* nits

* Feedback

* Fix build

* codegen metadata

* release: 0.1.0-beta.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
2026-04-30 17:23:03 +00:00
stainless-app[bot] 3220bc56d5 release: 0.1.0-alpha.28 (#130)
* feat(api): api update

* codegen metadata

* Fix publish action (#131)

* release: 0.1.0-alpha.28

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
2026-04-14 18:11:47 +00:00
stainless-app[bot] 9ccfc6a350 release: 0.1.0-alpha.27 (#128)
* codegen metadata

* codegen metadata

* Edong/bump kotlin gradle ver (#127) (#129)

* chore: bumping ver

* chore: add back warning

Co-authored-by: ericdong-langchain <ericdong@langchain.dev>

* release: 0.1.0-alpha.27

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
2026-04-14 00:14:52 +00:00
ericdong-langchain 9f90d52312 Edong/bump kotlin gradle ver (#127)
* chore: bumping ver

* chore: add back warning
2026-04-13 18:30:19 -04:00
stainless-app[bot] 4e86f4ee9e release: 0.1.0-alpha.26 (#125)
* codegen metadata

* feat(api): api update

* chore(deps): bump gradle-wrapper from 8.12 to 9.4.1 (#124)

Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 8.12 to 9.4.1.
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v8.12.0...v9.4.1)

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 9.4.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump gradle/actions (#121)

Bumps [gradle/actions](https://github.com/gradle/actions) from 205054a7257716ec64af10a2e2ff1ac5d3b132db to 39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/205054a7257716ec64af10a2e2ff1ac5d3b132db...39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: 39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump gradle/gradle-build-action (#122)

Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 12318b01111bfa6462c00534ffa998f8b397b979 to ac2d340dc04d9e1113182899e983b5400c17cda1.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/12318b01111bfa6462c00534ffa998f8b397b979...ac2d340dc04d9e1113182899e983b5400c17cda1)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  dependency-version: ac2d340dc04d9e1113182899e983b5400c17cda1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* docs: improve examples

* codegen metadata

* feat: Allow manually specifying parent run tree in traceable config (#126)

* Allow manually specifying parent run tree in traceable config

* Allow passing parent(null)

* Agents.md fixes

* Use singleton in both places

* release: 0.1.0-alpha.26

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
2026-04-13 17:59:16 +00:00
stainless-app[bot] f3d8fdeccf release: 0.1.0-alpha.25 (#109)
* Revert "main to next (#108)" (#110)

This reverts commit 5a8988d3f6.

* Fix/lint and remove method count (#111)

* release: 0.1.0-alpha.24

* fix: lint and remove outdated method count ci test

Made-with: Cursor

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>

* feat: Adds package version resource to build (#100)

* codegen metadata

* codegen metadata

* chore: make `Properties` more resilient to `null`

* chore: drop apache dependency

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): expand imports

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

removed endpoints not in openAPI spec

* codegen metadata

* codegen metadata

* codegen metadata

* chore: align user agent format (#96)

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* chore(internal): codegen related update

* chore(internal): bump palantir-java-format

* chore(ci): skip uploading artifacts on stainless-internal branches

* chore: update placeholder string

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* fix(client): incorrect `Retry-After` parsing

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* chore(internal): tweak CI branches

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update retry delay tests

* fix(client): allow updating header/query affecting fields in `toBuilder()`

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* chore(internal): bump ktfmt

* chore: remove old test (#97)

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update gitignore

* codegen metadata

* codegen metadata

* codegen metadata

* chore(ci): skip lint on metadata-only changes

Note that we still want to run tests, as these depend on the metadata.

* Fix error messages (#102)

* codegen metadata

* Merge

* Merge

* Lint

* Add debug log

* main to next (#108)

* codegen metadata

* codegen metadata

* chore: make `Properties` more resilient to `null`

* chore: drop apache dependency

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): expand imports

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

removed endpoints not in openAPI spec

* codegen metadata

* codegen metadata

* codegen metadata

* chore: align user agent format (#96)

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* chore(internal): codegen related update

* chore(internal): bump palantir-java-format

* chore(ci): skip uploading artifacts on stainless-internal branches

* chore: update placeholder string

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* fix(client): incorrect `Retry-After` parsing

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* chore(internal): tweak CI branches

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update retry delay tests

* fix(client): allow updating header/query affecting fields in `toBuilder()`

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* chore(internal): bump ktfmt

* chore: remove old test (#97)

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update gitignore

* codegen metadata

* codegen metadata

* codegen metadata

* chore(ci): skip lint on metadata-only changes

Note that we still want to run tests, as these depend on the metadata.

* Fix error messages (#102)

* codegen metadata

* release: 0.1.0-alpha.24

* fix: lint and remove outdated method count ci test (#103)

Made-with: Cursor

* chore(deps): bump gradle/actions from 5 to 6 in the all-actions group (#99)

Bumps the all-actions group with 1 update: [gradle/actions](https://github.com/gradle/actions).


Updates `gradle/actions` from 5 to 6
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v5...v6)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Change to warning and add a test

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Adds contributing guide (#112)

* ci: add minimum workflow permissions (#106)

- Add top-level `permissions: contents: read` to all 4 workflow files
- Change release-doctor.yml trigger from `pull_request` to `pull_request_target`
  to prevent secret exfiltration via PR-controlled workflow modifications

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* ci: SHA-pin third-party Gradle actions (#107)

Pin gradle/actions/setup-gradle and gradle/gradle-build-action to full
commit SHAs to prevent supply chain attacks via tag hijacking.

- gradle/actions/setup-gradle@v6  → @205054a...  (ci.yml ×2, codeql.yml)
- gradle/gradle-build-action@v3   → @12318b0...  (ci.yml, publish-sonatype.yml)

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* chore: fix dependabot.yml posture issues (#105)

* chore: update dependabot.yml to comply with posture checks

- Change schedule from weekly to monthly for all ecosystems
- Add update-types split (major vs minor-and-patch) for gradle and github-actions
- Add docker ecosystem entry for .devcontainer/Dockerfile

* chore: add target-branch next and fix docker group split for Stainless posture compliance

---------

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* feat(api): api update

* feat(api): api update

* feat: Add `traceable` function wrapper for LangSmith tracing (#101)

* Adds versioning resource to build

* Adds initial version of traceable

* Lint

* Deflake

* Progress

* Progress

* Fixes

* Fixes

* More refactor

* Small bug

* Refactor

* Fix

* Devin feedback

* Tests and feedback

* Remove redundant comment

* Docstring

* codegen metadata

* codegen metadata

* feat: Adds processInputs and processOutputs to traceable (#113)

* Adds processInputs and processOutputs to traceable

* Move generics into TraceProcessIO to avoid having them top level

* Fix docstring, nit

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

* codegen metadata

* fix: Fix format detection when pulling legacy prompts (#115)

* Fix format detection when pulling legacy prompts

* Feedback

* Update AGENTS.md (#116)

* feat: Rework wrapOpenAI (#114)

* Update wrapOpenAI

* Remove

* Fixes

* nits

* Feedback

* Cache

* Remove comment

* Fix

* feat(api): api update

* feat(api): api update

* fix: patch security alerts — bump Jackson and constrain vulnerable transitive deps (#119)

Bumps Jackson 2.18.2→2.18.6 in published api deps and adds version
constraints for vulnerable transitive dependencies in test and example
scopes (Tomcat 9.0.115, Jetty 9.4.57, logback 1.2.13,
commons-fileupload 1.6.0, commons-io 2.14.0, json-smart 2.4.9,
snakeyaml 1.31, spring-web 5.3.34).

Addresses alerts: #75 (jackson-core), #82/#81/#80/#79/#78/#72/#70/#69
/#68/#66/#65/#60/#59/#54/#52/#51/#48/#46/#45/#44/#43/#35/#34/#29/#31
(tomcat/jetty/logback/commons).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* codegen metadata

* codegen metadata

* feat: Adds support for tracing streams with traceable (#117)

* Adds support for tracing streams with traceable

* Polish

* Make stream tracing opt-in

* Rework to use a passthrough instead of a proxy

* Record stream cancellations as errors

* Feedback

* Format and add to AGENTS.md

* feat: Adds streaming support for wrapOpenAI (#118)

* Adds support for tracing streams with traceable

* Polish

* Make stream tracing opt-in

* Rework to use a passthrough instead of a proxy

* Adds streaming support for wrapOpenAI

* Record stream cancellations as errors

* Adds streaming example

* Allow empty config default, add example

* Fix

* release: 0.1.0-alpha.25

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: Posture Fix <posture-fix@langchain.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 10:46:03 -07:00
dependabot[bot] 4ba75e7776 chore(deps): bump gradle/actions from 5 to 6 in the all-actions group (#99)
Bumps the all-actions group with 1 update: [gradle/actions](https://github.com/gradle/actions).


Updates `gradle/actions` from 5 to 6
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v5...v6)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 12:46:52 -07:00
ericdong-langchain 35bbb77c96 fix: lint and remove outdated method count ci test (#103)
Made-with: Cursor
2026-03-25 17:36:20 +00:00
stainless-app[bot] ad80d1105b release: 0.1.0-alpha.24 2026-03-25 17:36:20 +00:00
stainless-app[bot] 12a841eeb4 codegen metadata 2026-03-25 17:36:20 +00:00
Jacob Lee 19c9b420ea Fix error messages (#102) 2026-03-25 17:36:20 +00:00
stainless-app[bot] 53f2092dcd chore(ci): skip lint on metadata-only changes
Note that we still want to run tests, as these depend on the metadata.
2026-03-25 17:36:20 +00:00
stainless-app[bot] 81f7359cac codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 5c4f4acb92 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 80dceff65a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] b5b42f69a3 chore(internal): update gitignore 2026-03-25 17:36:20 +00:00
stainless-app[bot] c794cc215d codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] d07b7f3f40 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 305cae3bef codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] dd6d82830b feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 9491540375 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 22ed526472 feat(api): manual updates 2026-03-25 17:36:20 +00:00
stainless-app[bot] f7f2672957 codegen metadata 2026-03-25 17:36:20 +00:00
ericdong-langchain eaf31118a5 chore: remove old test (#97) 2026-03-25 17:36:20 +00:00
stainless-app[bot] da3535ded5 chore(internal): bump ktfmt 2026-03-25 17:36:20 +00:00
stainless-app[bot] 2796973131 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] d977e55ba6 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 7b9a6f2b09 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 219e966da6 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] d8873a4cce codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 2ce327828b fix(client): allow updating header/query affecting fields in toBuilder() 2026-03-25 17:36:20 +00:00
stainless-app[bot] b0f3b5aab6 chore(internal): update retry delay tests 2026-03-25 17:36:20 +00:00
stainless-app[bot] a54a91735b codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 959d619ee0 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 960ee71498 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 26b3f9cb2f codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] d6e265a7df codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 315a4bd074 chore(internal): tweak CI branches 2026-03-25 17:36:20 +00:00
stainless-app[bot] bd1b0e8b59 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 57116f2104 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 554e497084 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] c8873ff34e codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 12e572bdd1 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] a91eece7d1 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] d3a153ef43 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 8705818e81 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] d272832132 fix(client): incorrect Retry-After parsing 2026-03-25 17:36:20 +00:00
stainless-app[bot] 0cf53d62a4 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 8336c6349a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 382da9a537 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] ad75c9789b codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] f6d0b66f60 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 6666c85031 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 8f1fc7bf69 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] f09f2c48b3 chore: update placeholder string 2026-03-25 17:36:20 +00:00
stainless-app[bot] 3517baad85 chore(ci): skip uploading artifacts on stainless-internal branches 2026-03-25 17:36:20 +00:00
stainless-app[bot] a42751fe92 chore(internal): bump palantir-java-format 2026-03-25 17:36:20 +00:00
stainless-app[bot] bca5f1858b chore(internal): codegen related update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 43023605f0 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] f247ab1bf3 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 4461ffbf99 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] bb060d0897 feat(api): api update 2026-03-25 17:36:20 +00:00
ericdong-langchain 3e731df52b chore: align user agent format (#96) 2026-03-25 17:36:20 +00:00
stainless-app[bot] b9ce5af2d4 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 3eed0e974f codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] c616f1a7af codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] c6e950f613 feat(api): manual updates
removed endpoints not in openAPI spec
2026-03-25 17:36:20 +00:00
stainless-app[bot] 2d81794530 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 33152dc37b feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] a1533b5161 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] c9238e7d8a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 8c1f5f27ad codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 06771e7b9e codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 9b3cd93e74 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 0ff2f3175f codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] f06475795a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 81ce4fe1f3 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 6c35351e46 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 7256a6ca4a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] feb7baba86 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 4baa783b9b codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 97f40ac895 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 7d80a1978c feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 92ce263397 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] 4466c613fc codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 848c0a5751 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 1d2e391160 feat(api): api update 2026-03-25 17:36:20 +00:00
stainless-app[bot] dab7b73dc9 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 59ea961218 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 873396aceb feat(api): manual updates 2026-03-25 17:36:20 +00:00
stainless-app[bot] f12b234a80 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] a2520117a5 feat(api): manual updates 2026-03-25 17:36:20 +00:00
stainless-app[bot] 10946e186a codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 9bd1c8c310 feat(api): manual updates 2026-03-25 17:36:20 +00:00
stainless-app[bot] 44a2308df3 chore(internal): expand imports 2026-03-25 17:36:20 +00:00
stainless-app[bot] 765bad3142 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 41fcd7cec9 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] f102c9929d codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 6866c4fdfe chore: drop apache dependency 2026-03-25 17:36:20 +00:00
stainless-app[bot] 45dd9a2727 chore: make Properties more resilient to null 2026-03-25 17:36:20 +00:00
stainless-app[bot] 7512302dd4 codegen metadata 2026-03-25 17:36:20 +00:00
stainless-app[bot] 98e16a283e codegen metadata 2026-03-25 17:36:20 +00:00
Jacob Lee 8cdc61ab86 Merge pull request #98 from langchain-ai/jacob/prompts
feat: Adds prompt pulling helpers
2026-03-25 09:01:29 -07:00
dependabot[bot] 3a1a72f559 chore(deps): bump the all-actions group with 5 updates (#92)
Bumps the all-actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4` | `6` |
| [actions/setup-java](https://github.com/actions/setup-java) | `4` | `5` |
| [gradle/actions](https://github.com/gradle/actions) | `4` | `5` |
| [actions/github-script](https://github.com/actions/github-script) | `6` | `8` |
| [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) | `2` | `3` |


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `actions/setup-java` from 4 to 5
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

Updates `gradle/actions` from 4 to 5
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

Updates `actions/github-script` from 6 to 8
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v6...v8)

Updates `gradle/gradle-build-action` from 2 to 3
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
- dependency-name: gradle/gradle-build-action
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 16:55:58 -04:00
ericdong-langchain 76da93f2c0 fix: fix streaming in java sdk (#95)
* fix: fix streaming in java sdk

* chore: cleanups

* chore: setting gen_ai.completion to json text

* chore: lint fix

* chore: comments

* chore: lint

* fix: type mismatch

* chore: lint

* chore: clean up building JSON
2026-03-09 16:07:54 -04:00
John Kennedy cf4696d51a Merge pull request #89 from langchain-ai/jk/deps
chore: dependabot
2026-02-24 18:54:28 -08:00
Eric Dong d99b42f8af fix: fix typing 2026-02-20 15:53:03 +00:00
stainless-app[bot] 19b171e07e release: 0.1.0-alpha.23 2026-02-20 15:53:03 +00:00
stainless-app[bot] d02088b5c9 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 8c1c7d1930 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] ee9eec9529 chore: update mock server docs 2026-02-20 15:53:03 +00:00
stainless-app[bot] 000f84af61 chore(internal): remove mock server code 2026-02-20 15:53:03 +00:00
stainless-app[bot] 115ecd9065 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] a5822f60d0 feat(client): add connection pooling option 2026-02-20 15:53:03 +00:00
stainless-app[bot] ea263dc48f feat(client): add more convenience service method overloads 2026-02-20 15:53:03 +00:00
stainless-app[bot] 2c88ad1bb9 chore(internal): make OkHttp constructor internal 2026-02-20 15:53:03 +00:00
stainless-app[bot] c4b69463ce codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] a797755dc6 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 750cb9727c chore(internal): update TestServerExtension comment 2026-02-20 15:53:03 +00:00
stainless-app[bot] 2e9af6f60c feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] ea568e078f codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 793ca57f56 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] f0673a8381 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] a3018d05fc feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] e11fa749b7 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 24bca7acbd feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 3dfde0eac4 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 66495749d9 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] cac6931b8f chore(api): minor updates 2026-02-20 15:53:03 +00:00
stainless-app[bot] 929a7d700f feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] fb2260d06f codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] a40f07edcd feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 7068b7797c codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 379af11f42 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 974472d764 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] cd7a03e546 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] b528d8b51a codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 9a58e84cda codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] e6d4a0e83d codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 0871a9da0f chore(internal): upgrade AssertJ 2026-02-20 15:53:03 +00:00
stainless-app[bot] 86774475b2 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 7b84fdac3d codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 71cba9d0e2 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] bd733743ac codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] bc76417677 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 84e2de05ce codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 5b3bba1323 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 2b6c2ed358 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 88e87f3eae codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 40409eb155 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 4825017b86 chore(internal): allow passing args to ./scripts/test 2026-02-20 15:53:03 +00:00
stainless-app[bot] f1fa362930 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] fdb1ee6552 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 0756d8db46 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] e4a386aa13 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 3b85a7e71f codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 021a770e89 feat(api): manual updates
added transformation for duplicate variant name
2026-02-20 15:53:03 +00:00
stainless-app[bot] c358fb4281 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 9c4625d82c feat(api): manual updates
Adding title to inline variant schema.
2026-02-20 15:53:03 +00:00
stainless-app[bot] e9125a7bdf codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] 74b82f7625 chore(ci): upgrade actions/github-script 2026-02-20 15:53:03 +00:00
stainless-app[bot] 449caf4826 codegen metadata 2026-02-20 15:53:03 +00:00
stainless-app[bot] e2583dfe8d feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 52f37011ed feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 4cde1b3db1 feat(api): manual updates
fix build error
2026-02-20 15:53:03 +00:00
stainless-app[bot] bb7ebc08af feat(api): manual updates 2026-02-20 15:53:03 +00:00
stainless-app[bot] 256dfab0e1 feat(api): api update 2026-02-20 15:53:03 +00:00
stainless-app[bot] 03270ff2a5 feat(api): manual updates
fix openapi spec
2026-02-20 15:53:03 +00:00
ericdong-langchain 1902c228dd chore: refactor (#90)
* chore: refactor

* fix: lint
2026-02-20 10:13:38 -05:00
John Kennedy 3c79eea5dd chore: dependabot 2026-02-06 16:42:56 -08:00
stainless-app[bot] cff199b8fa release: 0.1.0-alpha.22 2026-01-23 16:38:33 +00:00
stainless-app[bot] 88af6cd36e fix(client): preserve time zone in lenient date-time parsing 2026-01-23 16:38:33 +00:00
stainless-app[bot] 47f39a40e4 chore(internal): correct cache invalidation for SKIP_MOCK_TESTS 2026-01-23 16:38:33 +00:00
stainless-app[bot] 79f436a6a3 docs: add comment for arbitrary value fields 2026-01-23 16:38:33 +00:00
stainless-app[bot] 60e0ce825e codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] 54d56e2915 codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] 23597a4e3f feat(client): send X-Stainless-Kotlin-Version header 2026-01-23 16:38:33 +00:00
stainless-app[bot] e55e8c7fe2 chore(internal): update maven repo doc to include authentication 2026-01-23 16:38:33 +00:00
stainless-app[bot] 92d87eab22 codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] d40dd2c26f codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] f0f5ad3c41 feat(api): api update 2026-01-23 16:38:33 +00:00
stainless-app[bot] da14cc6869 chore(ci): upgrade actions/setup-java 2026-01-23 16:38:33 +00:00
stainless-app[bot] a2648390af fix(client): fully respect max retries
fix(client): send retry count header for max retries 0
chore(internal): depend on packages directly in example
2026-01-23 16:38:33 +00:00
stainless-app[bot] 43741790f7 fix: deserialization order 2026-01-23 16:38:33 +00:00
stainless-app[bot] 10e2af9aea chore(internal): update actions/checkout version 2026-01-23 16:38:33 +00:00
stainless-app[bot] 04508f89a2 fix(client): disallow coercion from float to int 2026-01-23 16:38:33 +00:00
stainless-app[bot] 4c2738351e chore(internal): improve maven repo docs 2026-01-23 16:38:33 +00:00
stainless-app[bot] b34611a2d6 chore: test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind#3240 in tests
fix: date time deserialization leniency
2026-01-23 16:38:33 +00:00
stainless-app[bot] 0a7b44831a chore(internal): clean up maven repo artifact script and add html documentation to repo root 2026-01-23 16:38:33 +00:00
stainless-app[bot] 49918bb9ab codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] 3c7a62f01e codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] 501beab343 codegen metadata 2026-01-23 16:38:33 +00:00
stainless-app[bot] fff7a4663f release: 0.1.0-alpha.21 2026-01-14 21:28:19 +00:00
stainless-app[bot] 398c86771f feat(api): api update 2026-01-14 21:28:19 +00:00
stainless-app[bot] 2d298cd81f feat(api): manual updates
added title to duplicate schemas
2026-01-14 21:28:19 +00:00
stainless-app[bot] 7050baf4e1 codegen metadata 2026-01-14 21:28:19 +00:00
stainless-app[bot] f0b2abc659 feat(api): api update 2026-01-14 21:28:19 +00:00
stainless-app[bot] 7ef6b9f8c7 chore(internal): support uploading Maven repo artifacts to stainless package server 2026-01-14 21:28:19 +00:00
stainless-app[bot] 5c4cf4f0f7 feat(api): api update 2026-01-14 21:28:19 +00:00
stainless-app[bot] ded153ef51 release: 0.1.0-alpha.20 (#85)
* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* feat(api): api update

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(client): add `HttpRequest#url()` method

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(client): allow configuring dispatcher executor service

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* release: 0.1.0-alpha.20

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
2026-01-14 14:43:32 -05:00
Eric Dong 5572d09902 fix: fixed build and test 2025-12-10 22:13:47 +00:00
stainless-app[bot] 2f56b7d053 release: 0.1.0-alpha.19 2025-12-10 22:13:47 +00:00
stainless-app[bot] fe3381c85e feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] 4518614715 feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] b8f700dbc3 chore(api): delete index methods
Removed get and delete index methods
2025-12-10 22:13:47 +00:00
stainless-app[bot] 83aa1fe5b7 feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] 7e0b329e90 feat(api): manual updates
Initial release
2025-12-10 22:13:47 +00:00
stainless-app[bot] 070629d380 feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] 6207c8bf2d codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] fbba779511 feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] b50bd7233c feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] 364329ec65 feat(api): manual updates 2025-12-10 22:13:47 +00:00
stainless-app[bot] 87d0bfb614 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 7863108ae3 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 1755b4ae62 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 034daaf52e codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] d267892e0d docs: remove $ for better copy-pasteabality 2025-12-10 22:13:47 +00:00
stainless-app[bot] 6bc095cc83 feat(api): api update 2025-12-10 22:13:47 +00:00
stainless-app[bot] 249b9c66c8 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 737f8d2cf2 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 4220932794 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] d4cab9eef6 codegen metadata 2025-12-10 22:13:47 +00:00
stainless-app[bot] 0a5f8a76be fix(client): cancel okhttp call when future cancelled 2025-12-10 22:13:47 +00:00
stainless-app[bot] 32eabcac91 feat(api): api update 2025-12-10 22:13:47 +00:00
ericdong-langchain 7a644105f6 fix: fix trace input and output (#83)
* fix: fix trace input and output

* fix: lint
2025-12-10 16:43:16 -05:00
stainless-app[bot] 9de590ad96 release: 0.1.0-alpha.18 (#79)
* chore: Configure workflow for CodeQL

* feat(api): pagination

added pagination to all endpoints that use pagination

* codegen metadata

* chore(internal): version bump

* codegen metadata

* feat(api): change paginated retrieve methods to list

change paginated retrieve methods to list

* feat(api): add go target

* codegen metadata

* feat(api): pagination read me

added pagination read me

* codegen metadata

* feat(api): readme example - change REPLACE_ME session_id to uuid

* feat: method counter in ci

* codegen metadata

* test

* feat(api): api update

* fix: formatting error in record experiment example (#81)

* release: 0.1.0-alpha.18

---------

Co-authored-by: xornivore <30147836+xornivore@users.noreply.github.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
2025-12-09 14:16:39 +00:00
stainless-app[bot] 2685825993 release: 0.1.0-alpha.17 2025-11-20 23:46:19 +00:00
xornivore cb8962fb25 chore: Configure workflow for CodeQL 2025-11-20 23:46:19 +00:00
stainless-app[bot] 4c0258d01f release: 0.1.0-alpha.16 2025-11-20 15:03:53 -05:00
stainless-app[bot] ded1673fa6 codegen metadata 2025-11-20 14:49:12 -05:00
xornivore 5597676151 chore: Update runner to ubuntu-latest-l-oss for Sonatype Publish 2025-11-20 14:49:12 -05:00
stainless-app[bot] dd880b1002 release: 0.1.0-alpha.15 2025-11-20 17:08:47 +00:00
Eric Dong 6eba79b3c5 chore: changed unit of time 2025-11-20 17:08:47 +00:00
1015 changed files with 233650 additions and 37279 deletions
-5
View File
@@ -1,5 +0,0 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
+12
View File
@@ -0,0 +1,12 @@
name: "CodeQL config"
queries:
- uses: security-extended
paths-ignore:
- '**/test/**'
- '**/tests/**'
- '**/*Test.java'
- '**/*Test.kt'
- '**/build/**'
- '.gradle/**'
+58
View File
@@ -0,0 +1,58 @@
version: 2
updates:
- package-ecosystem: gradle
directory: /
target-branch: "next"
schedule:
interval: monthly
groups:
minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
major:
patterns:
- "*"
update-types:
- major
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /.devcontainer
target-branch: "next"
schedule:
interval: monthly
groups:
minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
major:
patterns:
- "*"
update-types:
- major
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
target-branch: "next"
schedule:
interval: monthly
groups:
minor-and-patch:
patterns:
- "*"
update-types:
- minor
- patch
major:
patterns:
- "*"
update-types:
- major
open-pull-requests-limit: 5
@@ -0,0 +1,72 @@
name: Check Stainless File Modification
on:
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- labeled
- unlabeled
permissions:
contents: read
pull-requests: read
jobs:
check-stainless-files:
name: Check for Stainless-generated file modifications
runs-on: ubuntu-latest
if: github.repository == 'langchain-ai/langsmith-java-staging'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for modifications to Stainless-generated files
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
SKIP_LABEL="skip-stainless-generated-file-modification-check"
# Check if the skip label is present
LABELS=$(gh api repos/"$REPO"/pulls/"$PR_NUMBER" --jq '[.labels[].name] | join(",")')
if echo "$LABELS" | grep -qF "$SKIP_LABEL"; then
echo "Skip label '$SKIP_LABEL' is present — skipping check."
exit 0
fi
# Get the list of files changed in this PR
CHANGED_FILES=$(git diff --name-only "origin/${{ github.base_ref }}"...HEAD)
STAINLESS_FILES=""
while IFS= read -r FILE; do
[ -z "$FILE" ] && continue
[ ! -f "$FILE" ] && continue
if grep -qF "File generated from our OpenAPI spec by Stainless." "$FILE"; then
STAINLESS_FILES="$STAINLESS_FILES\n - $FILE"
fi
done <<< "$CHANGED_FILES"
if [ -n "$STAINLESS_FILES" ]; then
echo "::error::This PR modifies Stainless-generated files, which should not be edited directly."
printf "Stainless-generated files modified:%b\n" "$STAINLESS_FILES"
echo ""
echo "Please choose one of the following options:"
echo ""
echo " 1. Refactor your change to avoid modifying the generated file directly."
echo ""
echo " 2. Use Stainless configuration instead:"
echo " - OpenAPI extensions: https://www.stainless.com/docs/openapi/extensions/"
echo " - Transforms: https://www.stainless.com/docs/openapi/transforms/reference/"
echo ""
echo " 3. If neither option above is feasible, add the label '$SKIP_LABEL' to this PR to bypass this check."
echo " Use this only as a last resort and document why it is necessary."
exit 1
fi
echo "No Stainless-generated files were modified."
+25 -34
View File
@@ -1,29 +1,34 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'
permissions:
contents: read
jobs:
lint:
timeout-minutes: 15
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -32,7 +37,7 @@ jobs:
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
- name: Run lints
run: ./scripts/lint
@@ -40,14 +45,17 @@ jobs:
build:
timeout-minutes: 15
name: build
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -56,30 +64,13 @@ jobs:
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
- name: Build SDK
run: ./scripts/build
test:
timeout-minutes: 15
name: test
runs-on: ${{ github.repository == 'stainless-sdks/langsmith-api-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
21
cache: gradle
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3
- name: Run tests
run: ./scripts/test
+44
View File
@@ -0,0 +1,44 @@
name: "CodeQL Advanced"
on:
push:
branches: [main, next]
pull_request:
branches: [main, next]
schedule:
- cron: "0 0 * * 0" # Weekly scan on Sunday at midnight
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
8
21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: "java-kotlin"
config-file: ./.github/codeql/codeql-config.yml
- name: Build SDK
run: ./gradlew build testClasses -x test --no-daemon --no-build-cache clean build
+14 -8
View File
@@ -1,6 +1,5 @@
# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to Sonatype in case it failed for some reason.
# You can run this workflow by navigating to https://www.github.com/langchain-ai/langsmith-java/actions/workflows/publish-sonatype.yml
# workflow for re-running publishing to Sonatype in case it fails for some reason
# you can run this workflow by navigating to https://www.github.com/langchain-ai/langsmith-java/actions/workflows/publish-sonatype.yml
name: Publish Sonatype
on:
workflow_dispatch:
@@ -8,16 +7,19 @@ on:
release:
types: [published]
permissions:
contents: read
jobs:
publish:
name: publish
runs-on: ubuntu-latest-l
runs-on: ubuntu-latest-l-oss
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -26,14 +28,18 @@ jobs:
cache: gradle
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3
- name: Publish to Sonatype
run: |-
export -- GPG_SIGNING_KEY_ID
printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
export ORG_GRADLE_PROJECT_signingInMemoryKey="$GPG_SIGNING_KEY"
export ORG_GRADLE_PROJECT_signingInMemoryKeyId="$GPG_SIGNING_KEY_ID"
export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword="$GPG_SIGNING_PASSWORD"
# Keep publish execution serial to avoid Gradle/Dokka task graph race conditions.
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache --no-parallel
env:
SONATYPE_USERNAME: ${{ secrets.LANGCHAIN_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.LANGCHAIN_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+6 -2
View File
@@ -1,10 +1,13 @@
name: Release Doctor
on:
pull_request:
pull_request_target:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
release_doctor:
name: release doctor
@@ -12,12 +15,13 @@ jobs:
if: github.repository == 'langchain-ai/langsmith-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
bash ./bin/check-release-environment
env:
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
SONATYPE_USERNAME: ${{ secrets.LANGCHAIN_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.LANGCHAIN_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
GPG_SIGNING_KEY: ${{ secrets.LANGCHAIN_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+28
View File
@@ -0,0 +1,28 @@
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
actions: write
jobs:
release-please:
if: github.repository == 'langchain-ai/langsmith-java'
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger publish
if: ${{ steps.release.outputs.release_created }}
run: gh workflow run publish-sonatype.yml --ref ${{ steps.release.outputs.tag_name }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
@@ -0,0 +1,47 @@
name: "stlc: trigger promote on stlc/from-prod → main approval"
# Dispatches the ff-only merge workflow in langchainplus whenever the stlc/from-prod → main
# PR in this repo receives an approving review.
on:
pull_request_review:
types: [submitted]
permissions:
contents: read
jobs:
trigger-promote:
if: |
github.repository == 'langchain-ai/langsmith-java-staging' &&
github.event.review.state == 'approved' &&
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.head.ref == 'stlc/from-prod'
runs-on: self-hosted-ubuntu-small-amd64
steps:
- name: Load GitHub App key from 1Password
id: load-gh-app-secrets
uses: 1password/load-secrets-action@92467eb28f72e8255933372f1e0707c567ce2259 # v4.0.0
with:
export-env: false
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: "Iv23liMFOFBcPTKFWi3S"
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
repositories: langchainplus
- name: Dispatch ff-only merge workflow in langchainplus
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh workflow run git-ff-only-merge.yml \
--repo langchain-ai/langchainplus \
--field repo=langsmith-java-staging \
--field src_branch=stlc/from-prod \
--field target_branch=main
@@ -0,0 +1,87 @@
name: 'stlc: promote to production'
on:
push:
branches: [main]
permissions:
contents: read
jobs:
promote:
if: github.repository == 'langchain-ai/langsmith-java-staging'
runs-on: self-hosted-ubuntu-small-amd64
env:
PRODUCTION_REPO: langchain-ai/langsmith-java
steps:
- name: Check out staging
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Load GitHub App key from 1Password
id: load-gh-app-secrets
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
with:
export-env: false
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: "Iv23liMFOFBcPTKFWi3S"
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
repositories: langsmith-java-staging,langsmith-java
- name: Fetch production main
run: |
git remote add production \
"https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${PRODUCTION_REPO}.git"
git fetch production main
- name: Check if production is already in sync
id: diff
run: |
STAGING_SHA=$(git rev-parse origin/main)
PRODUCTION_SHA=$(git rev-parse production/main)
if [ "$STAGING_SHA" = "$PRODUCTION_SHA" ]; then
echo "Production is already at $STAGING_SHA. Nothing to release."
echo "synced=true" >> "$GITHUB_OUTPUT"
else
echo "synced=false" >> "$GITHUB_OUTPUT"
fi
- name: Push staging main to the release branch on production
if: steps.diff.outputs.synced == 'false'
run: |
git push production origin/main:refs/heads/stainless/release --force
- name: Open or update the release PR on production
if: steps.diff.outputs.synced == 'false'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
EXISTING_PR=$(gh pr list \
--repo "${PRODUCTION_REPO}" \
--head stainless/release \
--state open \
--json number \
--jq '.[0].number')
if [ -z "${EXISTING_PR}" ]; then
gh pr create \
--repo "${PRODUCTION_REPO}" \
--base main \
--head stainless/release \
--title "Release SDK updates" \
--body "$(git log --reverse --format="%s%n" production/main..origin/main)"
else
gh pr edit "${EXISTING_PR}" \
--repo "${PRODUCTION_REPO}" \
--body "$(git log --reverse --format="%s%n" production/main..origin/main)"
echo "Release PR #${EXISTING_PR} already exists. Updated its description."
fi
@@ -0,0 +1,47 @@
name: "stlc: seal custom code"
# Triggered on every push to the `main` branch of langsmith-java-staging.
# Delegates to the stlc_seal_custom_code workflow in langchainplus (where
# the stainless workspace and reusable actions live) by dispatching it via
# the GitHub API, scoped to the java target only.
on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions:
id-token: write
jobs:
seal:
if: github.repository == 'langchain-ai/langsmith-java-staging'
runs-on: self-hosted-ubuntu-small-amd64
steps:
- name: Load GitHub App key from 1Password
id: load-gh-app-secrets
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
with:
export-env: false
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: "Iv23liMFOFBcPTKFWi3S"
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
repositories: langsmith-java-staging,langchainplus
- name: Trigger stlc seal workflow in langchainplus
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh workflow run stlc_seal_custom_code.yml \
--repo langchain-ai/langchainplus \
--field repositories=langsmith-java-staging
@@ -0,0 +1,86 @@
name: "stlc: sync from production"
on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch: {}
permissions:
contents: write
pull-requests: write
concurrency:
group: stlc-sync-from-production
cancel-in-progress: true
jobs:
sync:
runs-on: self-hosted-ubuntu-small-amd64
if: github.repository == 'langchain-ai/langsmith-java-staging'
env:
PRODUCTION_REPO: langchain-ai/langsmith-java
steps:
- name: Load GitHub App key from 1Password
id: load-gh-app-secrets
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2
with:
export-env: false
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN_INTERNAL }}
OP_CONNECT_HOST: ${{ secrets.OP_CONNECT_HOST }}
LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY: op://${{ secrets.INTERNAL_INFRA_OP_VAULT }}/langchain-actions-pr-bot/private_key
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: "Iv23liMFOFBcPTKFWi3S"
private-key: ${{ steps.load-gh-app-secrets.outputs.LANGCHAINPLUS_GITHUB_APP_PRIVATE_KEY }}
repositories: langsmith-java-staging,langsmith-java,stlc,stlc-java,langchainplus
- name: Check out staging
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
token: ${{ steps.app-token.outputs.token }}
- name: Fetch production main
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git remote add production \
"https://x-access-token:${APP_TOKEN}@github.com/${PRODUCTION_REPO}.git"
git fetch production main
- name: Check whether production has content staging lacks
id: diff
run: |
MERGED=$(git merge-tree --write-tree origin/main production/main) || MERGED=conflict
STAGING_TREE=$(git rev-parse 'origin/main^{tree}')
if [ "$MERGED" = "$STAGING_TREE" ]; then
echo "Staging already has production's content. Nothing to pull back."
echo "behind=false" >> "$GITHUB_OUTPUT"
else
echo "behind=true" >> "$GITHUB_OUTPUT"
fi
- name: Push production main to the back-sync branch on staging
if: steps.diff.outputs.behind == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: git push origin production/main:refs/heads/stlc/from-prod --force
- name: Open or update the back-sync PR on staging
if: steps.diff.outputs.behind == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
EXISTING_PR=$(gh pr list --head stlc/from-prod --base main --state open --json number --jq '.[0].number')
if [ -z "${EXISTING_PR}" ]; then
gh pr create \
--base main \
--head stlc/from-prod \
--title "Sync release changes from production" \
--body "Pulls production's release-please commits (and any merged contributions) back onto staging \`main\` so the next \`stlc build\` reseals against the released state. Bot-authored; safe to auto-merge."
fi
+13
View File
@@ -1,7 +1,20 @@
.prism.log
.stdy.log
.env
.env.*
*.pem
*.key
*.crt
credentials.json
node_modules/
__pycache__/
.venv/
.DS_Store
.gradle
.idea
.kotlin
**/bin/main/
**/bin/test/
build/
codegen.log
kls_database.db
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.14"
".": "0.1.0-beta.19"
}
+1 -4
View File
@@ -1,4 +1 @@
configured_endpoints: 100
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/langsmith%2Flangsmith-api-1fcc324268478b6855afad6c2be9f95d44ecbfb80692c59c3aa766b0ee73c7ad.yml
openapi_spec_hash: fea408ff4197ce234ee92f3be98213fa
config_hash: df1c0c7c487784f41abaf300788fbf21
configured_endpoints: 149
+266
View File
@@ -0,0 +1,266 @@
# Agent Guidelines
Code conventions and patterns for this project, learned from review feedback.
## Code structure
### Break up complex functions with helpers
When a function has deeply nested logic or multiple concerns, extract helpers. Use `flatMap` + small named functions instead of imperative loops with nested `when`/`if`:
```kotlin
// Good
fun format(variables: Map<String, Any>): PromptMessages {
val formatted = messages.flatMap { msg ->
if (msg.isPlaceholder()) expandPlaceholder(msg, variables)
else listOf(PromptMessage.withTemplate(msg, msg.format(variables)))
}
return PromptMessages(formatted, inputVariables, outputSchema)
}
private fun expandPlaceholder(msg: PromptMessage, variables: Map<String, Any>): List<PromptMessage> {
val items = variables[msg.template] as? List<*> ?: return emptyList()
return items.mapNotNull(::toPromptMessage)
}
// Bad — deeply nested imperative loop
fun format(variables: Map<String, Any>): PromptMessages {
val formatted = mutableListOf<PromptMessage>()
for (msg in messages) {
if (msg.isPlaceholder()) {
val value = variables[msg.template]
if (value is List<*>) {
for (item in value) {
when (item) {
is PromptMessage -> formatted.add(item)
is Map<*, *> -> { /* 15 more lines */ }
}
}
}
} else { ... }
}
}
```
## Kotlin idioms
### Prefer immutable collection transformations
Avoid mutable accumulators when `map`, `filter`, `partition`, `associate`, `buildMap`, or `buildList` express the same logic clearly. Use mutation only when it materially improves readability, performance, or is required by an API.
### Use `buildMap` / `buildList` instead of mutable + convert
```kotlin
// Good
val messages = items.map { msg ->
buildMap<String, String> {
put("role", msg.role)
put("content", msg.content)
msg.toolCallId?.let { put("tool_call_id", it) }
}
}
// Bad — unnecessary mutable/immutable conversion
val messages = items.map { msg ->
val base = mutableMapOf<String, String>(
"role" to msg.role,
"content" to msg.content,
)
if (msg.toolCallId != null) {
base["tool_call_id"] = msg.toolCallId
}
base.toMap()
}
```
### Use `buildList` for conditional `toString()` parts
```kotlin
// Good
override fun toString(): String {
val parts = buildList {
add("messages=[${messages.joinToString(", ")}]")
if (inputVariables.isNotEmpty()) add("inputVariables=$inputVariables")
commitHash?.let { add("commitHash=$it") }
outputSchema?.let { add("outputSchema=${it["title"] ?: "..."}") }
}
return "Prompt{${parts.joinToString(", ")}}"
}
// Bad — chained ternary string concatenation
override fun toString(): String =
"Prompt{messages=[...]" +
(if (commitHash != null) ", commitHash=$commitHash" else "") +
(if (hasOutputSchema()) ", outputSchema=..." else "") +
"}"
```
### Prefer extension functions over type casts
When adding behavior to a type you don't own, use a `private` extension function instead of casting to an implementation type:
```kotlin
// Good — extension function, no cast needed
private fun Stream<*>.withErrorTracking(
errorRef: AtomicReference<Throwable>,
exhaustedRef: AtomicBoolean,
): Stream<Any?> { ... }
val instrumented = result.withErrorTracking(iterationError, streamExhausted)
// Bad — casting to implementation type
val instrumented = wrapStreamWithErrorCapture(result, iterationError, streamExhausted)
// or worse:
(runs as? RunServiceImpl)?.flush()
```
### Use `partition` instead of double `filter`
```kotlin
// Good — single pass
val (systemMessages, nonSystemMessages) =
messages.partition { it.role == Role.SYSTEM }
// Bad — iterates the list twice
val systemMessages = messages.filter { it.role == Role.SYSTEM }
val nonSystemMessages = messages.filter { it.role != Role.SYSTEM }
```
### Use parameterized tests for table-driven cases
When multiple tests share the exact same structure (input → assert same fields), use `@ParameterizedTest` with `@MethodSource`:
```kotlin
data class Case(val input: String, val expected: String)
@ParameterizedTest(name = "{index}: \"{0}\"")
@MethodSource("cases")
fun myTest(case: Case) {
assertThat(transform(case.input)).isEqualTo(case.expected)
}
companion object {
@JvmStatic
fun cases(): Stream<Case> = Stream.of(
Case("input1", "expected1"),
Case("input2", "expected2"),
)
}
```
Only do this when every test has the same assertion shape. If tests have different setup or assertions, keep them as individual `@Test` methods.
### Extract test assertion helpers to reduce repetition
When the same assertion pattern appears across many tests, extract a helper:
```kotlin
// Good — readable, DRY
private fun assertMessage(msg: Map<String, String>, role: String, content: String) {
assertThat(msg["role"]).isEqualTo(role)
assertThat(msg["content"]).isEqualTo(content)
}
assertMessage(result.messages[0], "system", "You are helpful.")
assertMessage(result.messages[1], "user", "Hello")
// Bad — verbose, repetitive
assertThat(result.messages[0]).isEqualTo(mapOf("role" to "system", "content" to "You are helpful."))
assertThat(result.messages[1]).isEqualTo(mapOf("role" to "user", "content" to "Hello"))
```
## Formatting and linting
```bash
./gradlew :langsmith-java-core:formatKotlin
./gradlew lintKotlin
```
The project uses ktfmt with `--kotlinlang-style`.
## Testing
### Don't add comments that restate the test name
Test function names should be descriptive enough on their own. Don't add comments that repeat what the name already says:
```kotlin
// Good — name is self-documenting
@Test
fun parseLegacyPromptTemplateWithTemplateFormat() {
val manifest = ...
}
// Bad — comment restates the function name
@Test
fun parseLegacyPromptTemplateWithTemplateFormat() {
// Legacy PromptTemplate format with template_format field
val manifest = ...
}
```
### Running tests
Try to run targeted tests matching the code you changed:
```bash
# Run tests for the package you changed
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.*" --rerun
# Run a single test class
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest" --rerun
# Run a single test method
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest.parsePromptTemplate" --rerun
# See println output
./gradlew :langsmith-java-core:test --tests "..." --rerun --info
```
Only run the full suite (`./gradlew :langsmith-java-core:test --rerun`) before finalizing a PR or after large cross-cutting changes.
### Integration tests
Integration tests require environment variables:
```bash
export LANGSMITH_API_KEY="lsv2_pt_..."
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
Tests skip gracefully via `assumeTrue` if keys are missing.
## Code style
- For cross-method concurrency coordination, prefer an explicit named `ReentrantLock` over `synchronized` when review clarity matters. Use Kotlin's `withLock { ... }` extension instead of manual `lock()` / `try` / `finally { unlock() }` unless explicit lock management is required. Keep the locked section minimal and do slow/blocking work outside the lock.
- Choose the simplest concurrency primitive that fits the state being protected:
- Use atomic types (`AtomicBoolean`, `AtomicInteger`, etc.) for simple flags, counters, and compare-and-set state.
- Use `synchronized` only for small, local critical sections where a named lock would not improve clarity.
- Use `ReentrantReadWriteLock` when reads are frequent, writes are infrequent, and concurrent reads materially help.
- Use coroutine `Mutex.withLock { ... }` for coroutine-based concurrency instead of blocking thread locks.
- `toString()` should be single-line, following the `ClassName{field=value, field=value}` convention used by the rest of the SDK.
- Avoid `@Suppress("UNCHECKED_CAST")` — restructure code to use safe patterns (`as? String`, `is Map<*, *>` with `entries.associate`, etc). When unavoidable (e.g. generic type erasure after an `is` check), add a comment explaining why the cast is safe.
- Use named arguments for constructor/function calls with 2+ parameters, especially when types could be confused:
```kotlin
// Good
PromptMessage(
role = PromptMessage.Role.HUMAN,
template = template,
templateFormat = templateFormat,
)
// Bad — positional args are ambiguous
PromptMessage(PromptMessage.Role.HUMAN, template, templateFormat = templateFormat)
```
- Name functions from the caller's perspective — describe what the caller gets, not what the function does internally. Prefer `stream.withErrorTracking()` over `wrapStreamWithErrorCapture(stream)`.
- When an `Optional` has a fallback default, use `orElse(default)` directly instead of `orElse(null) ?: default`:
```kotlin
// Good — default goes straight into orElse
kwargs["template_format"]?.asString()?.orElse("f-string") ?: "f-string"
// Bad — creates unnecessary null intermediary
kwargs["template_format"]?.asString()?.orElse(null) ?: "f-string"
```
The `?: "f-string"` is still needed to handle the case where the key is missing from the map (`null` from `kwargs["template_format"]`), but `orElse` should carry the default for when the key exists but isn't a string.
- Anthropic SDK is a `compileOnly` dependency — users must add it themselves. Methods that use Anthropic types should catch `NoClassDefFoundError` and throw `IllegalStateException` with a clear message.
+708
View File
@@ -1,5 +1,713 @@
# Changelog
## [0.1.0-beta.19](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.18...v0.1.0-beta.19) (2026-07-23)
### Features
* **backend:** Allow storing grid layout to custom dashboards ([8aded40](https://github.com/langchain-ai/langsmith-java/commit/8aded40800f6096880f43d2ef51a07963dec65e5))
* **backend:** update CRUD endpoint to allow retrieving / storing chart series metadata ([aa4be05](https://github.com/langchain-ai/langsmith-java/commit/aa4be05c111d702aa801cba4acbda69e0a5da7a6))
* **host:** p50/p99 run queue wait-time chart in project monitoring [LSD-1547] ([f660828](https://github.com/langchain-ai/langsmith-java/commit/f6608288d483e0041c6e0fe643f6881f3c1a3a68))
* **sandboxes:** free-form labels on sandboxes and snapshots ([6165434](https://github.com/langchain-ai/langsmith-java/commit/6165434d21025dfaeef22b4a63e31a0f0e4977a1))
* **sandboxes:** let proxy rules contribute sandbox env vars ([eace8c3](https://github.com/langchain-ai/langsmith-java/commit/eace8c3f9903e180fc7e9cfbaf0a13a7a91a5635))
### Bug Fixes
* **evaluators:** persist and wire through playground_settings_id for llm-as-judge oauth models [part of ENT-1402] ([b072b2e](https://github.com/langchain-ai/langsmith-java/commit/b072b2e2fcf6dafd32cb0213e02127a82ab0659a))
* **runs:** require session in POST /runs/stats ([056dbd3](https://github.com/langchain-ai/langsmith-java/commit/056dbd3673294fcd1fbc564d4332ba38c151ec6b))
* support last_queued_at in v2 runs query ([e7fe407](https://github.com/langchain-ai/langsmith-java/commit/e7fe407f74819ac775fcc092289d9dcfaafc036d))
### Reverts
* **runs:** require session in POST /runs/stats ([95da1e9](https://github.com/langchain-ai/langsmith-java/commit/95da1e9b944058f30b5274c882f4669933b8bbfd))
### Refactors
* **runs:** remove SmithDB v2 endpoint flag [LSO-3391] ([aece72d](https://github.com/langchain-ai/langsmith-java/commit/aece72d035ca24eb8ee94236d301f7610b18eefc))
## [0.1.0-beta.18](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.17...v0.1.0-beta.18) (2026-07-20)
### Features
* **runs:** add GET /v2/runs/{run_id}/url endpoint and SDK runs.get_url method ([57da309](https://github.com/langchain-ai/langsmith-java/commit/57da3096437fe210865bab3a7560eff50d20feef))
### Bug Fixes
* build, lint, and test failures (feat(gateway): rate limit policy CRUD, validation, and materialization [ENT-1138]) ([388984c](https://github.com/langchain-ai/langsmith-java/commit/388984c00503fe2dd39aacb93fe4053ae2030d43))
## [0.1.0-beta.17](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.16...v0.1.0-beta.17) (2026-07-17)
### Features
* **runs/v2:** make start_time optional in GET /v2/runs/:id ([101472f](https://github.com/langchain-ai/langsmith-java/commit/101472f4ce1210028bb37a577a3992a49511af40))
### Chores
* remove stlc-promote-to-main workflow ([#45](https://github.com/langchain-ai/langsmith-java/issues/45)) ([df8f95a](https://github.com/langchain-ai/langsmith-java/commit/df8f95af97e784a035da210551d5aa17c63608c3))
### Documentation
* minor rewording of List Commits endpoint description ([2f1ca87](https://github.com/langchain-ai/langsmith-java/commit/2f1ca870dd9a60016d2c0bad66b1ece2826d41da))
## [0.1.0-beta.16](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.15...v0.1.0-beta.16) (2026-07-17)
### Features
* add run rule evaluator tracing flag [LSO-3323] ([d53da42](https://github.com/langchain-ai/langsmith-java/commit/d53da42ffaa0d5072409f45d4398a03741013c6a))
* **engine:** issue lifecycle backend + webhook status filter (2/8) ([cbc0080](https://github.com/langchain-ai/langsmith-java/commit/cbc00807ee3991638b76430095c521243bcf5675))
* **feedback:** support GET /feedback for thread AQs ([5052542](https://github.com/langchain-ai/langsmith-java/commit/50525423cca7d2024be967a074267a97068c33ed))
### Bug Fixes
* **datasets:** serve public dataset sessions via Go, fixing SmithDB-only 501 ([8eb0670](https://github.com/langchain-ai/langsmith-java/commit/8eb067064eedc528a7e3381e9a84c9bceff0886f))
* patch 6 high-severity security alerts ([#42](https://github.com/langchain-ai/langsmith-java/issues/42)) ([8b1dff9](https://github.com/langchain-ai/langsmith-java/commit/8b1dff9a1ed8a7982278f393ca2859a3d96a97a0))
* remove July 2026 premier plan ([1409dd8](https://github.com/langchain-ai/langsmith-java/commit/1409dd89d8da75b5650ca15509c6e73d545364df))
### Chores
* new unshare endpoint that doesn't require lookup ([8391078](https://github.com/langchain-ai/langsmith-java/commit/839107858ca208c7238ef83e4fbd9379e31bf220))
* Stainless SDK methods for V2 public-run APIs ([306a079](https://github.com/langchain-ai/langsmith-java/commit/306a079479b914fc67a0d7627f29c8c95b6b5c4c))
## [0.1.0-beta.15](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.14...v0.1.0-beta.15) (2026-07-15)
### Features
* add July 2026 startup and premier plan tiers ([b2ed882](https://github.com/langchain-ai/langsmith-java/commit/b2ed882d0edd038733fcdeda5448519a5f4dfe9b))
* **context-hub:** fire commit webhook and deliver async ([2c9704f](https://github.com/langchain-ai/langsmith-java/commit/2c9704f3c0151bd51ff57ecc54edbcfb527e61d7))
* **sandboxes:** add preserve_memory_on_stop create attribute ([99fe7db](https://github.com/langchain-ai/langsmith-java/commit/99fe7dbe33f3a0bb8709577bcd08a3f24113d5e4))
### Chores
* update smithdb proto generated code ([d35ada2](https://github.com/langchain-ai/langsmith-java/commit/d35ada22c7e73d60b6fc44185d7a65a99a4255e1))
## [0.1.0-beta.14](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.13...v0.1.0-beta.14) (2026-07-14)
### Features
* **dashboards:** convert prebuilts to v2 charts on clone [LSO-3306, LSO-3322] ([0a29041](https://github.com/langchain-ai/langsmith-java/commit/0a2904189719cead7198fcc1765e2c44ab36dec9))
* **evaluators:** expose is_managed on Go evaluator reads [LSE-2532] ([17551de](https://github.com/langchain-ai/langsmith-java/commit/17551decf77e4e2141473fe9cdbf33ea4b4648b6))
* forward filter param to thread stats API ([12185be](https://github.com/langchain-ai/langsmith-java/commit/12185beb4cf348a4252af15257ae2b0133027a42))
* **sdk:** wire annotation-queue runs/by-key into stainless [LSAQ-141] ([18d7867](https://github.com/langchain-ai/langsmith-java/commit/18d7867206e7b66a91a3d4452811636aa173e55c))
### Bug Fixes
* patch remaining Java security alerts ([#38](https://github.com/langchain-ai/langsmith-java/issues/38)) ([e639046](https://github.com/langchain-ai/langsmith-java/commit/e639046090177450d005f9fa511d4c03a565a504))
* **smith-go:** default time bounds for POST /v2/threads/query ([63bde3c](https://github.com/langchain-ai/langsmith-java/commit/63bde3c6e4ef1c470a82a7814dd5f6083f98c110))
* **smith-sdks:** name thread stats response model ThreadStats ([1893f83](https://github.com/langchain-ai/langsmith-java/commit/1893f83cf27ed269a62fba637c829e31c1a94bf8))
### Refactors
* **sandboxes:** make context hub sync a generic mount ([9cfd26b](https://github.com/langchain-ai/langsmith-java/commit/9cfd26bef9d4efc3d66d25f6a54f2224393d935b))
## [0.1.0-beta.13](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.12...v0.1.0-beta.13) (2026-07-08)
### Features
* Adds attachment support to Java multipart requests ([#33](https://github.com/langchain-ai/langsmith-java/issues/33)) ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
* **backend:** extend_trace_retention field for annotation queue adds ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
### Bug Fixes
* re-emit commits missed by release-please due to timestamp ordering ([#181](https://github.com/langchain-ai/langsmith-java/issues/181)) ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
### Refactors
* **sandboxes:** resolve default sandbox via snapshot tag instead of blueprint ([38646cb](https://github.com/langchain-ai/langsmith-java/commit/38646cb1fabc1155b36ea8db43c69f2dd38dede9))
## [0.1.0-beta.12](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.11...v0.1.0-beta.12) (2026-07-08)
### ⚠ BREAKING CHANGES
* **stainless:** skip sessions resource for python and typescript
### Features
* **managed-eval:** expose is_managed_evaluator on run-rule read/write paths ([8e17124](https://github.com/langchain-ai/langsmith-java/commit/8e17124b681e504789b3bcb7b03653e327339d1a))
* **smith-sdks:** expose threads and traces v2 endpoints in public SDK ([3bc6173](https://github.com/langchain-ai/langsmith-java/commit/3bc61733286b179f5567439dd65cf94e6faff571))
* **stainless:** skip sessions resource for python and typescript ([d71cff9](https://github.com/langchain-ai/langsmith-java/commit/d71cff9f592c797a6b7383755ebfde6feea7e7c6))
### Bug Fixes
* remove duplicate threads/traces declarations in client impls ([#35](https://github.com/langchain-ai/langsmith-java/issues/35)) ([fdba8e6](https://github.com/langchain-ai/langsmith-java/commit/fdba8e6123bda467312acd260ef7cf20e21dba9b))
* **smith-sdks:** remove dead retrieve_thread_preview SDK mapping ([585b12d](https://github.com/langchain-ai/langsmith-java/commit/585b12dd2672808df97c6221c783342e17dd6755))
* **smith-sdks:** rename dataset runs POST methods from create to query ([4513b3f](https://github.com/langchain-ai/langsmith-java/commit/4513b3ffb05b51ec5be3e5f394d312726de1dd13))
### Chores
* **stainless:** skip all datasets endpoints in Python/TS SDKs except experiment-runs ([6ab6fa2](https://github.com/langchain-ai/langsmith-java/commit/6ab6fa281f3f9db790121a94d20a32323e56dc80))
## [0.1.0-beta.11](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.10...v0.1.0-beta.11) (2026-07-02)
### ⚠ BREAKING CHANGES
* disallow run_count session sort
### Bug Fixes
* restore title/additionalProperties on v2 RunResponse JSON fields for Stainless ([e3b4784](https://github.com/langchain-ai/langsmith-java/commit/e3b478437ef307d6895f0256b4945e68f5e32a23))
### Refactors
* disallow run_count session sort ([a00b1f1](https://github.com/langchain-ai/langsmith-java/commit/a00b1f18ca2c9b6048f4789df0da60704852a79a))
## [0.1.0-beta.10](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.9...v0.1.0-beta.10) (2026-07-01)
### Features
* **evaluation:** add core evaluate() param parity ([#17](https://github.com/langchain-ai/langsmith-java/issues/17)) ([7d9a2fa](https://github.com/langchain-ai/langsmith-java/commit/7d9a2fafd45c60dbe11852c1c4c74565c4517595))
### Bug Fixes
* **ci:** add GH_REPO to release-please trigger publish step ([#24](https://github.com/langchain-ai/langsmith-java/issues/24)) ([10a1f9b](https://github.com/langchain-ai/langsmith-java/commit/10a1f9bcbbf662e048b4e4f0a1bb4e9a29f18bc7))
* update hand-written code to use RunIngest after stainless rename ([#25](https://github.com/langchain-ai/langsmith-java/issues/25)) ([4d45358](https://github.com/langchain-ai/langsmith-java/commit/4d45358e0ef8c0df8905cc3f927a495af2bcbeca))
### Chores
* **examples:** dual-write source run lookup coordinates in Python ([2d4900b](https://github.com/langchain-ai/langsmith-java/commit/2d4900b16bdda9512fa71368dd03f4729f55620a))
* **stainless:** rename run models in runs resource ([2f0ce40](https://github.com/langchain-ai/langsmith-java/commit/2f0ce40b862dc33d52ff8cb0fb8d50fde790ef73))
## [0.1.0-beta.9](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.8...v0.1.0-beta.9) (2026-06-29)
### ⚠ BREAKING CHANGES
* **api:** document v2 experiment-runs and de-publicize legacy dataset endpoints
* **backend:** accept float value for percentile metric [LS-000]
### Features
* add GET endpoint to retrieve single workspace by id [closes ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
* add GET endpoint to retrieve single workspace by id [closes ENT-508] ([cdbd369](https://github.com/langchain-ai/langsmith-java/commit/cdbd36936fde80c7c3dea773deefd2789d0feecf))
* **api:** document v2 experiment-runs and de-publicize legacy dataset endpoints ([387526f](https://github.com/langchain-ai/langsmith-java/commit/387526fd3ac9e892de0786be5b538c53d2f40049))
* **evaluation:** add Java evaluate orchestrator ([#8](https://github.com/langchain-ai/langsmith-java/issues/8)) ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
* **evaluation:** add Java evaluate orchestrator ([#8](https://github.com/langchain-ai/langsmith-java/issues/8)) ([d1420ef](https://github.com/langchain-ai/langsmith-java/commit/d1420efd7a9c307d8b99d64ac57e3f0f26b1580b))
* **sandbox-host:** per-VM cgroups for CPU bursting (2x), fractional ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
* **sandbox-host:** per-VM cgroups for CPU bursting (2x), fractional vCPU, and sustained-burst limiting ([49bf68d](https://github.com/langchain-ai/langsmith-java/commit/49bf68d74763138fa7894d0e04eb1b73e72e82b3))
* **sandboxes:** expose registry CRUD and generate Python/TypeScript SDK ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
* **sandboxes:** expose registry CRUD and generate Python/TypeScript SDK clients ([cfed196](https://github.com/langchain-ai/langsmith-java/commit/cfed196979dce85b747a4171cb1dcacb38f88641))
### Bug Fixes
* **backend:** accept float value for percentile metric [LS-000] ([e885608](https://github.com/langchain-ai/langsmith-java/commit/e88560861c9f3de50e7448b97d2cde1cfdfbca46))
* trigger publish-sonatype workflow after release-please creates a ([34f1c52](https://github.com/langchain-ai/langsmith-java/commit/34f1c52fc1088310910de4f21670e2a4d41d196b))
* trigger publish-sonatype workflow after release-please creates a release ([#13](https://github.com/langchain-ai/langsmith-java/issues/13)) ([70edc45](https://github.com/langchain-ai/langsmith-java/commit/70edc4514cfc54a98fbf5605795a386b8149300d))
## [0.1.0-beta.8](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.7...v0.1.0-beta.8) (2026-06-25)
### ⚠ BREAKING CHANGES
* **sandboxes:** default S3 mount endpoint
* **sandboxes:** add ArtifactFS git mounts
* **sandboxes:** add gcs bucket mounts
### Features
* **abac:** add tag_value_ids to dataset creation endpoints ([280a374](https://github.com/langchain-ai/langsmith-java/commit/280a37489da492957d07773692e5ae62140d98d0))
* **abac:** add tag_value_ids to prompt creation endpoints [ENT-1176] ([f3ce273](https://github.com/langchain-ai/langsmith-java/commit/f3ce273a028c0de24171835d854edb4c69268d91))
* **abac:** add tag_value_ids to tracer session creation ([f859585](https://github.com/langchain-ai/langsmith-java/commit/f859585592c616310f02f91d8f0f6822ab9be17d))
* add online evaluator endpoints to stainless config ([77d19d3](https://github.com/langchain-ai/langsmith-java/commit/77d19d324195ca6a93b51ea74aaa4484fb7a2cef))
* adding time to first token for experiment metrics ([5a6bddd](https://github.com/langchain-ai/langsmith-java/commit/5a6bddd241bb95b0d176d70ae36c810a8157177d))
* **backend:** allow retrieving and storing V2 charts in database [LSO-2799] ([b86e854](https://github.com/langchain-ai/langsmith-java/commit/b86e854687089b26205de336c8c99d994fd57369))
* end-to-end OAuth bearer for Playground [part of ENT-760] ([77f12e3](https://github.com/langchain-ai/langsmith-java/commit/77f12e3a6e6d627a0e2309ecd35396abb01448ac))
* **evaluation:** Java evaluate() primitives ([#7](https://github.com/langchain-ai/langsmith-java/issues/7)) ([a82b5c8](https://github.com/langchain-ai/langsmith-java/commit/a82b5c88fc08de041e78657d368c2b0359196248))
* **fleet:** frontend passes typed fields (closes AB-000) ([bbbcf7b](https://github.com/langchain-ai/langsmith-java/commit/bbbcf7bf4027f95b13f2a0bffe18564da3fcfbe9))
* **group-stats:** back thread stats with SmithDB ([7f24878](https://github.com/langchain-ai/langsmith-java/commit/7f24878418347b187fbc2c7064c52725de775c8b))
* **hub:** add include_owners to repos list for Fleet [closes AB-2537] ([10f1d65](https://github.com/langchain-ai/langsmith-java/commit/10f1d6506332402672366470117774db374350a6))
* make online evaluator retention opt-in (backend) ([e480cd4](https://github.com/langchain-ai/langsmith-java/commit/e480cd4104a22b71607aca0f209b416320fe62e0))
* **run-rules:** per-action trace-retention control for automations [LSO-2749] ([3c218e9](https://github.com/langchain-ai/langsmith-java/commit/3c218e98bc255847a60b75499d9230372468adb6))
* **runs/v2:** expose runs v2 endpoints publicly with stainless config ([48c283c](https://github.com/langchain-ai/langsmith-java/commit/48c283c7a7448b03b52185053a578027005bbdbd))
* **sandboxes:** add ArtifactFS git mounts ([2ddc687](https://github.com/langchain-ai/langsmith-java/commit/2ddc687610df41bf15429b2492fd85cbcff86472))
* **sandboxes:** add GCP proxy auth flow ([fd2980c](https://github.com/langchain-ai/langsmith-java/commit/fd2980ce69f3be63a0fd44922f08f72a0a6d9ac2))
* **sandboxes:** add gcs bucket mounts ([87e3acb](https://github.com/langchain-ai/langsmith-java/commit/87e3acbb7b1d871a44e82472ff7fbce8669395de))
* **sandboxes:** add sandbox env var support ([da0fc7b](https://github.com/langchain-ai/langsmith-java/commit/da0fc7b507828609f0264e90a50f6f8910ce9567))
* **sandboxes:** filter lists by creator [INF-1492] ([bb9f973](https://github.com/langchain-ai/langsmith-java/commit/bb9f973f41a930ed23d33ee42deeaaaba4d07c35))
* **sandboxes:** move mounts under mount_config ([734a828](https://github.com/langchain-ai/langsmith-java/commit/734a8285cd37d10e9676e67ae8fa98b6420514b0))
* **sandboxes:** proxy runtime routes through platform backend [INF-0000] ([7cb70fc](https://github.com/langchain-ai/langsmith-java/commit/7cb70fc3e3dc71788c9ad66360dbcf4d577a098d))
* **sandboxes:** resolve AWS proxy rule secrets ([cb519f9](https://github.com/langchain-ai/langsmith-java/commit/cb519f93d44d0a34633cae88c0fa79a4c3db182b))
* **sandboxes:** set AWS proxy compatibility env ([6bbe522](https://github.com/langchain-ai/langsmith-java/commit/6bbe522f0a167e6671c5528eec2fa95dde0b680e))
* **sandboxes:** snapshot memory from stopped boxes + honor restore_memory in v2 [INF-0000] ([79647d1](https://github.com/langchain-ai/langsmith-java/commit/79647d1579da9cdc6027f7922075ddb489e9601a))
* **stats:** add include_details param to /runs/stats for backwards compat ([49039f2](https://github.com/langchain-ai/langsmith-java/commit/49039f277018a60f46af85108a30815609c8482c))
### Bug Fixes
* **evaluators:** show code evaluator trace counts [LSE-2359] ([3f3a0c9](https://github.com/langchain-ai/langsmith-java/commit/3f3a0c9c30fc61198a19b27c7b3c6b8e5aa444c6))
* **sandboxes:** default S3 mount endpoint ([54d105e](https://github.com/langchain-ai/langsmith-java/commit/54d105ef91e0c65de0f2a77ccc7e794570da15cb))
* **sandboxes:** use built-in gcp proxy host matching ([ac8da0f](https://github.com/langchain-ai/langsmith-java/commit/ac8da0f77c706c7fc072842d5ec4e95b4f82a2d7))
* **tests:** remove duplicate retrieve() method in RunServiceTest ([#10](https://github.com/langchain-ai/langsmith-java/issues/10)) ([ba419de](https://github.com/langchain-ai/langsmith-java/commit/ba419de13e4749f8427a98b1572bc1764404dd20))
### Chores
* add release-please workflow ([#11](https://github.com/langchain-ai/langsmith-java/issues/11)) ([65b5b9a](https://github.com/langchain-ai/langsmith-java/commit/65b5b9ae402be8b84253d5ca7c255bd188c0e415))
* add stlc sync and promotion workflows ([#5](https://github.com/langchain-ai/langsmith-java/issues/5)) ([f00fef5](https://github.com/langchain-ai/langsmith-java/commit/f00fef565e6c5c5edfa651223790e8e9cf90c317))
* **fleet:** add param to list threads [closes AB-2522] ([8b85e16](https://github.com/langchain-ai/langsmith-java/commit/8b85e16472982a2821e82a6677c843d0565d17f3))
* **smith-sdks:** update Java SDK stainless custom-code tracking file ([17bb73a](https://github.com/langchain-ai/langsmith-java/commit/17bb73a0061627a86d2ed1dba19316dcdfaf9c6d))
### Refactors
* **playground:** remove legacy experiment endpoints [LSO-2230] ([370b940](https://github.com/langchain-ai/langsmith-java/commit/370b940a1ca252f0198b49d315749dc5d9282714))
* rename field to match prev trace retention fields ([b963cab](https://github.com/langchain-ai/langsmith-java/commit/b963cab6c63f10bed9a7a1237b6df12c111be9d4))
## 0.1.0-beta.7 (2026-05-27)
Full Changelog: [v0.1.0-beta.6...v0.1.0-beta.7](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.6...v0.1.0-beta.7)
### Features
* **api:** api update ([cad4993](https://github.com/langchain-ai/langsmith-java/commit/cad499356e3a38d660814b7ceed92eafbcf2989d))
* **api:** api update ([84e287c](https://github.com/langchain-ai/langsmith-java/commit/84e287ca9be34e45b80fd0d33c3ebc1dd1ca0f32))
* **api:** api update ([ce44c3b](https://github.com/langchain-ai/langsmith-java/commit/ce44c3b540624598c9152b8d1d469bb28ec5c67e))
* **api:** api update ([81f7b39](https://github.com/langchain-ai/langsmith-java/commit/81f7b3937992f117bc573e968e9d5988c5bb5399))
* **api:** api update ([3d0c992](https://github.com/langchain-ai/langsmith-java/commit/3d0c992f4c4772d5a3e294ebeba2b28a641b21ed))
* **api:** api update ([b991c00](https://github.com/langchain-ai/langsmith-java/commit/b991c00c14ee68cffd2df17c6d6bdbc6c780cfd1))
* **api:** api update ([104d3c6](https://github.com/langchain-ai/langsmith-java/commit/104d3c65623d66f24f90589df3ee04586fa8d913))
* **api:** api update ([e01a424](https://github.com/langchain-ai/langsmith-java/commit/e01a42416bef9d17a897900e70cbef70e13ad552))
* **api:** api update ([e1443b6](https://github.com/langchain-ai/langsmith-java/commit/e1443b69ab955cbdcee618449f11bc3cdd41add4))
* **api:** api update ([9deafad](https://github.com/langchain-ai/langsmith-java/commit/9deafade612c0e7c163bf13fae4c2d27751122be))
* **api:** api update ([7e7c479](https://github.com/langchain-ai/langsmith-java/commit/7e7c479912229f27ae96878f2e939971b5fff829))
* **api:** api update ([b5a3200](https://github.com/langchain-ai/langsmith-java/commit/b5a32009fc34eb5a04d70205c19bf973414d800f))
* **api:** api update ([3ab0c6c](https://github.com/langchain-ai/langsmith-java/commit/3ab0c6c5482c2378fc1d2a2b8657248ec218a501))
* **client:** support proxy authentication ([d9e3ab5](https://github.com/langchain-ai/langsmith-java/commit/d9e3ab5b34a8fc2321fb1f9f86910517ec30dbdb))
* **sdk:** add hub directories endpoints to Stainless config ([b3dbb81](https://github.com/langchain-ai/langsmith-java/commit/b3dbb81bd63f8b21fce99d345cf9c73345626451))
### Bug Fixes
* **go:** avoid panic when http.DefaultTransport is wrapped ([aba9578](https://github.com/langchain-ai/langsmith-java/commit/aba9578496866a723d85540da2d9f9bc6418027a))
* **go:** avoid panic when http.DefaultTransport is wrapped ([9ea7653](https://github.com/langchain-ai/langsmith-java/commit/9ea76536a054185cbfdad5ddad7036b2e5d46137))
* restore dokka-javadoc plugin application to subprojects ([#157](https://github.com/langchain-ai/langsmith-java/issues/157)) ([1df1c4a](https://github.com/langchain-ai/langsmith-java/commit/1df1c4aacd6c3cc9477576403a1da641bd20302d))
### Performance Improvements
* **client:** create one json mapper ([e68229b](https://github.com/langchain-ai/langsmith-java/commit/e68229b7009315bf60149df92e4af5f0b083c012))
### Chores
* **internal:** codegen related update ([5e551de](https://github.com/langchain-ai/langsmith-java/commit/5e551de99e03fb34a737f3fcac3ee6a08fab8863))
* **internal:** renormalize gradlew.bat line endings ([4738606](https://github.com/langchain-ai/langsmith-java/commit/4738606db159ecc8ad5088714e3100c0e7972f52))
* remove duplicated dokka setup ([b1af8d8](https://github.com/langchain-ai/langsmith-java/commit/b1af8d842d8aa3b1a47cb00ffa22d71134b0d883))
### Documentation
* clarify forwards compat behavior ([d4b4164](https://github.com/langchain-ai/langsmith-java/commit/d4b41649cb18523851c353c6a47b83b492e0c8e5))
## 0.1.0-beta.6 (2026-05-20)
Full Changelog: [v0.1.0-beta.5...v0.1.0-beta.6](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.5...v0.1.0-beta.6)
### Bug Fixes
* patch security alerts — OTel, Tomcat, Jetty, Logback, Spring, SnakeYAML ([#153](https://github.com/langchain-ai/langsmith-java/issues/153)) ([dfa5469](https://github.com/langchain-ai/langsmith-java/commit/dfa54697292c14a987320ba776b580ee25c58f94))
## 0.1.0-beta.5 (2026-05-13)
Full Changelog: [v0.1.0-beta.4...v0.1.0-beta.5](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.4...v0.1.0-beta.5)
### Features
* **sandbox:** make snapshot optional ([#152](https://github.com/langchain-ai/langsmith-java/issues/152)) ([5849b16](https://github.com/langchain-ai/langsmith-java/commit/5849b1695ec4504460205c80cabf8d443fd8a5f0))
### Chores
* **deps:** bump actions/github-script from 8 to 9 ([#144](https://github.com/langchain-ai/langsmith-java/issues/144)) ([c3f8fbd](https://github.com/langchain-ai/langsmith-java/commit/c3f8fbd5449fb5f52529166fd9b2db5eb8dbe24a))
* **deps:** bump gradle/actions ([#143](https://github.com/langchain-ai/langsmith-java/issues/143)) ([fd8a8ae](https://github.com/langchain-ai/langsmith-java/commit/fd8a8aef399ba3784c6d459c758e7a06f6ee5e98))
* **deps:** bump the minor-and-patch group across 1 directory with 3 updates ([#145](https://github.com/langchain-ai/langsmith-java/issues/145)) ([c4a883d](https://github.com/langchain-ai/langsmith-java/commit/c4a883dfd2e89373180f88c7149c13ef426996ee))
## 0.1.0-beta.4 (2026-05-04)
Full Changelog: [v0.1.0-beta.3...v0.1.0-beta.4](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.3...v0.1.0-beta.4)
### Features
* **api:** api update ([4dc8a79](https://github.com/langchain-ai/langsmith-java/commit/4dc8a7912f8b1b449ad65c24bdf74b667dde30f4))
## 0.1.0-beta.3 (2026-05-01)
Full Changelog: [v0.1.0-beta.2...v0.1.0-beta.3](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.2...v0.1.0-beta.3)
## 0.1.0-beta.2 (2026-04-30)
Full Changelog: [v0.1.0-beta.1...v0.1.0-beta.2](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.1...v0.1.0-beta.2)
### Bug Fixes
* Fix another build issue ([#141](https://github.com/langchain-ai/langsmith-java/issues/141)) ([46baa8a](https://github.com/langchain-ai/langsmith-java/commit/46baa8a73e2770e190966d237996135b7fb6b0a3))
## 0.1.0-beta.1 (2026-04-30)
Full Changelog: [v0.1.0-beta.0...v0.1.0-beta.1](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-beta.0...v0.1.0-beta.1)
### Chores
* Fix build ([#139](https://github.com/langchain-ai/langsmith-java/issues/139)) ([cbaa348](https://github.com/langchain-ai/langsmith-java/commit/cbaa3480cd21c2f90a0056a575e977af1bf906b6))
## 0.1.0-beta.0 (2026-04-30)
Full Changelog: [v0.1.0-alpha.28...v0.1.0-beta.0](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.28...v0.1.0-beta.0)
### Features
* Add autobatch tracing mode ([#134](https://github.com/langchain-ai/langsmith-java/issues/134)) ([230959e](https://github.com/langchain-ai/langsmith-java/commit/230959e124ec705648c4cd1e48da915d945154b8))
* Add multipart batching ([#138](https://github.com/langchain-ai/langsmith-java/issues/138)) ([288221c](https://github.com/langchain-ai/langsmith-java/commit/288221c44835d472eb9f0ff59d1f22d0ec150ccb))
* Add zstd compression to runs service ([#136](https://github.com/langchain-ai/langsmith-java/issues/136)) ([e412191](https://github.com/langchain-ai/langsmith-java/commit/e4121913a5d5a8a6de2a16e272925c947694e7c0))
* **api:** add workspaces ([cc88cdf](https://github.com/langchain-ai/langsmith-java/commit/cc88cdf706b9f2ff7a1e66e00273af027c60f203))
* **api:** api update ([c3252b8](https://github.com/langchain-ai/langsmith-java/commit/c3252b8becd6ee89c95dc6ae39bf701d76629e18))
* **api:** api update ([af1ba52](https://github.com/langchain-ai/langsmith-java/commit/af1ba5242ec894847e0dc007674681858cb37a42))
* **api:** api update ([e1cca2b](https://github.com/langchain-ai/langsmith-java/commit/e1cca2b004858451074865be8b9fb4e532f2f5f5))
* **api:** api update ([fd1fc58](https://github.com/langchain-ai/langsmith-java/commit/fd1fc5830a739a88c221e682d1520612c0e45e66))
* **api:** api update ([b487509](https://github.com/langchain-ai/langsmith-java/commit/b4875093d10d752fcc8c3757e33d6c2bbe334c15))
* **api:** api update ([0758583](https://github.com/langchain-ai/langsmith-java/commit/0758583e5cc42b465c662a2897fff92a6fd61394))
* **api:** api update ([1310303](https://github.com/langchain-ai/langsmith-java/commit/131030331c0bab2568498a2bff96112f0fe46fee))
* **api:** manual updates ([0be4215](https://github.com/langchain-ai/langsmith-java/commit/0be4215e63ce63ee562ce1c48b001a6848cce704))
* **api:** manual updates ([f35b312](https://github.com/langchain-ai/langsmith-java/commit/f35b31249d59265e74d6d33f4e3298fe416757ae))
* **api:** sandbox apis ([bd21a1d](https://github.com/langchain-ai/langsmith-java/commit/bd21a1df96b708ac04712aaed7ef1a95505cf1f1))
* **evaluators:** add list evaluators (GET /api/v1/runs/rules) ([be32489](https://github.com/langchain-ai/langsmith-java/commit/be324893c09e4ab6e2176dda1d2f6a6cf15413e6))
* Make autobatch queue denote batches by payload size, add aggregation delay ([#137](https://github.com/langchain-ai/langsmith-java/issues/137)) ([2fb72ae](https://github.com/langchain-ai/langsmith-java/commit/2fb72ae9681964dd2e798642d88ded506c8189b5))
* Merge enqueued posts and patches to optimize batching ([#135](https://github.com/langchain-ai/langsmith-java/issues/135)) ([d65e301](https://github.com/langchain-ai/langsmith-java/commit/d65e3019910bb077ea59056aa37c9d0ef30bfb1a))
* support setting headers via env ([567af0c](https://github.com/langchain-ai/langsmith-java/commit/567af0c1eba8bf2dc6648529c11260f36e586bd3))
### Chores
* **deps:** bump the minor-and-patch group across 1 directory with 5 updates ([#123](https://github.com/langchain-ai/langsmith-java/issues/123)) ([1f88790](https://github.com/langchain-ai/langsmith-java/commit/1f887908d8d851dc641905b7beb8cf4f7cf935b1))
## 0.1.0-alpha.28 (2026-04-14)
Full Changelog: [v0.1.0-alpha.27...v0.1.0-alpha.28](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.27...v0.1.0-alpha.28)
### Features
* **api:** api update ([35484cf](https://github.com/langchain-ai/langsmith-java/commit/35484cf9b1f1ed6642ef857d3fb4aefdb37ae369))
## 0.1.0-alpha.27 (2026-04-14)
Full Changelog: [v0.1.0-alpha.26...v0.1.0-alpha.27](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.26...v0.1.0-alpha.27)
## 0.1.0-alpha.26 (2026-04-13)
Full Changelog: [v0.1.0-alpha.25...v0.1.0-alpha.26](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.25...v0.1.0-alpha.26)
### Features
* Allow manually specifying parent run tree in traceable config ([#126](https://github.com/langchain-ai/langsmith-java/issues/126)) ([a1409fb](https://github.com/langchain-ai/langsmith-java/commit/a1409fb0d7c0319d4129daea5300578e4371a858))
* **api:** api update ([8198ce7](https://github.com/langchain-ai/langsmith-java/commit/8198ce7001d0656bcb4807331d385b0eb8b81a75))
* **api:** api update ([286a62f](https://github.com/langchain-ai/langsmith-java/commit/286a62fba8b5bac8105c71dc52b4a36a37012eae))
### Chores
* **deps:** bump gradle-wrapper from 8.12 to 9.4.1 ([#124](https://github.com/langchain-ai/langsmith-java/issues/124)) ([78b7db0](https://github.com/langchain-ai/langsmith-java/commit/78b7db0e1f08c6de8c12af504dd098cf7c958faa))
* **deps:** bump gradle/actions ([#121](https://github.com/langchain-ai/langsmith-java/issues/121)) ([e3ca5d7](https://github.com/langchain-ai/langsmith-java/commit/e3ca5d7b712f92ce72586bfd7be208c73cec332f))
* **deps:** bump gradle/gradle-build-action ([#122](https://github.com/langchain-ai/langsmith-java/issues/122)) ([d91305f](https://github.com/langchain-ai/langsmith-java/commit/d91305f9157b8d73dfaf8a925c7b2b0e5743f4b2))
### Documentation
* improve examples ([1556365](https://github.com/langchain-ai/langsmith-java/commit/1556365dd6c04f3290b9a836b3c28c09056be5ad))
## 0.1.0-alpha.25 (2026-03-31)
Full Changelog: [v0.1.0-alpha.24...v0.1.0-alpha.25](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.24...v0.1.0-alpha.25)
### Features
* Add `traceable` function wrapper for LangSmith tracing ([#101](https://github.com/langchain-ai/langsmith-java/issues/101)) ([f3bf340](https://github.com/langchain-ai/langsmith-java/commit/f3bf3404fe56be9801806ea44f283eb586d96ce8))
* Adds package version resource to build ([#100](https://github.com/langchain-ai/langsmith-java/issues/100)) ([31cc003](https://github.com/langchain-ai/langsmith-java/commit/31cc0037be79a336cd86f6d53c7093f6c3fb7034))
* Adds processInputs and processOutputs to traceable ([#113](https://github.com/langchain-ai/langsmith-java/issues/113)) ([ee2991f](https://github.com/langchain-ai/langsmith-java/commit/ee2991f830703f29b3d0eddfd621109362a465e5))
* Adds streaming support for wrapOpenAI ([#118](https://github.com/langchain-ai/langsmith-java/issues/118)) ([aae9d2c](https://github.com/langchain-ai/langsmith-java/commit/aae9d2c0dc7f7dae40960113154d9ddff3bd0e06))
* Adds support for tracing streams with traceable ([#117](https://github.com/langchain-ai/langsmith-java/issues/117)) ([0217521](https://github.com/langchain-ai/langsmith-java/commit/0217521b852c70dba4795b145b3eb7b16fd2d10f))
* **api:** api update ([9182ef8](https://github.com/langchain-ai/langsmith-java/commit/9182ef84e304c602899c73b6865473d3ade4953f))
* **api:** api update ([0c91488](https://github.com/langchain-ai/langsmith-java/commit/0c914884473e1de2877708cd92db30fdc8fc10aa))
* **api:** api update ([efa16fc](https://github.com/langchain-ai/langsmith-java/commit/efa16fcb5974fbd5500714936cb52a6e930a6181))
* **api:** api update ([979e8af](https://github.com/langchain-ai/langsmith-java/commit/979e8af2b6f01444bcea33e1e09d1acbc986a6be))
* **api:** api update ([cd9f555](https://github.com/langchain-ai/langsmith-java/commit/cd9f555e525172d5ba0bcb0cd033dc045a9026bd))
* **api:** api update ([af93e09](https://github.com/langchain-ai/langsmith-java/commit/af93e09dc9dd8597454b2a02b1ed37de4e71fde4))
* **api:** manual updates ([493d5e9](https://github.com/langchain-ai/langsmith-java/commit/493d5e9b2844ff160598b87602c9f91733722fe4))
* Rework wrapOpenAI ([#114](https://github.com/langchain-ai/langsmith-java/issues/114)) ([63e0bdc](https://github.com/langchain-ai/langsmith-java/commit/63e0bdc5c893185399065c2c602ed0d5c46bbc68))
### Bug Fixes
* Fix format detection when pulling legacy prompts ([#115](https://github.com/langchain-ai/langsmith-java/issues/115)) ([ea6c3d2](https://github.com/langchain-ai/langsmith-java/commit/ea6c3d29d7c726b7b8455e6e93ec3322330ea5e7))
* patch security alerts — bump Jackson and constrain vulnerable transitive deps ([#119](https://github.com/langchain-ai/langsmith-java/issues/119)) ([dfe092c](https://github.com/langchain-ai/langsmith-java/commit/dfe092c51ab27c3131b81785321664a75d584764))
### Chores
* **deps:** bump gradle/actions from 5 to 6 in the all-actions group ([#99](https://github.com/langchain-ai/langsmith-java/issues/99)) ([4ba75e7](https://github.com/langchain-ai/langsmith-java/commit/4ba75e7776499833eddb1959b194b71eb847840f))
* fix dependabot.yml posture issues ([#105](https://github.com/langchain-ai/langsmith-java/issues/105)) ([a0e5441](https://github.com/langchain-ai/langsmith-java/commit/a0e544170d0c62e340a6250c32b6b521f818eaa4))
## 0.1.0-alpha.24 (2026-03-25)
Full Changelog: [v0.1.0-alpha.23...v0.1.0-alpha.24](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.23...v0.1.0-alpha.24)
### Features
* **api:** api update ([8e94d9d](https://github.com/langchain-ai/langsmith-java/commit/8e94d9dc65dfacd3f238e88631623a1338edec2e))
* **api:** api update ([3288052](https://github.com/langchain-ai/langsmith-java/commit/32880529fe17d465d20ee922894a9e1af3351edd))
* **api:** api update ([a4e3e5c](https://github.com/langchain-ai/langsmith-java/commit/a4e3e5cb858e66a9409ac162f04660c514cc24df))
* **api:** api update ([ff16250](https://github.com/langchain-ai/langsmith-java/commit/ff162506f81ad1616567c9119ded36c1a344c774))
* **api:** api update ([ef7c7a7](https://github.com/langchain-ai/langsmith-java/commit/ef7c7a798786b073e403d994ee2a9367d4db59ee))
* **api:** api update ([643c7b1](https://github.com/langchain-ai/langsmith-java/commit/643c7b121acd874a72c36e80fbd6d8d4aa772319))
* **api:** api update ([4b9c97d](https://github.com/langchain-ai/langsmith-java/commit/4b9c97d873492f919885fc1e394d1819fbedd09a))
* **api:** api update ([ea4e843](https://github.com/langchain-ai/langsmith-java/commit/ea4e843eea1709113fd259659c2cc85fd6317fa5))
* **api:** api update ([74d2131](https://github.com/langchain-ai/langsmith-java/commit/74d213181b7a2398fa8852235de5a1123ff0a7fe))
* **api:** api update ([2708533](https://github.com/langchain-ai/langsmith-java/commit/27085335b36f0ecb249a3bd079f96d8d9fad6c1f))
* **api:** api update ([8f48677](https://github.com/langchain-ai/langsmith-java/commit/8f48677e5ca0dfd9c9e9bc248c204863f233542a))
* **api:** api update ([c11821f](https://github.com/langchain-ai/langsmith-java/commit/c11821f56ba08939568332468a1e9376c78e6372))
* **api:** api update ([44fe8b3](https://github.com/langchain-ai/langsmith-java/commit/44fe8b31986af7ae7ac489c0a59335afb8021f9f))
* **api:** api update ([d11c759](https://github.com/langchain-ai/langsmith-java/commit/d11c759b1cf0d360f07aa0807f554c36f3262b14))
* **api:** manual updates ([09f1302](https://github.com/langchain-ai/langsmith-java/commit/09f1302329d0fcd9082bc52b2381271aabd71bab))
* **api:** manual updates ([dbed70a](https://github.com/langchain-ai/langsmith-java/commit/dbed70af760f0e807626650a2a7d6ecb8d30f0ad))
* **api:** manual updates ([48e2ca2](https://github.com/langchain-ai/langsmith-java/commit/48e2ca294bd412398290777db3ef14655ed47b1c))
* **api:** manual updates ([d8ac870](https://github.com/langchain-ai/langsmith-java/commit/d8ac87089db42004a53e880f419dcf6749c60817))
* **api:** manual updates ([bdb35f0](https://github.com/langchain-ai/langsmith-java/commit/bdb35f01f6374860fe6fa3c83f6f102f22590b23))
### Bug Fixes
* **client:** allow updating header/query affecting fields in `toBuilder()` ([5069881](https://github.com/langchain-ai/langsmith-java/commit/5069881aebcffc8ea2058de667a7af33efc15ec3))
* **client:** incorrect `Retry-After` parsing ([b607264](https://github.com/langchain-ai/langsmith-java/commit/b607264e2c7e10374f5d79d922c6850208b6f839))
* fix streaming in java sdk ([#95](https://github.com/langchain-ai/langsmith-java/issues/95)) ([76da93f](https://github.com/langchain-ai/langsmith-java/commit/76da93f2c0c3a0466feee196243a214e8642b303))
### Chores
* align user agent format ([#96](https://github.com/langchain-ai/langsmith-java/issues/96)) ([5cf06d6](https://github.com/langchain-ai/langsmith-java/commit/5cf06d612963f01b49d6f62117226d948e004046))
* **ci:** skip lint on metadata-only changes ([4c1197c](https://github.com/langchain-ai/langsmith-java/commit/4c1197ce483f317defed85f81d3ec3f5053f68d3))
* **ci:** skip uploading artifacts on stainless-internal branches ([c994122](https://github.com/langchain-ai/langsmith-java/commit/c994122d8c4eea78bb8a942b0ed1db58e6338e1e))
* **deps:** bump the all-actions group with 5 updates ([#92](https://github.com/langchain-ai/langsmith-java/issues/92)) ([3a1a72f](https://github.com/langchain-ai/langsmith-java/commit/3a1a72f5595393a437a8ddf1acc69bb44529b0e8))
* drop apache dependency ([75ca9d7](https://github.com/langchain-ai/langsmith-java/commit/75ca9d71b49bad02bdbd66141f693204b296a9ec))
* **internal:** bump ktfmt ([d9a4a39](https://github.com/langchain-ai/langsmith-java/commit/d9a4a396ad92c9f33c5922dabc00a8ba2451199b))
* **internal:** bump palantir-java-format ([576ebd2](https://github.com/langchain-ai/langsmith-java/commit/576ebd2a02613c632dd1e00ba9c606df99de8f37))
* **internal:** codegen related update ([29d55b0](https://github.com/langchain-ai/langsmith-java/commit/29d55b0ee9f40c2d327f1c08a714ccd9cad1384b))
* **internal:** expand imports ([4c7542f](https://github.com/langchain-ai/langsmith-java/commit/4c7542f4525b43d8fc2adcd7d084a080581b5c48))
* **internal:** tweak CI branches ([b7cecc1](https://github.com/langchain-ai/langsmith-java/commit/b7cecc1caa774426df72acd5734a2c29a96a3e60))
* **internal:** update gitignore ([d0df3fa](https://github.com/langchain-ai/langsmith-java/commit/d0df3fa13a923514d9d3c66d5d96b4fa1fea4a22))
* **internal:** update retry delay tests ([a9c7999](https://github.com/langchain-ai/langsmith-java/commit/a9c7999f55bc12c2d1bac268ce3a2ca5bbd58dc5))
* make `Properties` more resilient to `null` ([47c70f9](https://github.com/langchain-ai/langsmith-java/commit/47c70f91d5ec941fafc08a81baabf946602076c9))
* remove old test ([#97](https://github.com/langchain-ai/langsmith-java/issues/97)) ([79d708b](https://github.com/langchain-ai/langsmith-java/commit/79d708b935ba234c50d10980a7fef8ad12c58f61))
* update placeholder string ([be1baa7](https://github.com/langchain-ai/langsmith-java/commit/be1baa704ad8ebe899fbf8e4003f706fbc3d3f73))
## 0.1.0-alpha.23 (2026-02-20)
Full Changelog: [v0.1.0-alpha.22...v0.1.0-alpha.23](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.22...v0.1.0-alpha.23)
### Features
* **api:** api update ([9485112](https://github.com/langchain-ai/langsmith-java/commit/948511234707031e0e375ded4ed51cd142d0dae1))
* **api:** api update ([786edff](https://github.com/langchain-ai/langsmith-java/commit/786edfff5f8e200c20fb201d83daff8e0fe33b59))
* **api:** api update ([3a6c191](https://github.com/langchain-ai/langsmith-java/commit/3a6c1911a5127afe550cc6ff0cbc2182e7e9cce4))
* **api:** api update ([cd9f91d](https://github.com/langchain-ai/langsmith-java/commit/cd9f91d07e8e0c1c7d13f7a6a849856a9da19d90))
* **api:** api update ([b344828](https://github.com/langchain-ai/langsmith-java/commit/b3448285414b13fe8081c7b7eae023e033ad6ad2))
* **api:** api update ([2e0602a](https://github.com/langchain-ai/langsmith-java/commit/2e0602a9e218f3c9c435c7d1e254621d8f97f14b))
* **api:** api update ([3b1e4c6](https://github.com/langchain-ai/langsmith-java/commit/3b1e4c61e3b8ff44f0415aa808e6a7f550a7834b))
* **api:** api update ([703b77a](https://github.com/langchain-ai/langsmith-java/commit/703b77ac2c34ed600964d0c815b1fa759124756c))
* **api:** api update ([7d0f1d4](https://github.com/langchain-ai/langsmith-java/commit/7d0f1d4e07b0bd50a92947124e2bd1b83b840605))
* **api:** api update ([debadf0](https://github.com/langchain-ai/langsmith-java/commit/debadf0da6c073a569e1abea1c3b58fc8e33bcf4))
* **api:** api update ([9ebd84f](https://github.com/langchain-ai/langsmith-java/commit/9ebd84fe982b0a0304d1fcb7da168df0c97a043f))
* **api:** api update ([88bd850](https://github.com/langchain-ai/langsmith-java/commit/88bd85062b8c87a72f96d6fbac738963c1df562e))
* **api:** api update ([af7bf93](https://github.com/langchain-ai/langsmith-java/commit/af7bf93c5b285d509484c5f8cb838583dd0ed7e8))
* **api:** manual updates ([3f1d46e](https://github.com/langchain-ai/langsmith-java/commit/3f1d46e06a3616cb5fa22f3f89d13a0ff871c6c3))
* **api:** manual updates ([2ac51c9](https://github.com/langchain-ai/langsmith-java/commit/2ac51c9075348a4fd8e38dace4911d8a6bee2000))
* **api:** manual updates ([187c87f](https://github.com/langchain-ai/langsmith-java/commit/187c87f96a08b4c197b37232081c38797b8d59cc))
* **api:** manual updates ([5e0db72](https://github.com/langchain-ai/langsmith-java/commit/5e0db72d55d10c512dc0b0d4997968d2382dc32d))
* **api:** manual updates ([6bc9970](https://github.com/langchain-ai/langsmith-java/commit/6bc9970850b2ce9206eefa770c9e4e8164487745))
* **client:** add connection pooling option ([9acbf93](https://github.com/langchain-ai/langsmith-java/commit/9acbf9334972d4ff5d00caa346e3907ded7d3a72))
* **client:** add more convenience service method overloads ([fba86c8](https://github.com/langchain-ai/langsmith-java/commit/fba86c83fdbe3ad353ec9a5db065464ac0928e55))
### Chores
* **api:** minor updates ([5561ef0](https://github.com/langchain-ai/langsmith-java/commit/5561ef0a24492e78eab34609a48da09aa37496a1))
* **ci:** upgrade `actions/github-script` ([f758afb](https://github.com/langchain-ai/langsmith-java/commit/f758afb6d78e7ac01b085403f89ec33dbb0caa6c))
* **internal:** allow passing args to `./scripts/test` ([206af30](https://github.com/langchain-ai/langsmith-java/commit/206af3009b966f77ad4aec77a975b38ebafbbbde))
* **internal:** make `OkHttp` constructor internal ([b55c1e8](https://github.com/langchain-ai/langsmith-java/commit/b55c1e8e15bbb1c370ab4d8b4713d37965b2f4db))
* **internal:** remove mock server code ([f72c72d](https://github.com/langchain-ai/langsmith-java/commit/f72c72d36f8c390bfbcbd14cdf25cf25d8131a7f))
* **internal:** update `TestServerExtension` comment ([c4956f5](https://github.com/langchain-ai/langsmith-java/commit/c4956f59bae794a6da689eb891ed329ad6bac082))
* **internal:** upgrade AssertJ ([2535a3a](https://github.com/langchain-ai/langsmith-java/commit/2535a3ab58df98b740bee627f7fecd0f88adb05c))
* refactor ([#90](https://github.com/langchain-ai/langsmith-java/issues/90)) ([1902c22](https://github.com/langchain-ai/langsmith-java/commit/1902c228ddd2b4ddcc5b741bf7c67f250ae9da06))
* update mock server docs ([1704b40](https://github.com/langchain-ai/langsmith-java/commit/1704b408f7fe5af7ee8fa322f037a37749e22d05))
## 0.1.0-alpha.22 (2026-01-23)
Full Changelog: [v0.1.0-alpha.21...v0.1.0-alpha.22](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.21...v0.1.0-alpha.22)
### Features
* **api:** api update ([5fe36e6](https://github.com/langchain-ai/langsmith-java/commit/5fe36e66be0d8a4118d631794c5f70f443c88dac))
* **client:** send `X-Stainless-Kotlin-Version` header ([748e689](https://github.com/langchain-ai/langsmith-java/commit/748e6890d3560b3f373aa07031bc8b2e6fa288f8))
### Bug Fixes
* **client:** disallow coercion from float to int ([bc01e05](https://github.com/langchain-ai/langsmith-java/commit/bc01e05032fb90f0318bde2107978ccf93eaa330))
* **client:** fully respect max retries ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
* **client:** preserve time zone in lenient date-time parsing ([498395e](https://github.com/langchain-ai/langsmith-java/commit/498395ea719057dd6d5bbef7f38fc1206ffc6d04))
* **client:** send retry count header for max retries 0 ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
* date time deserialization leniency ([ac7a559](https://github.com/langchain-ai/langsmith-java/commit/ac7a559ad9704bac385a49c724b0d86704d0da4f))
* deserialization order ([71f380d](https://github.com/langchain-ai/langsmith-java/commit/71f380d5b09e54f6a0f0d2b703c2e3e2cc4e4766))
### Chores
* **ci:** upgrade `actions/setup-java` ([ee1ed58](https://github.com/langchain-ai/langsmith-java/commit/ee1ed58ada45c071662c61e700b473ee471660a1))
* **internal:** clean up maven repo artifact script and add html documentation to repo root ([85d8771](https://github.com/langchain-ai/langsmith-java/commit/85d877129ea7e78b889b4d28f6ea06bcefacfce1))
* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([1cd0cf2](https://github.com/langchain-ai/langsmith-java/commit/1cd0cf2d27d3c87c6196d1492a6c6a8b3562ba49))
* **internal:** depend on packages directly in example ([07ba637](https://github.com/langchain-ai/langsmith-java/commit/07ba63741949f3e159314c5556902e7a3fb93def))
* **internal:** improve maven repo docs ([d928145](https://github.com/langchain-ai/langsmith-java/commit/d92814500d00759eb188ab19bfc036b90022c782))
* **internal:** update `actions/checkout` version ([70785ca](https://github.com/langchain-ai/langsmith-java/commit/70785ca2b98445faebb66ef7a54223d5b3ea9e31))
* **internal:** update maven repo doc to include authentication ([2900175](https://github.com/langchain-ai/langsmith-java/commit/29001754d9b523a9bdb76a7c9650991284fbb761))
* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/langchain-ai/langsmith-java/issues/3240) in tests ([ac7a559](https://github.com/langchain-ai/langsmith-java/commit/ac7a559ad9704bac385a49c724b0d86704d0da4f))
### Documentation
* add comment for arbitrary value fields ([642227b](https://github.com/langchain-ai/langsmith-java/commit/642227ba6b07a16d3f3287f585077afbc2684573))
## 0.1.0-alpha.21 (2026-01-14)
Full Changelog: [v0.1.0-alpha.20...v0.1.0-alpha.21](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.20...v0.1.0-alpha.21)
### Features
* **api:** api update ([d5e92e1](https://github.com/langchain-ai/langsmith-java/commit/d5e92e102d1dd770ae1205dc52d5d7f0a0692dcb))
* **api:** api update ([591546c](https://github.com/langchain-ai/langsmith-java/commit/591546c935e7e10e9521310870af83c8bd67ebd4))
* **api:** api update ([440ea8a](https://github.com/langchain-ai/langsmith-java/commit/440ea8abf0a22148c636f06f5a4c778e20b947e9))
* **api:** manual updates ([ee36c44](https://github.com/langchain-ai/langsmith-java/commit/ee36c44834da0d371168b876b7f9619c281f749e))
### Chores
* **internal:** support uploading Maven repo artifacts to stainless package server ([d1b01b9](https://github.com/langchain-ai/langsmith-java/commit/d1b01b9073fc2db7643fa07de2d421088218f4a3))
## 0.1.0-alpha.20 (2026-01-12)
Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.19...v0.1.0-alpha.20)
### Features
* **api:** api update ([4b197f0](https://github.com/langchain-ai/langsmith-java/commit/4b197f03e8a2d7658d2e268d854114398429f7a9))
* **api:** api update ([5703f0f](https://github.com/langchain-ai/langsmith-java/commit/5703f0f838d16543a4c518dc108065d4e72e58b6))
* **api:** api update ([e2e98b7](https://github.com/langchain-ai/langsmith-java/commit/e2e98b7e9ab23ea0298e79cfc38a49797db0e978))
* **api:** api update ([96a6948](https://github.com/langchain-ai/langsmith-java/commit/96a69483bf3e39b21a65dfb1af1eea6914939dfa))
* **api:** api update ([8895f35](https://github.com/langchain-ai/langsmith-java/commit/8895f3571aafdffce373dac1052a4f26fd38347d))
* **api:** api update ([9c66288](https://github.com/langchain-ai/langsmith-java/commit/9c66288568aceb6d7b77b8742dc34e132c122b66))
* **api:** api update ([f73c866](https://github.com/langchain-ai/langsmith-java/commit/f73c86620f221064be73d86d9ce540f4e8aeb5c6))
* **client:** add `HttpRequest#url()` method ([7c51e6a](https://github.com/langchain-ai/langsmith-java/commit/7c51e6a7139f20c181bc4349c247da8aaa151a89))
* **client:** allow configuring dispatcher executor service ([4a50a22](https://github.com/langchain-ai/langsmith-java/commit/4a50a22f62527d90e5584b9c805b34cfa8c0a7c8))
## 0.1.0-alpha.19 (2025-12-10)
Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.18...v0.1.0-alpha.19)
### Features
* **api:** api update ([c0d8bec](https://github.com/langchain-ai/langsmith-java/commit/c0d8bec69c14c9300a39737092b4a29210d4df59))
* **api:** api update ([9a9d362](https://github.com/langchain-ai/langsmith-java/commit/9a9d362b6e509abc8257c4ff8d5f11911f3ca4c0))
* **api:** api update ([e27bc6f](https://github.com/langchain-ai/langsmith-java/commit/e27bc6fd053032d0597a5dc4f4d636bada559748))
* **api:** api update ([ae353d7](https://github.com/langchain-ai/langsmith-java/commit/ae353d775aa03a2f820c9d065f79528401963c2c))
* **api:** api update ([fdb6021](https://github.com/langchain-ai/langsmith-java/commit/fdb6021eb3de21d7e3930dfe9f99a943100118b2))
* **api:** api update ([da999c0](https://github.com/langchain-ai/langsmith-java/commit/da999c02267725a5d5c4fdbf17f9482ad5df638e))
* **api:** api update ([daa7b07](https://github.com/langchain-ai/langsmith-java/commit/daa7b07fa755ad0d1feb6f6b3370c3bb5ff3c049))
* **api:** api update ([4985a65](https://github.com/langchain-ai/langsmith-java/commit/4985a651acb592b38f4d94afda202a4ecddb6be2))
* **api:** manual updates ([9ff3976](https://github.com/langchain-ai/langsmith-java/commit/9ff39760388f6cf4e68945ce536a0dd92d4f84bf))
* **api:** manual updates ([aa705c4](https://github.com/langchain-ai/langsmith-java/commit/aa705c489b4a64d2815c5ceacde5161c7518afac))
### Bug Fixes
* **client:** cancel okhttp call when future cancelled ([89ab779](https://github.com/langchain-ai/langsmith-java/commit/89ab7791a8e27347610bbecea44867c5b991073b))
* fix trace input and output ([#83](https://github.com/langchain-ai/langsmith-java/issues/83)) ([7a64410](https://github.com/langchain-ai/langsmith-java/commit/7a644105f69db9d10064ba38c18ee294fc62f360))
### Chores
* **api:** delete index methods ([7e90f5f](https://github.com/langchain-ai/langsmith-java/commit/7e90f5f70e263458f594ce92ae9a7b1a29e81f85))
### Documentation
* remove `$` for better copy-pasteabality ([4163726](https://github.com/langchain-ai/langsmith-java/commit/4163726d6d496289f025006b9a03363caeb879c7))
## 0.1.0-alpha.18 (2025-12-09)
Full Changelog: [v0.1.0-alpha.17...v0.1.0-alpha.18](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.17...v0.1.0-alpha.18)
### Features
* **api:** add go target ([ff3556a](https://github.com/langchain-ai/langsmith-java/commit/ff3556a80ff2fc128c17d06cc78b366e27865239))
* **api:** api update ([9a1ad24](https://github.com/langchain-ai/langsmith-java/commit/9a1ad247305770017095b760fa59c49883f6d204))
* **api:** change paginated retrieve methods to list ([229ed3e](https://github.com/langchain-ai/langsmith-java/commit/229ed3e0214e1d73cd81e05e118f5f6c7be670a7))
* **api:** pagination ([bd22dc6](https://github.com/langchain-ai/langsmith-java/commit/bd22dc6238f748edb0f0f70214f1a60b9087b525))
* **api:** pagination read me ([eccc183](https://github.com/langchain-ai/langsmith-java/commit/eccc183d0ec53b8b06122598daadafd707e28be3))
* **api:** readme example - change REPLACE_ME session_id to uuid ([a12609a](https://github.com/langchain-ai/langsmith-java/commit/a12609a69ae1d7d45a75f3032c74dc8a16c49242))
* method counter in ci ([8bb864e](https://github.com/langchain-ai/langsmith-java/commit/8bb864eaeccd46cb8153836e7b25ccfb1ba79838))
### Bug Fixes
* formatting error in record experiment example ([#81](https://github.com/langchain-ai/langsmith-java/issues/81)) ([243e318](https://github.com/langchain-ai/langsmith-java/commit/243e318f8defffda4b4116a8e2728c75f341dea7))
### Chores
* **internal:** version bump ([dcfaf78](https://github.com/langchain-ai/langsmith-java/commit/dcfaf785e20ada8e5c967a324aa23c9aa008372c))
## 0.1.0-alpha.17 (2025-11-20)
Full Changelog: [v0.1.0-alpha.16...v0.1.0-alpha.17](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.16...v0.1.0-alpha.17)
### Chores
* Configure workflow for CodeQL ([f0b024d](https://github.com/langchain-ai/langsmith-java/commit/f0b024dde59c5aab9c1da151061342d2586fe882))
## 0.1.0-alpha.16 (2025-11-20)
Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.15...v0.1.0-alpha.16)
### Chores
* Update runner to ubuntu-latest-l-oss for Sonatype Publish ([5597676](https://github.com/langchain-ai/langsmith-java/commit/5597676151b5bd30087dcca93ae13f0e69714a16))
## 0.1.0-alpha.15 (2025-11-20)
Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.14...v0.1.0-alpha.15)
## 0.1.0-alpha.14 (2025-11-20)
Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/langchain-ai/langsmith-java/compare/v0.1.0-alpha.13...v0.1.0-alpha.14)
+122
View File
@@ -0,0 +1,122 @@
# Contributing
Thanks for your interest in contributing to the LangSmith Java SDK!
## Overview
This SDK is **auto-generated by [Stainless](https://www.stainless.com/)** from our OpenAPI spec. Custom code (prompts, OTel wrappers, etc.) lives alongside generated code — Stainless is configured to [preserve custom code](https://www.stainless.com/docs/sdks/configure/custom-code/) and never overwrite it.
> **Important:** Most files in the SDK are generated. If you need to change generated code, talk to @edongashi — there may be weird errors in the Stainless config, but they're generally fixable.
## Branching and workflow
| Branch | Purpose |
| ----------------------------------------------- | ----------------------------------------------------- |
| `next` | **Development branch.** All PRs should target `next`. |
| `main` | Release branch. Only updated via release-please. |
| `release-please--branches--main--changes--next` | Auto-generated release PR. Merges `next``main`. |
**Always work off `next`.** Do not merge directly to `main` — Dependabot or other bots occasionally violate this, which can cause headaches.
### Release process
1. Merge your PR into `next`.
2. A release-please PR is automatically created/updated (`release-please--branches--main--changes--next`).
3. Verify your change appears in the release PR diff and description.
4. Approve the release PR — it auto-merges on approval.
5. A GitHub Action publishes to Maven Central via `./gradlew publishAndReleaseToMavenCentral`.
6. Maven Central can be slow to index — verify on [repo1.maven.org](https://repo1.maven.org/maven2/com/langchain/smith/langsmith-java/) rather than [mvnrepository.com](https://mvnrepository.com/artifact/com.langchain.smith/langsmith-java), which lags behind.
> **Note:** If lint fixes or other changes are pushed to the release-please branch after a version bump, the publish action can fail. Always ensure CI is green before approving.
## Development setup
### Prerequisites
- Java 8+ (tested against 8 and 21)
- Gradle (uses the included `gradlew` wrapper)
### Building
```bash
./scripts/build
```
### Formatting
The project uses [ktfmt](https://github.com/facebook/ktfmt) (`--kotlinlang-style`) for Kotlin and [Palantir Java Format](https://github.com/palantir/palantir-java-format) for Java.
```bash
# Format all files
./scripts/format
# Format only Kotlin
./gradlew formatKotlin
# Format a single module
./gradlew :langsmith-java-core:formatKotlin
```
### Linting
```bash
# Lint all files
./scripts/lint
# Lint only Kotlin
./gradlew lintKotlin
```
CI blocks on lint failures. Always run `./scripts/format` before pushing.
### Testing
```bash
# Run all tests
./scripts/test
# Run specific tests
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.*"
# Force re-run (skip Gradle cache)
./gradlew :langsmith-java-core:test --tests "..." --rerun
# See stdout output
./gradlew :langsmith-java-core:test --tests "..." --rerun --info
```
#### Integration tests
Integration tests require API keys and skip gracefully via `assumeTrue` if they're missing:
```bash
export LANGSMITH_API_KEY="lsv2_pt_..."
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
## CI
CI runs three jobs on every push and PR:
1. **lint**`./scripts/lint`
2. **build**`./scripts/build`
3. **test**`./scripts/test`
All three must pass before merging.
## Code style
See [AGENTS.md](AGENTS.md) for detailed Kotlin conventions, including:
- Prefer `buildMap`/`buildList` over mutable collections
- Use `partition` instead of double `filter`
- Use parameterized tests for table-driven cases
- Avoid `@Suppress("UNCHECKED_CAST")` — restructure with safe casts
- `toString()` should follow the `ClassName{field=value}` convention
## Dependencies
- **Anthropic SDK** is a `compileOnly` dependency — users must add it themselves. Methods using Anthropic types should catch `NoClassDefFoundError` and throw `IllegalStateException` with a clear message.
- **SLF4J** (`slf4j-api`) is an `api` dependency for logging. Tests use `slf4j-simple`.
- **Jackson** 2.22.0 is the default, but the SDK is compatible with 2.13.4+.
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2025 langChain
Copyright 2026 langChain
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+222 -95
View File
@@ -2,8 +2,8 @@
<!-- x-release-please-start-version -->
[![Maven Central](https://img.shields.io/maven-central/v/com.langchain.smith/langsmith-java)](https://central.sonatype.com/artifact/com.langchain.smith/langsmith-java/0.1.0-alpha.14)
[![javadoc](https://javadoc.io/badge2/com.langchain.smith/langsmith-java/0.1.0-alpha.14/javadoc.svg)](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-alpha.14)
[![Maven Central](https://img.shields.io/maven-central/v/com.langchain.smith/langsmith-java)](https://central.sonatype.com/artifact/com.langchain.smith/langsmith-java/0.1.0-beta.19)
[![javadoc](https://javadoc.io/badge2/com.langchain.smith/langsmith-java/0.1.0-beta.19/javadoc.svg)](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-beta.18)
<!-- x-release-please-end -->
@@ -13,7 +13,7 @@ To learn more about LangSmith, check out the [docs](https://docs.smith.langchain
<!-- x-release-please-start-version -->
The REST API documentation can be found on [docs.smith.langchain.com](https://docs.smith.langchain.com/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-alpha.14).
The REST API documentation can be found on [docs.smith.langchain.com](https://docs.smith.langchain.com/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.langchain.smith/langsmith-java/0.1.0-beta.19).
<!-- x-release-please-end -->
@@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.smith.langchain.com](https://do
### Gradle
```kotlin
implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.14")
implementation("com.langchain.smith:langsmith-java:0.1.0-beta.19")
```
### Maven
@@ -33,7 +33,7 @@ implementation("com.langchain.smith:langsmith-java:0.1.0-alpha.14")
<dependency>
<groupId>com.langchain.smith</groupId>
<artifactId>langsmith-java</artifactId>
<version>0.1.0-alpha.14</version>
<version>0.1.0-beta.19</version>
</dependency>
```
@@ -48,25 +48,17 @@ This library requires Java 8 or later.
```java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.models.runs.RunQueryParams;
import com.langchain.smith.models.runs.RunQueryV2Page;
import com.langchain.smith.models.runs.RunQueryV2Params;
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
RunQueryParams params = RunQueryParams.builder()
.addSession("REPLACE_ME")
.limit(10L)
RunQueryV2Params params = RunQueryV2Params.builder()
.addProjectId("00000000-0000-0000-0000-000000000000")
.build();
var response = client.runs().query(params);
// Print runs
System.out.println("Found " + response.runs().size() + " runs:");
for (var run : response.runs()) {
System.out.println("Run ID: " + run.id());
System.out.println("Run Name: " + run.name());
System.out.println("---");
}
RunQueryV2Page page = client.runs().queryV2(params);
```
## Examples
@@ -103,8 +95,8 @@ Configure the client using system properties or environment variables:
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
```
@@ -117,7 +109,6 @@ import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
LangsmithClient client = LangsmithOkHttpClient.builder()
.apiKey("My API Key")
.tenantId("My Tenant ID")
.organizationId("My Organization ID")
.build();
```
@@ -128,8 +119,8 @@ import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
LangsmithClient client = LangsmithOkHttpClient.builder()
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
.fromEnv()
.apiKey("My API Key")
.build();
@@ -137,13 +128,11 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
See this table for the available options:
| Setter | System property | Environment variable | Required | Default value |
| ---------------- | ----------------------------------- | --------------------------- | -------- | ------------------------------------ |
| `apiKey` | `langchain.langsmithApiKey` | `LANGSMITH_API_KEY` | false | - |
| `tenantId` | `langchain.langsmithTenantId` | `LANGSMITH_TENANT_ID` | false | - |
| `bearerToken` | `langchain.langsmithBearerToken` | `LANGSMITH_BEARER_TOKEN` | false | - |
| `organizationId` | `langchain.langsmithOrganizationId` | `LANGSMITH_ORGANIZATION_ID` | false | - |
| `baseUrl` | `langchain.baseUrl` | `LANGSMITH_ENDPOINT` | true | `"https://api.smith.langchain.com/"` |
| Setter | System property | Environment variable | Required | Default value |
| ---------- | ----------------------------- | --------------------- | -------- | ------------------------------------ |
| `apiKey` | `langchain.langsmithApiKey` | `LANGSMITH_API_KEY` | false | - |
| `tenantId` | `langchain.langsmithTenantId` | `LANGSMITH_TENANT_ID` | false | - |
| `baseUrl` | `langchain.baseUrl` | `LANGSMITH_ENDPOINT` | true | `"https://api.smith.langchain.com/"` |
System properties take precedence over environment variables.
@@ -170,7 +159,7 @@ The `withOptions()` method does not affect the original client or service.
To send a request to the LangChain API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
For example, `client.sessions().dashboard(...)` should be called with an instance of `SessionDashboardParams`, and it will return an instance of `CustomChartsSection`.
For example, `client.runs().queryV2(...)` should be called with an instance of `RunQueryV2Params`, and it will return an instance of `RunQueryV2Page`.
## Immutability
@@ -187,20 +176,18 @@ The default client is synchronous. To switch to asynchronous execution, call the
```java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
import com.langchain.smith.models.runs.RunQueryV2Params;
import java.util.concurrent.CompletableFuture;
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
LangsmithClient client = LangsmithOkHttpClient.fromEnv();
SessionDashboardParams params = SessionDashboardParams.builder()
.sessionId("REPLACE_ME")
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
RunQueryV2Params params = RunQueryV2Params.builder()
.addProjectId("00000000-0000-0000-0000-000000000000")
.build();
CompletableFuture<CustomChartsSection> customChartsSection = client.async().sessions().dashboard(params);
CompletableFuture<RunQueryV2PageAsync> page = client.async().runs().queryV2(params);
```
Or create an asynchronous client from the beginning:
@@ -208,20 +195,18 @@ Or create an asynchronous client from the beginning:
```java
import com.langchain.smith.client.LangsmithClientAsync;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClientAsync;
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
import com.langchain.smith.models.runs.RunQueryV2Params;
import java.util.concurrent.CompletableFuture;
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId`, `langchain.langsmithBearerToken`, `langchain.langsmithOrganizationId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID`, `LANGSMITH_BEARER_TOKEN`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_ENDPOINT` environment variables
// Configures using the `langchain.langsmithApiKey`, `langchain.langsmithTenantId` and `langchain.baseUrl` system properties
// Or configures using the `LANGSMITH_API_KEY`, `LANGSMITH_TENANT_ID` and `LANGSMITH_ENDPOINT` environment variables
LangsmithClientAsync client = LangsmithOkHttpClientAsync.fromEnv();
SessionDashboardParams params = SessionDashboardParams.builder()
.sessionId("REPLACE_ME")
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
RunQueryV2Params params = RunQueryV2Params.builder()
.addProjectId("00000000-0000-0000-0000-000000000000")
.build();
CompletableFuture<CustomChartsSection> customChartsSection = client.sessions().dashboard(params);
CompletableFuture<RunQueryV2PageAsync> page = client.runs().queryV2(params);
```
The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
@@ -303,26 +288,24 @@ To access this data, prefix any HTTP method call on a client or service with `wi
```java
import com.langchain.smith.core.http.Headers;
import com.langchain.smith.core.http.HttpResponseFor;
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.models.runs.RunQueryV2Page;
import com.langchain.smith.models.runs.RunQueryV2Params;
SessionDashboardParams params = SessionDashboardParams.builder()
.sessionId("REPLACE_ME")
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
RunQueryV2Params params = RunQueryV2Params.builder()
.addProjectId("00000000-0000-0000-0000-000000000000")
.build();
HttpResponseFor<CustomChartsSection> customChartsSection = client.sessions().withRawResponse().dashboard(params);
HttpResponseFor<RunQueryV2Page> page = client.runs().withRawResponse().queryV2(params);
int statusCode = customChartsSection.statusCode();
Headers headers = customChartsSection.headers();
int statusCode = page.statusCode();
Headers headers = page.headers();
```
You can still deserialize the response into an instance of a Java class if needed:
```java
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.runs.RunQueryV2Page;
CustomChartsSection parsedCustomChartsSection = customChartsSection.parse();
RunQueryV2Page parsedPage = page.parse();
```
## Error handling
@@ -350,20 +333,131 @@ The SDK throws custom unchecked exception types:
- [`LangChainException`](langsmith-java-core/src/main/kotlin/com/langchain/smith/errors/LangChainException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
## Logging
## Pagination
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
The SDK defines methods that return a paginated lists of results. It provides convenient ways to access the results either one page at a time or item-by-item across all pages.
### Auto-pagination
To iterate through all results across all pages, use the `autoPager()` method, which automatically fetches more pages as needed.
When using the synchronous client, the method returns an [`Iterable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html)
```java
import com.langchain.smith.models.runs.Run;
import com.langchain.smith.models.runs.RunQueryV2Page;
RunQueryV2Page page = client.runs().queryV2();
// Process as an Iterable
for (Run run : page.autoPager()) {
System.out.println(run);
}
// Process as a Stream
page.autoPager()
.stream()
.limit(50)
.forEach(run -> System.out.println(run));
```
When using the asynchronous client, the method returns an [`AsyncStreamResponse`](langsmith-java-core/src/main/kotlin/com/langchain/smith/core/http/AsyncStreamResponse.kt):
```java
import com.langchain.smith.core.http.AsyncStreamResponse;
import com.langchain.smith.models.runs.Run;
import com.langchain.smith.models.runs.RunQueryV2PageAsync;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
CompletableFuture<RunQueryV2PageAsync> pageFuture = client.async().runs().queryV2();
pageFuture.thenRun(page -> page.autoPager().subscribe(run -> {
System.out.println(run);
}));
// If you need to handle errors or completion of the stream
pageFuture.thenRun(page -> page.autoPager().subscribe(new AsyncStreamResponse.Handler<>() {
@Override
public void onNext(Run run) {
System.out.println(run);
}
@Override
public void onComplete(Optional<Throwable> error) {
if (error.isPresent()) {
System.out.println("Something went wrong!");
throw new RuntimeException(error.get());
} else {
System.out.println("No more!");
}
}
}));
// Or use futures
pageFuture.thenRun(page -> page.autoPager()
.subscribe(run -> {
System.out.println(run);
})
.onCompleteFuture()
.whenComplete((unused, error) -> {
if (error != null) {
System.out.println("Something went wrong!");
throw new RuntimeException(error);
} else {
System.out.println("No more!");
}
}));
```
### Manual pagination
To access individual page items and manually request the next page, use the `items()`,
`hasNextPage()`, and `nextPage()` methods:
```java
import com.langchain.smith.models.runs.Run;
import com.langchain.smith.models.runs.RunQueryV2Page;
RunQueryV2Page page = client.runs().queryV2();
while (true) {
for (Run run : page.items()) {
System.out.println(run);
}
if (!page.hasNextPage()) {
break;
}
page = page.nextPage();
}
```
## Logging
Enable logging by setting the `LANGCHAIN_LOG` environment variable to `info`:
```sh
$ export LANGCHAIN_LOG=info
export LANGCHAIN_LOG=info
```
Or to `debug` for more verbose logging:
```sh
$ export LANGCHAIN_LOG=debug
export LANGCHAIN_LOG=debug
```
Or configure the client manually using the `logLevel` method:
```java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.core.LogLevel;
LangsmithClient client = LangsmithOkHttpClient.builder()
.fromEnv()
.logLevel(LogLevel.INFO)
.build();
```
## ProGuard and R8
@@ -374,7 +468,7 @@ ProGuard and R8 should automatically detect and use the published rules, but you
## Jackson
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.22.0 by default.
The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
@@ -383,6 +477,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
> [!CAUTION]
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
## Network options
### Retries
@@ -413,16 +509,14 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
### Timeouts
Requests time out after 1.5 minutes by default.
Requests time out after 90 seconds by default.
To set a custom timeout, configure the method call using the `timeout` method:
```java
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.runs.RunQueryV2Page;
CustomChartsSection customChartsSection = client.sessions().dashboard(
params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
);
RunQueryV2Page page = client.runs().queryV2(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build());
```
Or configure the default for all method calls at the client level:
@@ -458,6 +552,40 @@ LangsmithClient client = LangsmithOkHttpClient.builder()
.build();
```
If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
```java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.core.http.ProxyAuthenticator;
LangsmithClient client = LangsmithOkHttpClient.builder()
.fromEnv()
.proxy(...)
// Or a custom implementation of `ProxyAuthenticator`.
.proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
.build();
```
### Connection pooling
To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:
```java
import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import java.time.Duration;
LangsmithClient client = LangsmithOkHttpClient.builder()
.fromEnv()
// If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa.
.maxIdleConnections(10)
.keepAliveDuration(Duration.ofMinutes(2))
.build();
```
If both options are unset, OkHttp's default connection pool settings are used.
### HTTPS
> [!NOTE]
@@ -525,9 +653,9 @@ To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQu
```java
import com.langchain.smith.core.JsonValue;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.models.runs.RunQueryV2Params;
SessionDashboardParams params = SessionDashboardParams.builder()
RunQueryV2Params params = RunQueryV2Params.builder()
.putAdditionalHeader("Secret-Header", "42")
.putAdditionalQueryParam("secret_query_param", "42")
.putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
@@ -539,11 +667,11 @@ These can be accessed on the built object later using the `_additionalHeaders()`
To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](langsmith-java-core/src/main/kotlin/com/langchain/smith/core/Values.kt) object to its setter:
```java
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.core.JsonValue;
import com.langchain.smith.models.runs.RunQueryV2Params;
SessionDashboardParams params = SessionDashboardParams.builder()
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
RunQueryV2Params params = RunQueryV2Params.builder()
.projectIds(JsonValue.from(42))
.build();
```
@@ -592,11 +720,10 @@ To forcibly omit a required parameter or property, pass [`JsonMissing`](langsmit
```java
import com.langchain.smith.core.JsonMissing;
import com.langchain.smith.models.sessions.CustomChartsSectionRequest;
import com.langchain.smith.models.sessions.SessionDashboardParams;
import com.langchain.smith.models.runs.RunQueryV2Params;
import com.langchain.smith.models.sessions.SessionRetrieveParams;
SessionDashboardParams params = SessionDashboardParams.builder()
.customChartsSectionRequest(CustomChartsSectionRequest.builder().build())
RunQueryV2Params params = SessionRetrieveParams.builder()
.sessionId(JsonMissing.of())
.build();
```
@@ -609,7 +736,7 @@ To access undocumented response properties, call the `_additionalProperties()` m
import com.langchain.smith.core.JsonValue;
import java.util.Map;
Map<String, JsonValue> additionalProperties = client.sessions().dashboard(params)._additionalProperties();
Map<String, JsonValue> additionalProperties = client.sessions().create(params)._additionalProperties();
JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
@@ -639,19 +766,19 @@ To access a property's raw JSON value, which may be undocumented, call its `_` p
import com.langchain.smith.core.JsonField;
import java.util.Optional;
JsonField<Object> field = client.sessions().dashboard(params)._field();
JsonField<String> id = client.sessions().create(params)._id();
if (field.isMissing()) {
if (id.isMissing()) {
// The property is absent from the JSON response
} else if (field.isNull()) {
} else if (id.isNull()) {
// The property was set to literal null
} else {
// Check if value was provided as a string
// Other methods include `asNumber()`, `asBoolean()`, etc.
Optional<String> jsonString = field.asString();
Optional<String> jsonString = id.asString();
// Try to deserialize into a custom type
MyClass myObject = field.asUnknown().orElseThrow().convert(MyClass.class);
MyClass myObject = id.asUnknown().orElseThrow().convert(MyClass.class);
}
```
@@ -661,22 +788,22 @@ In rare cases, the API may return a response that doesn't match the expected typ
By default, the SDK will not throw an exception in this case. It will throw [`LangChainInvalidDataException`](langsmith-java-core/src/main/kotlin/com/langchain/smith/errors/LangChainInvalidDataException.kt) only if you directly access the property.
If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
Validating the response is _not_ forwards compatible with new types from the API for existing fields.
If you would still prefer to check that the response is completely well-typed upfront, then either call `validate()`:
```java
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.sessions.TracerSessionWithoutVirtualFields;
CustomChartsSection customChartsSection = client.sessions().dashboard(params).validate();
TracerSessionWithoutVirtualFields tracerSessionWithoutVirtualFields = client.sessions().create(params).validate();
```
Or configure the method call to validate the response using the `responseValidation` method:
```java
import com.langchain.smith.models.sessions.CustomChartsSection;
import com.langchain.smith.models.runs.RunQueryV2Page;
CustomChartsSection customChartsSection = client.sessions().dashboard(
params, RequestOptions.builder().responseValidation(true).build()
);
RunQueryV2Page page = client.runs().queryV2(RequestOptions.builder().responseValidation(true).build());
```
Or configure the default for all method calls at the client level:
+30
View File
@@ -0,0 +1,30 @@
# Security Fix Plan - langsmith-java
Validated 2026-07-02 against the open Dependabot alerts for `langsmith-java-staging`.
## Status
| Item | Scope | Status |
|------|-------|--------|
| Jackson 2.13.4/2.14.0/2.18.6 -> 2.22.0 | Published SDK + examples + tests | Done |
| Spring Boot 2.7.18 -> 3.5.16 | Example module only, non-published | Done |
| Spring Framework 5.x pins removed | Example module only, non-published | Done |
| Tomcat 9.x pins removed | Example module only, non-published | Done |
| SnakeYAML 1.x pin removed | Example module only, non-published | Done |
| WireMock JRE8 2.35.2 -> org.wiremock:wiremock 4.0.0-beta.37 | Test scope only | Done |
| Jetty test constraints -> 12.1.10 | Test scope only | Done |
| Bouncy Castle test constraints -> 1.84 | Test scope only | Done |
| Log4j Core test constraint -> 2.25.4 | Test scope only | Done |
| OpenNLP test constraint -> 2.5.9 | Test scope only | Done |
| Handlebars test constraint -> 4.5.2 | Test scope only | Done |
| Plexus Utils test constraint -> 4.0.3 | Test scope only | Done |
| Commons/Guava/json-path/XMLUnit/json-smart test constraints bumped | Test scope only | Done |
## Notes
The Spring Boot upgrade is intentionally limited to the example module, which is not published as part
of the SDK artifacts. It removes the old Spring Boot 2/Spring Framework 5 workaround pins that kept
several alerts open.
WireMock 4 is currently beta-only, but it is test scoped and moves the test server dependency graph to
Jetty 12. Keeping WireMock 3 would still leave Jetty 11 in at least one advisory range.
+4
View File
@@ -2,6 +2,10 @@
errors=()
if [ -z "${RELEASE_PLEASE_TOKEN}" ]; then
errors+=("The RELEASE_PLEASE_TOKEN secret has not been set. Create a fine-grained GitHub PAT and add it as a repository secret.")
fi
if [ -z "${SONATYPE_USERNAME}" ]; then
errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
+28 -3
View File
@@ -1,5 +1,16 @@
buildscript {
configurations.classpath {
resolutionStrategy {
// CVE-2026-54512, CVE-2026-54513, and CVE-2026-54515: Dokka 2.2.0 depends on a vulnerable
// jackson-databind version. Remove once Dokka resolves jackson-databind >= 2.18.9.
force("com.fasterxml.jackson.core:jackson-databind:2.18.9")
}
}
}
plugins {
id("org.jetbrains.dokka") version "2.0.0"
id("org.jetbrains.dokka") version "2.2.0"
id("org.jetbrains.dokka-javadoc") version "2.2.0"
}
repositories {
@@ -8,7 +19,21 @@ repositories {
allprojects {
group = "com.langchain.smith"
version = "0.1.0-alpha.14" // x-release-please-version
version = "0.1.0-beta.19" // x-release-please-version
configurations.matching { it.name.startsWith("dokka") }.configureEach {
// Keep Dokka worker classpaths on the same patched version as the buildscript classpath.
resolutionStrategy.force("com.fasterxml.jackson.core:jackson-databind:2.18.9")
}
configurations.matching { it.name == "kotlinBouncyCastleConfiguration" }.configureEach {
// CVE-2026-3505: remove once Kotlin's publishing validation selects Bouncy Castle >= 1.84.
resolutionStrategy.force(
"org.bouncycastle:bcpg-jdk18on:1.84",
"org.bouncycastle:bcpkix-jdk18on:1.84",
"org.bouncycastle:bcprov-jdk18on:1.84",
)
}
}
subprojects {
@@ -21,11 +46,11 @@ subprojects {
group = "Verification"
description = "Verifies all source files are formatted."
}
apply(plugin = "org.jetbrains.dokka")
}
subprojects {
apply(plugin = "org.jetbrains.dokka")
apply(plugin = "org.jetbrains.dokka-javadoc")
}
// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+4 -4
View File
@@ -1,7 +1,7 @@
plugins {
`kotlin-dsl`
kotlin("jvm") version "2.0.21"
id("com.vanniktech.maven.publish") version "0.28.0"
kotlin("jvm") version "2.3.21"
id("com.vanniktech.maven.publish") version "0.36.0"
}
repositories {
@@ -16,6 +16,6 @@ java {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.36.0")
}
@@ -44,9 +44,14 @@ tasks.withType<Test>().configureEach {
}
}
val palantir by configurations.creating
val palantir by configurations.creating {
resolutionStrategy {
// CVE-2026-54515: remove once Palantir resolves jackson-databind >= 2.21.5.
force("com.fasterxml.jackson.core:jackson-databind:2.21.5")
}
}
dependencies {
palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
palantir("com.palantir.javaformat:palantir-java-format:2.93.0")
}
fun registerPalantir(
@@ -24,19 +24,26 @@ kotlin {
"-nowarn",
)
jvmTarget.set(JvmTarget.JVM_1_8)
languageVersion.set(KotlinVersion.KOTLIN_2_0)
apiVersion.set(KotlinVersion.KOTLIN_2_0)
languageVersion.set(KotlinVersion.KOTLIN_2_1)
apiVersion.set(KotlinVersion.KOTLIN_2_1)
}
}
tasks.withType<Test>().configureEach {
systemProperty("junit.jupiter.execution.parallel.enabled", true)
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
// Suppress JVM warnings about dynamically loaded agents (e.g. byte-buddy used by Mockito).
// Without this, agent warnings are written to stderr and break tests that capture stderr output.
jvmArgs("-XX:+EnableDynamicAgentLoading")
// `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation.
inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true)
}
val ktfmt by configurations.creating
dependencies {
ktfmt("com.facebook:ktfmt:0.56")
ktfmt("com.facebook:ktfmt:0.61")
}
fun registerKtfmt(
@@ -1,37 +1,46 @@
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
import com.vanniktech.maven.publish.SourcesJar
plugins {
id("com.vanniktech.maven.publish")
}
publishing {
repositories {
if (project.hasProperty("publishLocal")) {
maven {
name = "LocalFileSystem"
url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo")
}
}
}
}
repositories {
gradlePluginPortal()
mavenCentral()
}
extra["signingInMemoryKey"] = System.getenv("GPG_SIGNING_KEY")
extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID")
extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD")
configure<MavenPublishBaseExtension> {
signAllPublications()
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
if (!project.hasProperty("publishLocal")) {
signAllPublications()
publishToMavenCentral()
}
coordinates(project.group.toString(), project.name, project.version.toString())
configure(
KotlinJvm(
javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
sourcesJar = true,
javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationJavadoc"),
sourcesJar = SourcesJar.Sources(),
)
)
pom {
name.set("LangSmith")
description.set("The LangSmith API is used to programmatically create and manage LangSmith\nresources.\n\n## Host\n\nhttps://dev.api.smith.langchain.com\n\n## Authentication\n\nTo authenticate with the LangSmith API, set the `X-Api-Key` header to a valid\n[LangSmith API key](https://docs.langchain.com/langsmith/create-account-api-key#create-an-api-key).")
url.set("https://docs.smith.langchain.com/")
description.set("The LangSmith API is used to programmatically create and manage LangSmith\nresources.\n\n## Host\n\nhttp://localhost:1984\n\n## Authentication\n\nTo authenticate with the LangSmith API, set the `X-Api-Key` header to a valid\n[LangSmith API key](https://docs.langchain.com/langsmith/create-account-api-key#create-an-api-key).")
url.set("https://www.github.com/langchain-ai/langsmith-java")
licenses {
license {
@@ -42,7 +51,6 @@ configure<MavenPublishBaseExtension> {
developers {
developer {
name.set("LangChain")
email.set("oss@langchain.dev")
}
}
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+4 -7
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -205,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+82 -94
View File
@@ -1,94 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
-4
View File
@@ -1,4 +0,0 @@
File generated from our OpenAPI spec by Stainless.
This directory can be used to store custom files to expand the SDK.
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
+25 -2
View File
@@ -3,12 +3,35 @@ plugins {
id("langchain.publish")
}
configurations.matching { it.name in setOf("testCompileClasspath", "testRuntimeClasspath") }.configureEach {
// Test-only WireMock 4 requires Java 17+ dependencies. Published artifacts still target Java 8.
attributes.attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
dependencies {
api(project(":langsmith-java-core"))
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
constraints {
testImplementation("org.eclipse.jetty:jetty-server") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-http") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-servlets") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-xml") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-common") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-hpack") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-server") { version { require("12.1.10") } }
testImplementation("com.github.jknack:handlebars") { version { require("4.5.2") } }
testImplementation("commons-fileupload:commons-fileupload") { version { require("1.6.0") } }
testImplementation("commons-io:commons-io") { version { require("2.22.0") } }
testImplementation("com.google.guava:guava") { version { require("33.6.0-jre") } }
testImplementation("com.jayway.jsonpath:json-path") { version { require("2.10.0") } }
testImplementation("org.xmlunit:xmlunit-core") { version { require("2.12.0") } }
testImplementation("net.minidev:json-smart") { version { require("2.6.0") } }
}
testImplementation(kotlin("test"))
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("org.wiremock:wiremock:4.0.0-beta.37")
testImplementation("org.wiremock:wiremock-junit5:4.0.0-beta.37")
}
@@ -6,16 +6,21 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.client.LangsmithClientImpl
import com.langchain.smith.core.ClientOptions
import com.langchain.smith.core.LogLevel
import com.langchain.smith.core.Sleeper
import com.langchain.smith.core.Timeout
import com.langchain.smith.core.http.AsyncStreamResponse
import com.langchain.smith.core.http.Headers
import com.langchain.smith.core.http.HttpClient
import com.langchain.smith.core.http.ProxyAuthenticator
import com.langchain.smith.core.http.QueryParams
import com.langchain.smith.core.jsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,16 +49,93 @@ class LangsmithOkHttpClient private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
private var proxyAuthenticator: ProxyAuthenticator? = null
private var maxIdleConnections: Int? = null
private var keepAliveDuration: Duration? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
/**
* The executor service to use for running HTTP requests.
*
* Defaults to OkHttp's
* [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
*
* This class takes ownership of the executor service and shuts it down when closed.
*/
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
this.dispatcherExecutorService = dispatcherExecutorService
}
/**
* Alias for calling [Builder.dispatcherExecutorService] with
* `dispatcherExecutorService.orElse(null)`.
*/
fun dispatcherExecutorService(dispatcherExecutorService: Optional<ExecutorService>) =
dispatcherExecutorService(dispatcherExecutorService.getOrNull())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
fun proxy(proxy: Optional<Proxy>) = proxy(proxy.getOrNull())
/**
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
* Required`.
*/
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
this.proxyAuthenticator = proxyAuthenticator
}
/**
* Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
*/
fun proxyAuthenticator(proxyAuthenticator: Optional<ProxyAuthenticator>) =
proxyAuthenticator(proxyAuthenticator.getOrNull())
/**
* The maximum number of idle connections kept by the underlying OkHttp connection pool.
*
* If this is set, then [keepAliveDuration] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
this.maxIdleConnections = maxIdleConnections
}
/**
* Alias for [Builder.maxIdleConnections].
*
* This unboxed primitive overload exists for backwards compatibility.
*/
fun maxIdleConnections(maxIdleConnections: Int) =
maxIdleConnections(maxIdleConnections as Int?)
/**
* Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
*/
fun maxIdleConnections(maxIdleConnections: Optional<Int>) =
maxIdleConnections(maxIdleConnections.getOrNull())
/**
* The keep-alive duration for idle connections in the underlying OkHttp connection pool.
*
* If this is set, then [maxIdleConnections] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
this.keepAliveDuration = keepAliveDuration
}
/** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
fun keepAliveDuration(keepAliveDuration: Optional<Duration>) =
keepAliveDuration(keepAliveDuration.getOrNull())
/**
* The socket factory used to secure HTTPS connections.
*
@@ -121,6 +203,17 @@ class LangsmithOkHttpClient private constructor() {
*/
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
/**
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
*
* Defaults to a dedicated cached thread pool.
*
* This class takes ownership of the executor and shuts it down, if possible, when closed.
*/
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
clientOptions.streamHandlerExecutor(streamHandlerExecutor)
}
/**
* The interface to use for delaying execution, like during retries.
*
@@ -154,6 +247,9 @@ class LangsmithOkHttpClient private constructor() {
/**
* Whether to call `validate` on every response before returning it.
*
* Setting this to `true` is _not_ forwards compatible with new types from the API for
* existing fields.
*
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -195,6 +291,25 @@ class LangsmithOkHttpClient private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
/**
* Whether run create/update calls should be automatically batched for tracing.
*
* Defaults to true. Set to false to send run create/update calls synchronously through the
* single-run endpoints.
*/
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
clientOptions.autoBatchTracing(autoBatchTracing)
}
/**
* The level at which to log request and response information.
*
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
*
* Defaults to [LogLevel.fromEnv].
*/
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
@@ -205,23 +320,6 @@ class LangsmithOkHttpClient private constructor() {
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
/**
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
* x-organization-id (for org scoped apis).
*/
fun bearerToken(bearerToken: String?) = apply { clientOptions.bearerToken(bearerToken) }
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
fun organizationId(organizationId: String?) = apply {
clientOptions.organizationId(organizationId)
}
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
fun organizationId(organizationId: Optional<String>) =
organizationId(organizationId.getOrNull())
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map<String, Iterable<String>>) = apply {
@@ -321,6 +419,10 @@ class LangsmithOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.maxIdleConnections(maxIdleConnections)
.keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
@@ -6,16 +6,21 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.langchain.smith.client.LangsmithClientAsync
import com.langchain.smith.client.LangsmithClientAsyncImpl
import com.langchain.smith.core.ClientOptions
import com.langchain.smith.core.LogLevel
import com.langchain.smith.core.Sleeper
import com.langchain.smith.core.Timeout
import com.langchain.smith.core.http.AsyncStreamResponse
import com.langchain.smith.core.http.Headers
import com.langchain.smith.core.http.HttpClient
import com.langchain.smith.core.http.ProxyAuthenticator
import com.langchain.smith.core.http.QueryParams
import com.langchain.smith.core.jsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,16 +49,93 @@ class LangsmithOkHttpClientAsync private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
private var proxyAuthenticator: ProxyAuthenticator? = null
private var maxIdleConnections: Int? = null
private var keepAliveDuration: Duration? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
/**
* The executor service to use for running HTTP requests.
*
* Defaults to OkHttp's
* [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
*
* This class takes ownership of the executor service and shuts it down when closed.
*/
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
this.dispatcherExecutorService = dispatcherExecutorService
}
/**
* Alias for calling [Builder.dispatcherExecutorService] with
* `dispatcherExecutorService.orElse(null)`.
*/
fun dispatcherExecutorService(dispatcherExecutorService: Optional<ExecutorService>) =
dispatcherExecutorService(dispatcherExecutorService.getOrNull())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
fun proxy(proxy: Optional<Proxy>) = proxy(proxy.getOrNull())
/**
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
* Required`.
*/
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
this.proxyAuthenticator = proxyAuthenticator
}
/**
* Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
*/
fun proxyAuthenticator(proxyAuthenticator: Optional<ProxyAuthenticator>) =
proxyAuthenticator(proxyAuthenticator.getOrNull())
/**
* The maximum number of idle connections kept by the underlying OkHttp connection pool.
*
* If this is set, then [keepAliveDuration] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
this.maxIdleConnections = maxIdleConnections
}
/**
* Alias for [Builder.maxIdleConnections].
*
* This unboxed primitive overload exists for backwards compatibility.
*/
fun maxIdleConnections(maxIdleConnections: Int) =
maxIdleConnections(maxIdleConnections as Int?)
/**
* Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
*/
fun maxIdleConnections(maxIdleConnections: Optional<Int>) =
maxIdleConnections(maxIdleConnections.getOrNull())
/**
* The keep-alive duration for idle connections in the underlying OkHttp connection pool.
*
* If this is set, then [maxIdleConnections] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
this.keepAliveDuration = keepAliveDuration
}
/** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
fun keepAliveDuration(keepAliveDuration: Optional<Duration>) =
keepAliveDuration(keepAliveDuration.getOrNull())
/**
* The socket factory used to secure HTTPS connections.
*
@@ -121,6 +203,17 @@ class LangsmithOkHttpClientAsync private constructor() {
*/
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
/**
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
*
* Defaults to a dedicated cached thread pool.
*
* This class takes ownership of the executor and shuts it down, if possible, when closed.
*/
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
clientOptions.streamHandlerExecutor(streamHandlerExecutor)
}
/**
* The interface to use for delaying execution, like during retries.
*
@@ -154,6 +247,9 @@ class LangsmithOkHttpClientAsync private constructor() {
/**
* Whether to call `validate` on every response before returning it.
*
* Setting this to `true` is _not_ forwards compatible with new types from the API for
* existing fields.
*
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -195,6 +291,25 @@ class LangsmithOkHttpClientAsync private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
/**
* Whether run create/update calls should be automatically batched for tracing.
*
* Defaults to true. Set to false to send run create/update calls through the single-run
* endpoints.
*/
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
clientOptions.autoBatchTracing(autoBatchTracing)
}
/**
* The level at which to log request and response information.
*
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
*
* Defaults to [LogLevel.fromEnv].
*/
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
@@ -205,23 +320,6 @@ class LangsmithOkHttpClientAsync private constructor() {
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
/**
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
* x-organization-id (for org scoped apis).
*/
fun bearerToken(bearerToken: String?) = apply { clientOptions.bearerToken(bearerToken) }
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
fun organizationId(organizationId: String?) = apply {
clientOptions.organizationId(organizationId)
}
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
fun organizationId(organizationId: Optional<String>) =
organizationId(organizationId.getOrNull())
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map<String, Iterable<String>>) = apply {
@@ -321,6 +419,10 @@ class LangsmithOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.maxIdleConnections(maxIdleConnections)
.keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
@@ -8,17 +8,26 @@ import com.langchain.smith.core.http.HttpMethod
import com.langchain.smith.core.http.HttpRequest
import com.langchain.smith.core.http.HttpRequestBody
import com.langchain.smith.core.http.HttpResponse
import com.langchain.smith.core.http.ProxyAuthenticator
import com.langchain.smith.errors.LangChainIoException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Proxy
import java.time.Duration
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
import kotlin.jvm.optionals.getOrNull
import okhttp3.Call
import okhttp3.Callback
import okhttp3.ConnectionPool
import okhttp3.Dispatcher
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -26,17 +35,18 @@ import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
import okio.buffer
import okio.sink
class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpClient) :
HttpClient {
class OkHttpClient
internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
val call = newCall(request, requestOptions)
return try {
call.execute().toResponse()
call.execute().toHttpResponse()
} catch (e: IOException) {
throw LangChainIoException("Request failed", e)
} finally {
@@ -50,20 +60,25 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
): CompletableFuture<HttpResponse> {
val future = CompletableFuture<HttpResponse>()
request.body?.run { future.whenComplete { _, _ -> close() } }
newCall(request, requestOptions)
.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
future.complete(response.toResponse())
}
override fun onFailure(call: Call, e: IOException) {
future.completeExceptionally(LangChainIoException("Request failed", e))
}
val call = newCall(request, requestOptions)
call.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
future.complete(response.toHttpResponse())
}
)
override fun onFailure(call: Call, e: IOException) {
future.completeExceptionally(LangChainIoException("Request failed", e))
}
}
)
future.whenComplete { _, e ->
if (e is CancellationException) {
call.cancel()
}
request.body?.close()
}
return future
}
@@ -77,23 +92,6 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
val clientBuilder = okHttpClient.newBuilder()
val logLevel =
when (System.getenv("LANGCHAIN_LOG")?.lowercase()) {
"info" -> HttpLoggingInterceptor.Level.BASIC
"debug" -> HttpLoggingInterceptor.Level.BODY
else -> null
}
if (logLevel != null) {
clientBuilder.addNetworkInterceptor(
HttpLoggingInterceptor().setLevel(logLevel).apply {
redactHeader("X-API-Key")
redactHeader("X-Tenant-Id")
redactHeader("Authorization")
redactHeader("X-Organization-Id")
}
)
}
requestOptions.timeout?.let {
clientBuilder
.connectTimeout(it.connect())
@@ -106,89 +104,6 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
return client.newCall(request.toRequest(client))
}
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
var body: RequestBody? = body?.toRequestBody()
if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
val builder = Request.Builder().url(toUrl()).method(method.name, body)
headers.names().forEach { name ->
headers.values(name).forEach { builder.addHeader(name, it) }
}
if (
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
) {
builder.addHeader(
"X-Stainless-Read-Timeout",
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
)
}
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
builder.addHeader(
"X-Stainless-Timeout",
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
)
}
return builder.build()
}
/** `OkHttpClient` always requires a request body for some methods. */
private fun requiresBody(method: HttpMethod): Boolean =
when (method) {
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.PATCH -> true
else -> false
}
private fun HttpRequest.toUrl(): String {
val builder = baseUrl.toHttpUrl().newBuilder()
pathSegments.forEach(builder::addPathSegment)
queryParams.keys().forEach { key ->
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
}
return builder.toString()
}
private fun HttpRequestBody.toRequestBody(): RequestBody {
val mediaType = contentType()?.toMediaType()
val length = contentLength()
return object : RequestBody() {
override fun contentType(): MediaType? = mediaType
override fun contentLength(): Long = length
override fun isOneShot(): Boolean = !repeatable()
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
}
}
private fun Response.toResponse(): HttpResponse {
val headers = headers.toHeaders()
return object : HttpResponse {
override fun statusCode(): Int = code
override fun headers(): Headers = headers
override fun body(): InputStream = body!!.byteStream()
override fun close() = body!!.close()
}
}
private fun okhttp3.Headers.toHeaders(): Headers {
val headersBuilder = Headers.builder()
forEach { (name, value) -> headersBuilder.put(name, value) }
return headersBuilder.build()
}
companion object {
@JvmStatic fun builder() = Builder()
}
@@ -197,6 +112,10 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
private var proxyAuthenticator: ProxyAuthenticator? = null
private var maxIdleConnections: Int? = null
private var keepAliveDuration: Duration? = null
private var dispatcherExecutorService: ExecutorService? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
@@ -207,6 +126,36 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
this.proxyAuthenticator = proxyAuthenticator
}
/**
* Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
*
* If this is set, then [keepAliveDuration] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun maxIdleConnections(maxIdleConnections: Int?) = apply {
this.maxIdleConnections = maxIdleConnections
}
/**
* Sets the keep-alive duration for idle connections in the underlying [ConnectionPool].
*
* If this is set, then [maxIdleConnections] must also be set.
*
* If unset, then OkHttp's default is used.
*/
fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
this.keepAliveDuration = keepAliveDuration
}
fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
this.dispatcherExecutorService = dispatcherExecutorService
}
fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
this.sslSocketFactory = sslSocketFactory
}
@@ -222,12 +171,45 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
// `RetryingHttpClient` handles retries if the user enabled them.
.retryOnConnectionFailure(false)
.connectTimeout(timeout.connect())
.readTimeout(timeout.read())
.writeTimeout(timeout.write())
.callTimeout(timeout.request())
.proxy(proxy)
.apply {
proxyAuthenticator?.let { auth ->
proxyAuthenticator { route, response ->
auth
.authenticate(
route?.proxy ?: Proxy.NO_PROXY,
response.request.toHttpRequest(),
response.toHttpResponse(),
)
.getOrNull()
?.toRequest(client = null)
}
}
dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
val maxIdleConnections = maxIdleConnections
val keepAliveDuration = keepAliveDuration
if (maxIdleConnections != null && keepAliveDuration != null) {
connectionPool(
ConnectionPool(
maxIdleConnections,
keepAliveDuration.toNanos(),
TimeUnit.NANOSECONDS,
)
)
} else {
check((maxIdleConnections != null) == (keepAliveDuration != null)) {
"Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set"
}
}
val sslSocketFactory = sslSocketFactory
val trustManager = trustManager
if (sslSocketFactory != null && trustManager != null) {
@@ -249,3 +231,126 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC
)
}
}
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request {
var body: RequestBody? = body?.toRequestBody()
if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
val builder = Request.Builder().url(toUrl()).method(method.name, body)
headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } }
if (client != null) {
if (
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
) {
builder.addHeader(
"X-Stainless-Read-Timeout",
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
)
}
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
builder.addHeader(
"X-Stainless-Timeout",
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
)
}
}
return builder.build()
}
/** `OkHttpClient` always requires a request body for some methods. */
private fun requiresBody(method: HttpMethod): Boolean =
when (method) {
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.PATCH -> true
else -> false
}
private fun HttpRequest.toUrl(): String {
val builder = baseUrl.toHttpUrl().newBuilder()
pathSegments.forEach(builder::addPathSegment)
queryParams.keys().forEach { key ->
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
}
return builder.toString()
}
private fun HttpRequestBody.toRequestBody(): RequestBody {
val mediaType = contentType()?.toMediaType()
val length = contentLength()
return object : RequestBody() {
override fun contentType(): MediaType? = mediaType
override fun contentLength(): Long = length
override fun isOneShot(): Boolean = !repeatable()
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
}
}
private fun Request.toHttpRequest(): HttpRequest {
val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl())
url.pathSegments.forEach(builder::addPathSegment)
url.queryParameterNames.forEach { name ->
url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
}
headers.forEach { (name, value) -> builder.putHeader(name, value) }
body?.let { builder.body(it.toHttpRequestBody()) }
return builder.build()
}
private fun HttpUrl.toBaseUrl(): String = buildString {
append(scheme).append("://").append(host)
if (port != HttpUrl.defaultPort(scheme)) {
append(":").append(port)
}
}
private fun RequestBody.toHttpRequestBody(): HttpRequestBody {
val mediaType = contentType()?.toString()
val length = contentLength()
val isOneShot = isOneShot()
val source = this
return object : HttpRequestBody {
override fun contentType(): String? = mediaType
override fun contentLength(): Long = length
override fun repeatable(): Boolean = !isOneShot
override fun writeTo(outputStream: OutputStream) {
val sink = outputStream.sink().buffer()
source.writeTo(sink)
sink.flush()
}
override fun close() {}
}
}
private fun Response.toHttpResponse(): HttpResponse {
val headers = headers.toHeaders()
return object : HttpResponse {
override fun statusCode(): Int = code
override fun headers(): Headers = headers
override fun body(): InputStream = body!!.byteStream()
override fun close() = body!!.close()
}
}
private fun okhttp3.Headers.toHeaders(): Headers {
val headersBuilder = Headers.builder()
forEach { (name, value) -> headersBuilder.put(name, value) }
return headersBuilder.build()
}
@@ -0,0 +1,9 @@
package com.langchain.smith.client.okhttp
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.tracing.LangsmithClientProvider
/** [LangsmithClientProvider] backed by [LangsmithOkHttpClient]. */
class OkHttpLangsmithClientProvider : LangsmithClientProvider {
override fun createClient(): LangsmithClient = LangsmithOkHttpClient.fromEnv()
}
@@ -0,0 +1 @@
com.langchain.smith.client.okhttp.OkHttpLangsmithClientProvider
@@ -0,0 +1,44 @@
package com.langchain.smith.client.okhttp
import com.github.tomakehurst.wiremock.client.WireMock.*
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
import com.github.tomakehurst.wiremock.junit5.WireMockTest
import com.langchain.smith.core.http.HttpMethod
import com.langchain.smith.core.http.HttpRequest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.parallel.ResourceLock
@WireMockTest
@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
internal class OkHttpClientTest {
private lateinit var baseUrl: String
private lateinit var httpClient: OkHttpClient
@BeforeEach
fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
baseUrl = wmRuntimeInfo.httpBaseUrl
httpClient = OkHttpClient.builder().build()
}
@Test
fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
val responseFuture =
httpClient.executeAsync(
HttpRequest.builder()
.method(HttpMethod.POST)
.baseUrl(baseUrl)
.addPathSegment("something")
.build()
)
val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
responseFuture.cancel(false)
// Should have cancelled the underlying call
assertThat(call.isCanceled()).isTrue()
}
}
+89 -24
View File
@@ -3,53 +3,118 @@ plugins {
id("langchain.publish")
}
val generateVersionProperties by tasks.registering {
val outputDir = layout.buildDirectory.dir("generated/resources/version")
val projectVersion = provider { project.version.toString() }
outputs.dir(outputDir)
inputs.property("version", projectVersion)
doLast {
val dir = outputDir.get().asFile.resolve("com/langchain/smith")
dir.mkdirs()
dir.resolve("version.properties").writeText("version=${projectVersion.get()}\n")
}
}
sourceSets.main {
resources.srcDir(generateVersionProperties)
}
val jacksonVersion = "2.22.1"
val jacksonAnnotationsVersion = "2.22"
configurations.matching { it.name in setOf("testCompileClasspath", "testRuntimeClasspath") }.configureEach {
// Test-only WireMock 4 requires Java 17+ dependencies. Published artifacts still target Java 8.
attributes.attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
configurations.all {
resolutionStrategy {
// Compile and test against a lower Jackson version to ensure we're compatible with it.
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
// Keep all Jackson modules aligned so dependency resolution cannot select a vulnerable
// transitive version from SDK or test dependencies.
force("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
force("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
force("com.fasterxml.jackson.core:jackson-annotations:$jacksonAnnotationsVersion")
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion")
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
force("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
}
}
dependencies {
api("com.fasterxml.jackson.core:jackson-core:2.18.2")
api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
api("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
api("com.google.errorprone:error_prone_annotations:2.33.0")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonAnnotationsVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.github.luben:zstd-jni:1.5.7-7")
implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
// OpenTelemetry dependencies
api("io.opentelemetry:opentelemetry-api:1.32.0")
api("io.opentelemetry:opentelemetry-sdk:1.32.0")
api("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0")
api("io.opentelemetry.semconv:opentelemetry-semconv:1.23.1-alpha")
// OpenAI SDK (for OpenTelemetry wrappers)
api("com.openai:openai-java:4.6.1")
api("io.opentelemetry:opentelemetry-api:1.62.0")
api("io.opentelemetry:opentelemetry-sdk:1.62.0")
api("io.opentelemetry:opentelemetry-exporter-otlp:1.62.0")
api("io.opentelemetry.semconv:opentelemetry-semconv:1.41.1")
// OpenAI SDK (for wrapOpenAI tracing wrapper)
api("com.openai:openai-java:4.30.0")
// Mustache template engine (for prompt template formatting)
implementation("com.samskivert:jmustache:1.16")
// SLF4J for logging (API only - consumers choose implementation)
api("org.slf4j:slf4j-api:2.0.17")
// Anthropic SDK — optional peer dependency for prompt conversion.
// Users who call AnthropicPayload.toAnthropicParams() must add this to their own dependencies.
compileOnly("com.anthropic:anthropic-java:2.18.0")
testImplementation("com.anthropic:anthropic-java:2.18.0")
// Security: constrain vulnerable transitive test dependencies.
// These constraints apply to the test scope only and do not affect published artifacts.
constraints {
testImplementation("org.eclipse.jetty:jetty-server") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-http") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-servlets") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty:jetty-xml") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-common") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-hpack") { version { require("12.1.10") } }
testImplementation("org.eclipse.jetty.http2:http2-server") { version { require("12.1.10") } }
testImplementation("org.bouncycastle:bcpg-jdk18on:1.84")
testImplementation("org.bouncycastle:bcpkix-jdk18on:1.84")
testImplementation("org.bouncycastle:bcprov-jdk18on:1.84")
testImplementation("org.apache.logging.log4j:log4j-core:2.25.4")
testImplementation("org.apache.opennlp:opennlp-tools") { version { require("2.5.9") } }
testImplementation("com.github.jknack:handlebars") { version { require("4.5.2") } }
testImplementation("org.codehaus.plexus:plexus-utils:4.0.3")
testImplementation("org.apache.commons:commons-lang3:3.18.0")
testImplementation("org.apache.commons:commons-compress") { version { require("1.28.0") } }
testImplementation("commons-fileupload:commons-fileupload") { version { require("1.6.0") } }
testImplementation("commons-io:commons-io") { version { require("2.22.0") } }
testImplementation("com.google.guava:guava") { version { require("33.6.0-jre") } }
testImplementation("com.jayway.jsonpath:json-path") { version { require("2.10.0") } }
testImplementation("org.xmlunit:xmlunit-core") { version { require("2.12.0") } }
testImplementation("net.minidev:json-smart") { version { require("2.6.0") } }
}
testImplementation(kotlin("test"))
// Simple logging for tests only
testImplementation("org.slf4j:slf4j-simple:2.0.17")
testImplementation(project(":langsmith-java-client-okhttp"))
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.wiremock:wiremock:4.0.0-beta.37")
testImplementation("org.wiremock:wiremock-junit5:4.0.0-beta.37")
testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
// LangChain4j for testing parallel tool execution context propagation
testImplementation("dev.langchain4j:langchain4j:1.12.2")
testImplementation("dev.langchain4j:langchain4j-core:1.12.2")
}
@@ -1,230 +0,0 @@
package com.langchain.smith.otel;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Configuration for OpenTelemetry trace export.
*
* <p>This class provides configuration options for exporting LangSmith runs as OpenTelemetry traces.
* Configuration can be loaded from environment variables or system properties, or set manually via
* the builder.
*
* <p>Environment variables:
* <ul>
* <li>OTEL_EXPORTER_OTLP_ENDPOINT: The OTEL endpoint URL (default: http://localhost:4318/v1/traces)
* <li>OTEL_EXPORTER_OTLP_ENABLED: Whether export is enabled (default: false)
* <li>OTEL_EXPORTER_OTLP_TIMEOUT: Timeout in seconds (default: 10)
* </ul>
*
* <p>System properties:
* <ul>
* <li>langchain.otel.endpoint
* <li>langchain.otel.enabled
* <li>langchain.otel.timeout
* </ul>
*/
public final class OtelConfig {
private final String endpoint;
private final boolean enabled;
private final Duration timeout;
private final Map<String, String> headers;
private final String serviceName;
private OtelConfig(Builder builder) {
this.endpoint = builder.endpoint;
this.enabled = builder.enabled;
this.timeout = builder.timeout;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.serviceName = builder.serviceName;
}
/**
* Returns the OpenTelemetry endpoint URL.
*
* @return the endpoint URL
*/
public String getEndpoint() {
return endpoint;
}
/**
* Returns whether OpenTelemetry export is enabled.
*
* @return true if enabled, false otherwise
*/
public boolean isEnabled() {
return enabled;
}
/**
* Returns the timeout for export requests.
*
* @return the timeout duration
*/
public Duration getTimeout() {
return timeout;
}
/**
* Returns additional headers to include in export requests.
*
* @return a map of header names to values
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* Returns the service name for OpenTelemetry traces.
*
* @return the service name, or null if not set
*/
public String getServiceName() {
return serviceName;
}
/**
* Creates a new builder for constructing an OtelConfig.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a configuration from environment variables or system properties.
*
* <p>System properties take precedence over environment variables.
*
* @return a configuration loaded from environment/system properties
*/
public static OtelConfig fromEnv() {
String endpoint = System.getProperty("langchain.otel.endpoint") != null
? System.getProperty("langchain.otel.endpoint")
: System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != null
? System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
: "http://localhost:4318/v1/traces";
String enabledStr = System.getProperty("langchain.otel.enabled") != null
? System.getProperty("langchain.otel.enabled")
: System.getenv("OTEL_EXPORTER_OTLP_ENABLED") != null
? System.getenv("OTEL_EXPORTER_OTLP_ENABLED")
: "false";
boolean enabled = "true".equalsIgnoreCase(enabledStr);
String timeoutStr = System.getProperty("langchain.otel.timeout") != null
? System.getProperty("langchain.otel.timeout")
: System.getenv("OTEL_EXPORTER_OTLP_TIMEOUT") != null
? System.getenv("OTEL_EXPORTER_OTLP_TIMEOUT")
: "10";
long timeoutSeconds = 10L;
try {
timeoutSeconds = Long.parseLong(timeoutStr);
} catch (NumberFormatException e) {
// Use default
}
Duration timeout = Duration.ofSeconds(timeoutSeconds);
String serviceName = System.getProperty("langchain.otel.service.name") != null
? System.getProperty("langchain.otel.service.name")
: System.getenv("OTEL_SERVICE_NAME") != null ? System.getenv("OTEL_SERVICE_NAME") : null;
Builder configBuilder = builder().endpoint(endpoint).enabled(enabled).timeout(timeout);
if (serviceName != null) {
configBuilder.serviceName(serviceName);
}
return configBuilder.build();
}
/** Builder for OtelConfig. */
public static final class Builder {
private String endpoint = "http://localhost:4318/v1/traces";
private boolean enabled = false;
private Duration timeout = Duration.ofSeconds(10);
private Map<String, String> headers = new HashMap<>();
private String serviceName = null;
private Builder() {}
/**
* Sets the OpenTelemetry endpoint URL.
*
* @param endpoint the endpoint URL
* @return this builder
*/
public Builder endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
/**
* Sets whether OpenTelemetry export is enabled.
*
* @param enabled true to enable export, false to disable
* @return this builder
*/
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* Sets the timeout for export requests.
*
* @param timeout the timeout duration
* @return this builder
*/
public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Sets additional headers to include in export requests.
*
* @param headers a map of header names to values
* @return this builder
*/
public Builder headers(Map<String, String> headers) {
this.headers.clear();
this.headers.putAll(headers);
return this;
}
/**
* Adds a header to include in export requests.
*
* @param name the header name
* @param value the header value
* @return this builder
*/
public Builder putHeader(String name, String value) {
this.headers.put(name, value);
return this;
}
/**
* Sets the service name for OpenTelemetry traces.
*
* @param serviceName the service name (e.g., "langsmith-java", "my-application")
* @return this builder
*/
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
/**
* Builds the OtelConfig.
*
* @return a new OtelConfig instance
*/
public OtelConfig build() {
return new OtelConfig(this);
}
}
}
@@ -1,143 +0,0 @@
package com.langchain.smith.otel;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
/**
* Utility class for creating OpenTelemetry spans with Gen AI semantic conventions.
* Provides helper methods to create spans for LLM, tool, retrieval, and chain operations.
*/
public final class OtelSpanCreator {
private OtelSpanCreator() {}
/**
* Creates an LLM span with common gen_ai attributes pre-configured.
*
* @param tracer the OpenTelemetry tracer
* @param name the span name
* @param system the AI system (e.g., "openai", "anthropic")
* @param model the model name (e.g., "gpt-4", "claude-3")
* @param serviceName the service name
* @param sessionId optional session ID
* @return a started Span with gen_ai attributes set
*/
public static Span createLlmSpan(
Tracer tracer, String name, String system, String model, String serviceName, String sessionId) {
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "chat");
span.setAttribute(AttributeKey.stringKey("gen_ai.system"), system);
span.setAttribute(AttributeKey.stringKey("gen_ai.request.model"), model);
if (serviceName != null) {
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
}
if (sessionId != null) {
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
}
return span;
}
/**
* Sets the prompt/input on a span.
*
* @param span the span to set the input on
* @param input the input/prompt text
*/
public static void setInput(Span span, String input) {
if (input != null) {
span.setAttribute(AttributeKey.stringKey("gen_ai.prompt"), input);
}
}
/**
* Sets the completion/output on a span.
*
* @param span the span to set the output on
* @param output the output/completion text
*/
public static void setOutput(Span span, String output) {
if (output != null) {
span.setAttribute(AttributeKey.stringKey("gen_ai.completion"), output);
}
}
/**
* Sets token usage information on a span.
*
* @param span the span to set token usage on
* @param inputTokens number of input tokens
* @param outputTokens number of output tokens
*/
public static void setTokenUsage(Span span, int inputTokens, int outputTokens) {
span.setAttribute(AttributeKey.longKey("gen_ai.usage.input_tokens"), (long) inputTokens);
span.setAttribute(AttributeKey.longKey("gen_ai.usage.output_tokens"), (long) outputTokens);
}
/**
* Creates a tool span with common gen_ai attributes pre-configured.
*
* @param tracer the OpenTelemetry tracer
* @param name the span name
* @param toolName the tool name
* @param serviceName the service name
* @param sessionId optional session ID
* @return a started Span with gen_ai attributes set
*/
public static Span createToolSpan(
Tracer tracer, String name, String toolName, String serviceName, String sessionId) {
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "tool");
span.setAttribute(AttributeKey.stringKey("tool.name"), toolName);
if (serviceName != null) {
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
}
if (sessionId != null) {
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
}
return span;
}
/**
* Creates a retrieval span with common gen_ai attributes pre-configured.
*
* @param tracer the OpenTelemetry tracer
* @param name the span name
* @param serviceName the service name
* @param sessionId optional session ID
* @return a started Span with gen_ai attributes set
*/
public static Span createRetrievalSpan(Tracer tracer, String name, String serviceName, String sessionId) {
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "retrieval");
if (serviceName != null) {
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
}
if (sessionId != null) {
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
}
return span;
}
/**
* Creates a chain/workflow span with common gen_ai attributes pre-configured.
*
* @param tracer the OpenTelemetry tracer
* @param name the span name
* @param serviceName the service name
* @param sessionId optional session ID
* @return a started Span with gen_ai attributes set
*/
public static Span createChainSpan(Tracer tracer, String name, String serviceName, String sessionId) {
Span span = tracer.spanBuilder(name).setParent(Context.current()).startSpan();
span.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "chat");
if (serviceName != null) {
span.setAttribute(AttributeKey.stringKey("service.name"), serviceName);
}
if (sessionId != null) {
span.setAttribute(AttributeKey.stringKey("session.id"), sessionId);
}
return span;
}
}
@@ -1,281 +0,0 @@
package com.langchain.smith.otel;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.ResourceAttributes;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages OpenTelemetry SDK for exporting traces to OTLP endpoints.
*
* <p>This class initializes the OpenTelemetry SDK with OTLP HTTP export capabilities,
* providing a Tracer for creating spans with Gen AI semantic conventions.
*/
public final class OtelTraceExporter {
private static final Logger logger = LoggerFactory.getLogger(OtelTraceExporter.class);
private static final String INSTRUMENTATION_NAME = "langsmith-java";
private static final String INSTRUMENTATION_VERSION = "0.1.0";
private final OtelConfig config;
private final OpenTelemetry openTelemetry;
private final Tracer tracer;
private final SdkTracerProvider tracerProvider;
private final String projectName;
private OtelTraceExporter(
OtelConfig config,
OpenTelemetry openTelemetry,
Tracer tracer,
SdkTracerProvider tracerProvider,
String projectName) {
this.config = config;
this.openTelemetry = openTelemetry;
this.tracer = tracer;
this.tracerProvider = tracerProvider;
this.projectName = projectName != null ? projectName : "default";
}
/**
* Get the OpenTelemetry Tracer for creating spans directly.
* @return The OpenTelemetry Tracer instance
*/
public Tracer getTracer() {
return tracer;
}
/**
* Get the project name configured for this exporter.
* @return The project name
*/
public String getProjectName() {
return projectName;
}
/**
* Shuts down the exporter and flushes any pending exports.
*
* @return a CompletableResultCode indicating success or failure
*/
public CompletableResultCode shutdown() {
return tracerProvider.shutdown();
}
/**
* Flushes any pending exports.
*
* @return a CompletableResultCode indicating success or failure
*/
public CompletableResultCode flush() {
return tracerProvider.forceFlush();
}
/**
* Creates an OtelTraceExporter from environment variables or system properties.
*
* @return a new OtelTraceExporter instance
* @see OtelConfig#fromEnv()
*/
public static OtelTraceExporter fromEnv() {
return fromConfig(OtelConfig.fromEnv());
}
/**
* Creates an OtelTraceExporter from a configuration.
* Sets up the complete OpenTelemetry SDK with TracerProvider and BatchSpanProcessor.
*
* @param config the configuration to use
* @return a new OtelTraceExporter instance
*/
public static OtelTraceExporter fromConfig(OtelConfig config) {
String serviceName = config.getServiceName() != null ? config.getServiceName() : "langsmith-app";
Resource resource = Resource.getDefault().toBuilder()
.put(ResourceAttributes.SERVICE_NAME, serviceName)
.put(ResourceAttributes.SERVICE_VERSION, INSTRUMENTATION_VERSION)
.build();
if (!config.isEnabled()) {
SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build();
OpenTelemetry openTelemetry =
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
return new OtelTraceExporter(config, openTelemetry, tracer, tracerProvider, null);
}
// Build the OTLP HTTP exporter
OtlpHttpSpanExporterBuilder exporterBuilder =
OtlpHttpSpanExporter.builder().setEndpoint(config.getEndpoint()).setTimeout(config.getTimeout());
// Add custom headers
for (Map.Entry<String, String> header : config.getHeaders().entrySet()) {
exporterBuilder.addHeader(header.getKey(), header.getValue());
}
OtlpHttpSpanExporter exporter = exporterBuilder.build();
BatchSpanProcessor spanProcessor = BatchSpanProcessor.builder(exporter)
.setScheduleDelay(5, TimeUnit.SECONDS) // Batch every 5 seconds
.setMaxQueueSize(2048)
.setMaxExportBatchSize(512)
.build();
// Create TracerProvider with the processor
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addResource(resource)
.addSpanProcessor(spanProcessor)
.build();
// Create OpenTelemetry SDK
OpenTelemetry openTelemetry =
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
String projectName = config.getHeaders().get("Langsmith-Project");
if (projectName == null) {
projectName = "default";
}
// Log configuration for debugging
logger.debug(
"Created OpenTelemetry SDK with endpoint: {}, timeout: {}", config.getEndpoint(), config.getTimeout());
logger.debug("Headers: {}", config.getHeaders());
logger.debug("Service name: {}, Project name: {}", serviceName, projectName);
return new OtelTraceExporter(config, openTelemetry, tracer, tracerProvider, projectName);
}
/**
* Creates a builder for OtelTraceExporter.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}
/** Builder for OtelTraceExporter. */
public static final class Builder {
private OtelConfig config;
private String endpoint;
private Boolean enabled;
private Duration timeout;
private Map<String, String> headers = new java.util.HashMap<>();
private String serviceName;
private Builder() {}
/**
* Sets the configuration to use.
*
* @param config the configuration
* @return this builder
*/
public Builder config(OtelConfig config) {
this.config = config;
return this;
}
/**
* Sets the OpenTelemetry endpoint URL.
*
* @param endpoint the endpoint URL
* @return this builder
*/
public Builder endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
/**
* Sets whether OpenTelemetry export is enabled.
*
* @param enabled true to enable export, false to disable
* @return this builder
*/
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* Sets the timeout for export requests.
*
* @param timeout the timeout duration
* @return this builder
*/
public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Sets additional headers to include in export requests.
*
* @param headers a map of header names to values
* @return this builder
*/
public Builder headers(Map<String, String> headers) {
this.headers.clear();
this.headers.putAll(headers);
return this;
}
/**
* Adds a header to include in export requests.
*
* @param name the header name
* @param value the header value
* @return this builder
*/
public Builder putHeader(String name, String value) {
this.headers.put(name, value);
return this;
}
/**
* Sets the service name for OpenTelemetry traces.
*
* @param serviceName the service name (e.g., "langsmith-java", "my-application")
* @return this builder
*/
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
/**
* Builds the OtelTraceExporter.
*
* @return a new OtelTraceExporter instance
*/
public OtelTraceExporter build() {
OtelConfig.Builder configBuilder = config != null
? null
: OtelConfig.builder()
.endpoint(endpoint != null ? endpoint : "http://localhost:4318/v1/traces")
.enabled(enabled != null ? enabled : false)
.timeout(timeout != null ? timeout : Duration.ofSeconds(10))
.headers(headers);
if (configBuilder != null && serviceName != null) {
configBuilder.serviceName(serviceName);
}
OtelConfig finalConfig = config != null ? config : configBuilder.build();
return fromConfig(finalConfig);
}
}
}
@@ -1,210 +0,0 @@
package com.langchain.smith.wrappers.openai;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;
import io.opentelemetry.context.Scope;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Immutable context for experiment metadata that will be automatically
* attached to OpenTelemetry spans.
*
* <p>
* This class provides a convenient way to associate experiment metadata with
* LLM calls without requiring manual span creation. When you set experiment
* context using this class, the wrapped OpenAI client will automatically
* attach it to the spans it creates.
*
* <p>
* This class follows the OpenTelemetry Context pattern with immutable
* context objects and explicit scope management.
*
* <p>
* Example usage (recommended - try-with-resources):
*
* <pre>{@code
* // Convenience method (recommended)
* try (Scope scope = ExperimentContext.withExperiment("example-123", "session-789")) {
* ChatCompletion completion = client.chat().completions().create(params);
* }
* }</pre>
*
* <p>
* This is particularly useful for running experiments where you want to link
* LLM traces to specific dataset examples in LangSmith.
*
* <p>
* Thread Safety: This class is thread-safe. Each thread maintains its own
* context via OpenTelemetry's Context mechanism. Context instances are
* immutable and safe to share across threads.
*/
public final class ExperimentContext {
private static final ContextKey<ExperimentData> CONTEXT_KEY = ContextKey.named("langsmith-experiment-context");
private final ExperimentData data;
private ExperimentContext(ExperimentData data) {
this.data = data;
}
/**
* Gets the current ExperimentContext from the OpenTelemetry Context.
* Returns an empty context if no context has been set.
*
* @return the current ExperimentContext
*/
public static ExperimentContext current() {
Context otelContext = Context.current();
ExperimentData data = otelContext.get(CONTEXT_KEY);
return new ExperimentContext(data != null ? data : ExperimentData.empty());
}
/**
* Convenience method to set all experiment context values and return a Scope.
* This is the recommended method for running experiments as it properly links runs to
* the experiment session.
*
* <pre>{@code
* try (Scope scope = ExperimentContext.withExperiment(example.id(), session.id())) {
* ChatCompletion completion = client.chat().completions().create(params);
* }
* }</pre>
*
* @param exampleId the reference example ID from your LangSmith dataset
* @param sessionId the session/experiment UUID
* @return a Scope that will restore the previous context when closed
* @throws IllegalArgumentException if exampleId or sessionId is null or empty
*/
public static Scope withExperiment(String exampleId, String sessionId) {
if (exampleId == null || exampleId.trim().isEmpty()) {
throw new IllegalArgumentException("exampleId cannot be null or empty");
}
if (sessionId == null || sessionId.trim().isEmpty()) {
throw new IllegalArgumentException("sessionId cannot be null or empty");
}
return current()
.withReferenceExampleId(exampleId)
.withSessionId(sessionId)
.makeCurrent();
}
/**
* Returns a new ExperimentContext with the reference example ID set.
*
* @param exampleId the reference example ID from your LangSmith dataset
* @return a new ExperimentContext with the updated value
* @throws IllegalArgumentException if exampleId is null or empty
*/
public ExperimentContext withReferenceExampleId(String exampleId) {
if (exampleId == null || exampleId.trim().isEmpty()) {
throw new IllegalArgumentException("exampleId cannot be null or empty");
}
return new ExperimentContext(data.withReferenceExampleId(exampleId));
}
/**
* Returns a new ExperimentContext with the session ID set.
*
* @param sessionId the session/experiment UUID from LangSmith
* @return a new ExperimentContext with the updated value
* @throws IllegalArgumentException if sessionId is null or empty
*/
public ExperimentContext withSessionId(String sessionId) {
if (sessionId == null || sessionId.trim().isEmpty()) {
throw new IllegalArgumentException("sessionId cannot be null or empty");
}
return new ExperimentContext(data.withSessionId(sessionId));
}
/**
* Returns a new ExperimentContext with custom metadata added.
* Metadata will be attached to spans as attributes prefixed with "langsmith.metadata.".
*
* @param key the metadata key
* @param value the metadata value
* @return a new ExperimentContext with the updated metadata
* @throws IllegalArgumentException if key or value is null or empty
*/
public ExperimentContext withMetadata(String key, String value) {
if (key == null || key.trim().isEmpty()) {
throw new IllegalArgumentException("metadata key cannot be null or empty");
}
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("metadata value cannot be null or empty");
}
return new ExperimentContext(data.withMetadata(key, value));
}
/**
* Makes this ExperimentContext the current context in the OpenTelemetry Context.
* Returns a Scope that will restore the previous context when closed.
*/
private Scope makeCurrent() {
Context otelContext = Context.current().with(CONTEXT_KEY, this.data);
return otelContext.makeCurrent();
}
/**
* Gets the reference example ID.
*
* @return an Optional containing the reference example ID, or empty if not set
*/
public Optional<String> getReferenceExampleId() {
return Optional.ofNullable(data.referenceExampleId);
}
/**
* Gets the session ID.
*
* @return an Optional containing the session ID, or empty if not set
*/
public Optional<String> getSessionId() {
return Optional.ofNullable(data.sessionId);
}
/**
* Gets all custom metadata as an unmodifiable map.
*
* @return an unmodifiable map of metadata key-value pairs
*/
public Map<String, String> getMetadata() {
return data.metadata;
}
/**
* Immutable data holder for experiment context values.
*/
private static class ExperimentData {
final String referenceExampleId;
final String sessionId;
final Map<String, String> metadata;
private ExperimentData(String referenceExampleId, String sessionId, Map<String, String> metadata) {
this.referenceExampleId = referenceExampleId;
this.sessionId = sessionId;
this.metadata = Collections.unmodifiableMap(new HashMap<>(metadata));
}
static ExperimentData empty() {
return new ExperimentData(null, null, new HashMap<>());
}
ExperimentData withReferenceExampleId(String exampleId) {
return new ExperimentData(exampleId, sessionId, metadata);
}
ExperimentData withSessionId(String sessionId) {
return new ExperimentData(referenceExampleId, sessionId, metadata);
}
ExperimentData withMetadata(String key, String value) {
Map<String, String> newMetadata = new HashMap<>(metadata);
newMetadata.put(key, value);
return new ExperimentData(referenceExampleId, sessionId, newMetadata);
}
}
}
@@ -1,539 +0,0 @@
package com.langchain.smith.wrappers.openai;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configuration utility for setting up OpenTelemetry to export traces to
* LangSmith.
*
* <p>
* This class provides a convenient way to configure OpenTelemetry with
* LangSmith's OTLP
* endpoint using a Builder pattern. Configuration can be done programmatically
* or via environment variables.
*
* <p>
* Example usage:
*
* <pre>{@code
* // Configure OpenTelemetry for LangSmith before using the wrapper
* OpenTelemetryConfig.builder()
* .apiKey("your-langsmith-api-key")
* .projectName("your-project-name")
* .build();
*
* // Or using only environment variables (LANGSMITH_API_KEY, LANGSMITH_PROJECT)
* OpenTelemetryConfig.builder().build();
*
* // Now use the wrapped client - traces will be sent to LangSmith
* WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
* }</pre>
*/
public final class OpenTelemetryConfig {
private static final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfig.class);
private OpenTelemetryConfig() {
// Utility class
}
/**
* Default LangSmith base URL.
* Can be overridden by setting LANGSMITH_ENDPOINT environment variable or
* by passing a custom base URL to the configuration methods.
*/
public static final String DEFAULT_BASE_URL = "https://api.smith.langchain.com";
/**
* OTLP traces endpoint path.
* This path is appended to the base URL to construct the full OTLP endpoint.
*/
public static final String OTLP_TRACES_PATH = "/otel/v1/traces";
/**
* Span processor type for configuring how spans are exported.
*/
public enum SpanProcessorType {
/**
* BatchSpanProcessor - Queues spans and exports them in batches.
* Best for production use - efficient and non-blocking.
*/
BATCH,
/**
* SimpleSpanProcessor - Exports spans synchronously on span.end().
* Best for testing - predictable but blocks application thread.
* WARNING: Not recommended for production due to performance impact.
*/
SIMPLE
}
/**
* Builder for configuring OpenTelemetry for LangSmith.
* Provides a fluent API for setting only the parameters you need.
*
* <p>
* Example usage:
* <pre>{@code
* OpenTelemetry otel = OpenTelemetryConfig.builder()
* .apiKey("your-api-key")
* .projectName("MyProject")
* .processorType(SpanProcessorType.SIMPLE)
* .maxBatchSize(1)
* .build();
* }</pre>
*/
public static class Builder {
private String apiKey;
private String projectName;
private String serviceName;
private String baseUrl;
private SpanProcessorType processorType = SpanProcessorType.BATCH;
private int maxBatchSize = 512;
private Builder() {
// Initialize from environment variables as defaults
this.apiKey = System.getenv("LANGSMITH_API_KEY");
this.projectName = System.getenv("LANGSMITH_PROJECT");
this.serviceName = System.getenv("OTEL_SERVICE_NAME");
this.baseUrl = System.getenv("LANGSMITH_ENDPOINT");
}
/**
* Sets the LangSmith API key (optional, defaults to LANGSMITH_API_KEY env var).
*
* <p>
* This is your authentication key for LangSmith. You can find it in your
* LangSmith account settings.
*
* <p>
* If not set explicitly, the builder will use the value from the
* LANGSMITH_API_KEY environment variable. At least one of these must be provided.
*
* @param apiKey your LangSmith API key
* @return this Builder for method chaining
* @throws IllegalStateException if apiKey is null or empty when build() is called
*/
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
/**
* Sets the LangSmith project name (optional, defaults to LANGSMITH_PROJECT env var).
*
* <p>
* If set, all traces will be sent to this project in LangSmith. The project
* will be created automatically if it doesn't exist.
*
* <p>
* If not set explicitly, the builder will use the value from the
* LANGSMITH_PROJECT environment variable.
*
* <p>
* <b>Note:</b> For experiments, you may want to leave this unset and instead
* use {@link ExperimentContext#withExperiment(String, String)} to set
* the session ID via span attributes. This prevents the project name from
* overwriting the session's reference_dataset_id.
*
* @param projectName your LangSmith project name
* @return this Builder for method chaining
*/
public Builder projectName(String projectName) {
this.projectName = projectName;
return this;
}
/**
* Sets the service name for OpenTelemetry (optional, defaults to OTEL_SERVICE_NAME env var).
*
* <p>
* This identifies your application in the traces. If not set explicitly,
* the builder will use the value from the OTEL_SERVICE_NAME environment variable,
* or fall back to "langsmith-java-otel-wrappers" if not set.
*
* <p>
* The service name appears in the trace metadata and can be used to filter
* traces from different services.
*
* @param serviceName the service name to identify your application
* @return this Builder for method chaining
*/
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
/**
* Sets the LangSmith base URL (optional, defaults to LANGSMITH_ENDPOINT env var).
*
* <p>
* The base URL is used to construct the OTLP endpoint by appending "/otel/v1/traces".
* If not set explicitly, the builder will use the value from the LANGSMITH_ENDPOINT
* environment variable, or default to "https://api.smith.langchain.com".
*
* <p>
* This is useful for:
* <ul>
* <li>Self-hosted LangSmith instances</li>
* <li>Internal/development environments</li>
* <li>Testing against different LangSmith deployments</li>
* </ul>
*
* @param baseUrl the LangSmith base URL (e.g., "https://dev.api.smith.langchain.com")
* @return this Builder for method chaining
*/
public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
/**
* Sets the span processor type (optional).
*
* <p>
* Determines how spans are processed and exported:
*
* <ul>
* <li><b>BATCH</b> (default, recommended for production):
* <ul>
* <li>Queues spans and exports them in batches</li>
* <li>Non-blocking - doesn't slow down your application</li>
* <li>Efficient - reduces network overhead</li>
* <li>May have a slight delay before spans appear in LangSmith</li>
* </ul>
* </li>
* <li><b>SIMPLE</b> (recommended for testing/examples):
* <ul>
* <li>Exports spans immediately when span.end() is called</li>
* <li>Blocking - waits for export to complete</li>
* <li>Predictable - spans appear in LangSmith right away</li>
* <li><b>Warning:</b> Can impact performance, not recommended for production</li>
* </ul>
* </li>
* </ul>
*
* <p>
* <b>When to use SIMPLE:</b>
* <ul>
* <li>Short-lived applications or scripts</li>
* <li>Testing and debugging</li>
* <li>When you need immediate visibility of traces</li>
* </ul>
*
* <p>
* <b>When to use BATCH:</b>
* <ul>
* <li>Production applications</li>
* <li>High-throughput services</li>
* <li>Long-running applications</li>
* <li>When performance is critical</li>
* </ul>
*
* @param processorType the span processor type (BATCH or SIMPLE)
* @return this Builder for method chaining
* @see SpanProcessorType
*/
public Builder processorType(SpanProcessorType processorType) {
this.processorType = processorType;
return this;
}
/**
* Sets the maximum batch size for span export (optional).
*
* <p>
* <b>Only applies to BATCH processor type.</b> Ignored when using SIMPLE processor.
*
* <p>
* Controls how many spans are batched together before triggering an export:
* <ul>
* <li><b>Default: 512</b> - Good balance between efficiency and latency</li>
* <li><b>1</b> - Exports immediately (still non-blocking unlike SIMPLE)</li>
* <li><b>Higher values</b> - More efficient but increased latency</li>
* </ul>
*
* <p>
* <b>Setting to 1:</b> If you want immediate export but don't want to block
* your application thread, use BATCH processor with maxBatchSize=1 instead of
* SIMPLE processor. This gives you non-blocking immediate export.
*
* <p>
* Example for immediate non-blocking export:
* <pre>{@code
* OpenTelemetryConfig.builder()
* .apiKey(apiKey)
* .processorType(SpanProcessorType.BATCH)
* .maxBatchSize(1) // Export immediately but don't block
* .build();
* }</pre>
*
* @param maxBatchSize the maximum batch size (1-512, default 512)
* @return this Builder for method chaining
*/
public Builder maxBatchSize(int maxBatchSize) {
this.maxBatchSize = maxBatchSize;
return this;
}
/**
* Builds and configures the OpenTelemetry instance.
*
* @return the configured OpenTelemetry instance
* @throws IllegalStateException if apiKey is not set
*/
public OpenTelemetry build() {
// Validate required fields
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalStateException(
"LangSmith API key is required. Set it using apiKey() or LANGSMITH_API_KEY environment"
+ " variable.");
}
// Build OTLP endpoint from base URL
String endpointUrl = buildOtlpEndpoint(baseUrl);
// Create OTLP HTTP exporter configured for LangSmith
OtlpHttpSpanExporterBuilder exporterBuilder =
OtlpHttpSpanExporter.builder().setEndpoint(endpointUrl).addHeader("x-api-key", apiKey);
// Only add project header if projectName is not null and not empty
if (projectName != null && !projectName.isEmpty()) {
exporterBuilder.addHeader("Langsmith-Project", projectName);
}
OtlpHttpSpanExporter spanExporter = exporterBuilder.build();
// Wrap exporter to log export errors
SpanExporter loggingExporter = new LoggingSpanExporter(spanExporter);
// Create resource with service name
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put("service.name", serviceName != null ? serviceName : "langsmith-java-otel-wrappers")
.build());
// Build and configure span processor based on type
SpanProcessor spanProcessor;
if (processorType == SpanProcessorType.SIMPLE) {
// SimpleSpanProcessor - exports synchronously on span.end()
// Good for testing, but blocks the application thread
spanProcessor = SimpleSpanProcessor.create(loggingExporter);
} else {
// BatchSpanProcessor - queues and exports in batches
// Good for production, non-blocking
// If maxBatchSize is 1, spans are exported immediately as they complete
spanProcessor = BatchSpanProcessor.builder(loggingExporter)
.setScheduleDelay(100, TimeUnit.MILLISECONDS) // Export every 100ms
.setMaxExportBatchSize(maxBatchSize) // Trigger export when batch reaches this size
.setExporterTimeout(5, TimeUnit.SECONDS) // 5 second timeout
.build();
}
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(spanProcessor)
.setResource(resource)
.build();
return OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal();
}
}
/**
* Creates a new Builder for configuring OpenTelemetry.
*
* @return a new Builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Forces flushing of all pending spans to ensure they are exported.
* This should be called before application shutdown to ensure all spans
* are sent to LangSmith.
*
* @return true if flush completed successfully, false otherwise
*/
public static boolean flush() {
return flush(5, TimeUnit.SECONDS);
}
/**
* Forces flushing of all pending spans to ensure they are exported.
* This should be called before application shutdown to ensure all spans
* are sent to LangSmith.
*
* @param timeout the maximum time to wait for flush to complete
* @param unit the time unit of the timeout
* @return true if flush completed successfully, false otherwise
*/
public static boolean flush(long timeout, TimeUnit unit) {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
if (openTelemetry instanceof OpenTelemetrySdk) {
// Don't close the SDK instance - it's the global instance that should remain
// alive
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
if (tracerProvider != null) {
try {
io.opentelemetry.sdk.common.CompletableResultCode result = tracerProvider.forceFlush();
result.join(timeout, unit);
if (!result.isSuccess()) {
logger.warn("Flush did not complete successfully");
}
return result.isSuccess();
} catch (Exception e) {
logger.warn("Failed to flush spans", e);
return false;
}
}
}
return true;
}
/**
* Builds the OTLP endpoint URL from a base URL.
*
* <p>
* The endpoint is constructed as: baseUrl + OTLP_TRACES_PATH
*
* <p>
* If baseUrl is null or empty, defaults to DEFAULT_BASE_URL.
*
* @param baseUrl the base URL (can be null)
* @return the OTLP endpoint URL
*/
private static String buildOtlpEndpoint(String baseUrl) {
// Use provided base URL or default
String effectiveBaseUrl = baseUrl;
if (effectiveBaseUrl == null || effectiveBaseUrl.isEmpty()) {
effectiveBaseUrl = System.getenv("LANGSMITH_ENDPOINT");
}
if (effectiveBaseUrl == null || effectiveBaseUrl.isEmpty()) {
effectiveBaseUrl = DEFAULT_BASE_URL;
}
// Remove trailing slash if present
if (effectiveBaseUrl.endsWith("/")) {
effectiveBaseUrl = effectiveBaseUrl.substring(0, effectiveBaseUrl.length() - 1);
}
return effectiveBaseUrl + OTLP_TRACES_PATH;
}
/**
* Shuts down the OpenTelemetry SDK and ensures all spans are exported.
* This should be called before application shutdown.
*/
public static void shutdown() {
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
if (openTelemetry instanceof OpenTelemetrySdk) {
// Don't close the SDK instance - it's the global instance that should remain
// alive
@SuppressWarnings("resource")
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
if (tracerProvider != null) {
try {
tracerProvider.shutdown().join(5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.warn("Failed to shutdown OpenTelemetry", e);
}
}
}
}
/**
* Wrapper SpanExporter that logs export errors to help debug issues.
*/
private static class LoggingSpanExporter implements SpanExporter {
private final SpanExporter delegate;
private static final boolean DEBUG =
Boolean.getBoolean("langsmith.debug") || "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
LoggingSpanExporter(SpanExporter delegate) {
this.delegate = delegate;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode export(
java.util.Collection<io.opentelemetry.sdk.trace.data.SpanData> spans) {
if (DEBUG) {
logger.debug("[LangSmith] Exporting " + spans.size() + " span(s):");
for (io.opentelemetry.sdk.trace.data.SpanData span : spans) {
logger.debug(" - " + span.getName()
+ " (kind=" + span.getKind()
+ ", attributes=" + span.getAttributes().size() + ")");
}
}
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.export(spans);
// For SimpleSpanProcessor, wait for the result synchronously to get immediate
// feedback
// For BatchSpanProcessor, this will return immediately but we can still check
// status
if (DEBUG) {
// Wait up to 5 seconds for the result
try {
result.join(5, java.util.concurrent.TimeUnit.SECONDS);
if (!result.isSuccess()) {
logger.error("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
logger.error(" This usually indicates a network error or authentication problem");
logger.error(" Check your LANGSMITH_API_KEY and network connectivity");
} else {
logger.debug("[LangSmith] Successfully exported " + spans.size() + " span(s)");
}
} catch (Exception e) {
logger.error("[LangSmith ERROR] Exception waiting for export result", e);
}
} else {
// Without DEBUG, still log errors but don't block
result.whenComplete(() -> {
if (!result.isSuccess()) {
logger.error("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
logger.error(" This usually indicates a network error or authentication problem");
logger.error(" Check your LANGSMITH_API_KEY and network connectivity");
}
});
}
return result;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode flush() {
if (DEBUG) {
logger.debug("[LangSmith] Flushing spans...");
}
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.flush();
result.whenComplete(() -> {
if (!result.isSuccess()) {
logger.error("[LangSmith ERROR] Failed to flush spans");
} else if (DEBUG) {
logger.debug("[LangSmith] Flush completed successfully");
}
});
return result;
}
@Override
public io.opentelemetry.sdk.common.CompletableResultCode shutdown() {
if (DEBUG) {
logger.debug("[LangSmith] Shutting down span exporter...");
}
return delegate.shutdown();
}
}
}
@@ -1,120 +0,0 @@
package com.langchain.smith.wrappers.openai;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
/** Internal utility for OpenTelemetry span creation and management. */
final class TracingUtils {
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
private TracingUtils() {}
static Tracer getTracer() {
try {
Tracer tracer = io.opentelemetry.api.GlobalOpenTelemetry.get().getTracer(INSTRUMENTATION_NAME);
boolean debug =
Boolean.getBoolean("langsmith.debug") || "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
if (debug) {
io.opentelemetry.api.OpenTelemetry otel = io.opentelemetry.api.GlobalOpenTelemetry.get();
boolean isNoop = otel.getClass().getName().contains("Noop");
System.out.println("[TracingUtils] Tracer obtained: "
+ tracer.getClass().getName() + ", OpenTelemetry isNoop: " + isNoop);
}
return tracer;
} catch (Exception e) {
return io.opentelemetry.api.GlobalOpenTelemetry.get().getTracer(INSTRUMENTATION_NAME);
}
}
static SpanBuilder createSpanBuilder(String model, String operationType, String spanKind) {
Tracer tracer = getTracer();
String spanName = operationType + " " + (model != null ? model : "unknown");
SpanBuilder builder = tracer.spanBuilder(spanName)
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.system", "openai")
.setAttribute("gen_ai.operation.name", operationType)
.setAttribute("gen_ai.provider.name", "openai");
if (spanKind != null) {
builder.setAttribute("langsmith.span.kind", spanKind);
}
return builder;
}
static SpanBuilder createSpanBuilder(String model, String operationType) {
return createSpanBuilder(model, operationType, "llm");
}
static void setRequestAttributes(Span span, String model) {
if (model != null) {
span.setAttribute("gen_ai.request.model", model);
}
}
static void setRequestParameters(Span span, Double temperature, Double topP, Long maxTokens) {
if (temperature != null) {
span.setAttribute("gen_ai.request.temperature", temperature);
}
if (topP != null) {
span.setAttribute("gen_ai.request.top_p", topP);
}
if (maxTokens != null) {
span.setAttribute("gen_ai.request.max_tokens", maxTokens);
}
}
static void setInputMessages(Span span, String messagesJson) {
if (messagesJson != null) {
span.setAttribute("gen_ai.input.messages", messagesJson);
}
}
static void setOutputMessages(Span span, String messagesJson) {
if (messagesJson != null) {
span.setAttribute("gen_ai.output.messages", messagesJson);
}
}
static void setResponseAttributes(Span span, Long inputTokens, Long outputTokens, Long totalTokens) {
if (inputTokens != null) {
span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
}
if (outputTokens != null) {
span.setAttribute("gen_ai.usage.output_tokens", outputTokens);
}
if (totalTokens != null) {
span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
}
}
static void setResponseMetadata(Span span, String responseModel, String finishReason) {
if (responseModel != null) {
span.setAttribute("gen_ai.response.model", responseModel);
}
if (finishReason != null) {
span.setAttribute("gen_ai.response.finish_reason", finishReason);
}
}
static void recordException(Span span, Throwable exception) {
span.recordException(exception);
span.setAttribute("error", true);
}
static String escapeJsonString(String str) {
if (str == null) {
return "";
}
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
@@ -1,899 +0,0 @@
package com.langchain.smith.wrappers.openai;
import com.openai.core.ClientOptions;
import com.openai.core.RequestOptions;
import com.openai.core.http.StreamResponse;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionChunk;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
import com.openai.models.chat.completions.StructuredChatCompletion;
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
import com.openai.services.blocking.ChatService;
import com.openai.services.blocking.chat.ChatCompletionService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wrapped ChatService that adds OpenTelemetry tracing to chat completion
* operations.
*/
class WrappedChatService implements ChatService {
private final ChatService delegate;
WrappedChatService(ChatService delegate) {
this.delegate = delegate;
}
@Override
public ChatService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ChatService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedChatService(delegate.withOptions(options));
}
@Override
public ChatCompletionService completions() {
return new WrappedChatCompletionService(delegate.completions());
}
/**
* Wrapped ChatCompletionService that adds tracing to create operations.
*/
private static class WrappedChatCompletionService implements ChatCompletionService {
private static final Logger logger = LoggerFactory.getLogger(WrappedChatCompletionService.class);
private final ChatCompletionService delegate;
WrappedChatCompletionService(ChatCompletionService delegate) {
this.delegate = delegate;
}
@Override
public ChatCompletionService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ChatCompletionService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedChatCompletionService(delegate.withOptions(options));
}
@Override
public com.openai.services.blocking.chat.completions.MessageService messages() {
return delegate.messages();
}
@Override
public ChatCompletion create(ChatCompletionCreateParams params) {
return create(params, null);
}
@Override
public ChatCompletion create(ChatCompletionCreateParams params, RequestOptions requestOptions) {
// Extract model from params for span naming
String model = params.model() != null ? params.model().toString() : null;
Span span = TracingUtils.createSpanBuilder(model, "chat").startSpan();
// Debug: Check if span is recording (not a noop span)
if (logger.isDebugEnabled()) {
boolean isRecording = span.isRecording();
logger.debug(
"[WrappedChatService] Created span: {}, isRecording: {}, traceId: {}",
span.getSpanContext().getSpanId(),
isRecording,
span.getSpanContext().getTraceId());
}
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
Long maxTokens = params.maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params);
TracingUtils.setInputMessages(span, inputMessagesJson);
ChatCompletion result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model and finish reason
String responseModel = result.model();
String finishReason = !result.choices().isEmpty()
? result.choices().get(0).finishReason().toString()
: "stop";
TracingUtils.setResponseMetadata(span, responseModel, finishReason);
// Capture output messages in JSON format
String outputMessagesJson = formatOutputMessages(result);
TracingUtils.setOutputMessages(span, outputMessagesJson);
// Extract usage information from result
result.usage().ifPresent(usage -> {
TracingUtils.setResponseAttributes(
span, (long) usage.promptTokens(), (long) usage.completionTokens(), (long)
usage.totalTokens());
});
// Create tool call spans for any tool calls in the response
createToolCallSpans(result, span);
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
if (logger.isDebugEnabled()) {
logger.debug(
"[WrappedChatService] Ending span: {}",
span.getSpanContext().getSpanId());
}
span.end();
if (logger.isDebugEnabled()) {
logger.debug(
"[WrappedChatService] Span ended: {}",
span.getSpanContext().getSpanId());
}
}
}
/**
* Sets experiment context attributes on the span if they are present.
* This includes reference example ID, session ID, and metadata.
*
* @param span the span to set attributes on
*/
private void setExperimentContextAttributes(Span span) {
// Set reference example ID if present
ExperimentContext.current()
.getReferenceExampleId()
.filter(id -> !id.isEmpty())
.ifPresent(id -> span.setAttribute("langsmith.reference_example_id", id));
// Set session ID (experiment ID) if present
// This is critical for linking runs to experiments in the dataset's Experiments tab
ExperimentContext.current()
.getSessionId()
.filter(id -> !id.isEmpty())
.ifPresent(id -> span.setAttribute("langsmith.trace.session_id", id));
// Set custom metadata
java.util.Map<String, String> metadata = ExperimentContext.current().getMetadata();
for (java.util.Map.Entry<String, String> entry : metadata.entrySet()) {
span.setAttribute("langsmith.metadata." + entry.getKey(), entry.getValue());
}
}
/**
* Formats input messages from ChatCompletionCreateParams as a JSON array
* string.
*
* <p>
* Properly formats messages as JSON with role and content, following
* LangSmith conventions.
*/
private String formatInputMessages(ChatCompletionCreateParams params) {
if (params.messages().isEmpty()) {
return "[]";
}
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Processing {} message(s)",
params.messages().size());
}
StringBuilder json = new StringBuilder("[");
boolean first = true;
for (com.openai.models.chat.completions.ChatCompletionMessageParam messageParam : params.messages()) {
if (!first) {
json.append(",");
}
first = false;
json.append("{");
String role = null;
String content = null;
// Try to extract role and content based on message type
// The OpenAI SDK uses different message types (UserMessage, SystemMessage,
// etc.)
String className = messageParam.getClass().getSimpleName();
String fullClassName = messageParam.getClass().getName();
// Debug logging
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] Processing message: {}", fullClassName);
java.lang.reflect.Method[] allMethods =
messageParam.getClass().getMethods();
logger.debug(
"[formatInputMessages] Available methods: {}",
java.util.Arrays.stream(allMethods)
.map(m -> m.getName() + "(" + m.getParameterCount() + ")")
.collect(java.util.stream.Collectors.joining(", ")));
}
// ChatCompletionMessageParam is a union type - check type FIRST before calling
// as*() methods
// This prevents InvocationTargetException when calling asUser() on non-user
// messages
Object actualMessage = null;
try {
// Use type checking methods first to avoid exceptions
java.lang.reflect.Method isUserMethod =
messageParam.getClass().getMethod("isUser");
java.lang.reflect.Method isSystemMethod =
messageParam.getClass().getMethod("isSystem");
java.lang.reflect.Method isAssistantMethod =
messageParam.getClass().getMethod("isAssistant");
java.lang.reflect.Method isToolMethod =
messageParam.getClass().getMethod("isTool");
boolean isUser = (Boolean) isUserMethod.invoke(messageParam);
boolean isSystem = (Boolean) isSystemMethod.invoke(messageParam);
boolean isAssistant = (Boolean) isAssistantMethod.invoke(messageParam);
boolean isTool = (Boolean) isToolMethod.invoke(messageParam);
if (isUser) {
java.lang.reflect.Method asUserMethod =
messageParam.getClass().getMethod("asUser");
actualMessage = asUserMethod.invoke(messageParam);
role = "user";
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Found user message: {}",
actualMessage.getClass().getName());
}
} else if (isSystem) {
java.lang.reflect.Method asSystemMethod =
messageParam.getClass().getMethod("asSystem");
actualMessage = asSystemMethod.invoke(messageParam);
role = "system";
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Found system message: {}",
actualMessage.getClass().getName());
}
} else if (isAssistant) {
java.lang.reflect.Method asAssistantMethod =
messageParam.getClass().getMethod("asAssistant");
actualMessage = asAssistantMethod.invoke(messageParam);
role = "assistant";
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Found assistant message: {}",
actualMessage.getClass().getName());
}
} else if (isTool) {
java.lang.reflect.Method asToolMethod =
messageParam.getClass().getMethod("asTool");
actualMessage = asToolMethod.invoke(messageParam);
role = "tool";
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Found tool message: {}",
actualMessage.getClass().getName());
}
}
// Now get content from the actual message object
if (actualMessage != null) {
try {
java.lang.reflect.Method contentMethod =
actualMessage.getClass().getMethod("content");
Object contentResult = contentMethod.invoke(actualMessage);
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] content() returned: {}",
contentResult != null
? contentResult.getClass().getName()
: "null");
}
// Content might be a Content object with text() method
if (contentResult != null) {
try {
java.lang.reflect.Method textMethod =
contentResult.getClass().getMethod("text");
Object textResult = textMethod.invoke(contentResult);
if (textResult instanceof java.util.Optional) {
@SuppressWarnings("unchecked")
java.util.Optional<String> textOpt = (java.util.Optional<String>) textResult;
if (textOpt.isPresent()) {
content = textOpt.get();
}
} else if (textResult instanceof String) {
content = (String) textResult;
}
} catch (NoSuchMethodException e) {
// Content might be a String directly
if (contentResult instanceof String) {
content = (String) contentResult;
} else if (contentResult instanceof java.util.Optional) {
@SuppressWarnings("unchecked")
java.util.Optional<String> contentOpt =
(java.util.Optional<String>) contentResult;
if (contentOpt.isPresent()) {
content = contentOpt.get();
}
}
}
}
} catch (NoSuchMethodException e) {
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] No content() method on actual message");
}
}
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] Error accessing message", e);
}
}
// Role should already be set from asUser/asSystem/asAssistant above
// If not, try to get it from the message
if (role == null) {
try {
java.lang.reflect.Method roleMethod =
messageParam.getClass().getMethod("role");
Object roleResult = roleMethod.invoke(messageParam);
if (roleResult != null) {
role = roleResult.toString().toLowerCase();
}
} catch (NoSuchMethodException e) {
// Fallback: determine role from class name
if (className.contains("User") || fullClassName.contains("User")) {
role = "user";
} else if (className.contains("System") || fullClassName.contains("System")) {
role = "system";
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
role = "assistant";
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
role = "tool";
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] Error calling role()", e);
}
// Fallback to class name
if (className.contains("User") || fullClassName.contains("User")) {
role = "user";
} else if (className.contains("System") || fullClassName.contains("System")) {
role = "system";
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
role = "assistant";
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
role = "tool";
}
}
}
// Final fallback: try toString parsing
if (content == null) {
String messageStr = messageParam.toString();
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] toString(): {}", messageStr);
}
// For tool messages, look for content=Content{text={...}} pattern
if (role != null && role.equals("tool")) {
// Extract JSON content from tool messages:
// content=Content{text={"key":"value"}}
java.util.regex.Pattern toolContentPattern =
java.util.regex.Pattern.compile("text=\\{([^}]+)\\}");
java.util.regex.Matcher toolMatcher = toolContentPattern.matcher(messageStr);
if (toolMatcher.find()) {
content = "{" + toolMatcher.group(1) + "}";
} else {
// Try simpler pattern: text="..."
java.util.regex.Pattern simplePattern =
java.util.regex.Pattern.compile("text=[\"']([^\"']+)[\"']");
java.util.regex.Matcher simpleMatcher = simplePattern.matcher(messageStr);
if (simpleMatcher.find()) {
content = simpleMatcher.group(1);
}
}
}
// Improved extraction: try multiple patterns
if (content == null) {
// Pattern 1: content="..."
int contentIdx = messageStr.indexOf("content=");
if (contentIdx >= 0) {
// Look for Content{text="..."} pattern
int textIdx = messageStr.indexOf("text=", contentIdx);
if (textIdx > contentIdx) {
int start = messageStr.indexOf("\"", textIdx);
if (start >= 0) {
// Find matching closing quote, handling escaped quotes
int end = start + 1;
while (end < messageStr.length() && messageStr.charAt(end) != '"') {
if (messageStr.charAt(end) == '\\') {
end += 2; // Skip escaped character
} else {
end++;
}
}
if (end < messageStr.length()) {
content = messageStr.substring(start + 1, end);
}
}
}
// Fallback to simple quote matching
if (content == null) {
int start = messageStr.indexOf("\"", contentIdx);
if (start >= 0) {
int end = messageStr.indexOf("\"", start + 1);
if (end > start) {
content = messageStr.substring(start + 1, end);
}
} else {
// Try single quotes
start = messageStr.indexOf("'", contentIdx);
if (start >= 0) {
int end = messageStr.indexOf("'", start + 1);
if (end > start) {
content = messageStr.substring(start + 1, end);
}
}
}
}
}
}
// Pattern 2: Look for content in JSON-like format
if (content == null) {
java.util.regex.Pattern pattern =
java.util.regex.Pattern.compile("content[=:]\\s*[\"']([^\"']+)[\"']");
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
if (matcher.find()) {
content = matcher.group(1);
}
}
// Pattern 3: If still null, try to extract from the end of the string
if (content == null && messageStr.length() > 0) {
// Last resort: try to find any quoted string that might be content
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[\"']([^\"']+)[\"']");
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
if (matcher.find()) {
String potentialContent = matcher.group(1);
// Only use if it looks like actual content (not a class name or role)
if (!potentialContent.contains("com.openai")
&& !potentialContent.equals("user")
&& !potentialContent.equals("system")
&& !potentialContent.equals("assistant")
&& !potentialContent.equals("tool")
&& potentialContent.length() > 0) {
content = potentialContent;
}
}
}
}
// Build JSON
if (role != null) {
json.append("\"role\":\"").append(role).append("\"");
}
if (content != null) {
if (role != null) {
json.append(",");
}
json.append("\"content\":\"")
.append(TracingUtils.escapeJsonString(content))
.append("\"");
}
if (logger.isDebugEnabled()) {
logger.debug(
"[formatInputMessages] Final: role={}, content={}",
role,
content != null ? content.substring(0, Math.min(50, content.length())) : "null");
}
json.append("}");
}
json.append("]");
String result = json.toString();
if (logger.isDebugEnabled()) {
logger.debug("[formatInputMessages] Final JSON: {}", result);
}
return result;
}
/**
* Creates tool call spans for any tool calls detected in the chat completion
* response.
*
* @param completion the chat completion response
* @param parentSpan the parent span (the chat completion span)
*/
private void createToolCallSpans(ChatCompletion completion, Span parentSpan) {
if (completion.choices() == null || completion.choices().isEmpty()) {
return;
}
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
// Check if message has tool calls
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
if (toolCallsOpt.isPresent()) {
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
// Check if it's a function tool call
if (toolCall.isFunction()) {
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
createToolCallSpan(functionToolCall, parentSpan);
}
// Note: Custom tool calls are not yet supported
}
}
}
}
/**
* Creates a single tool call span from a function tool call object.
*
* @param functionToolCall the function tool call object from the OpenAI SDK
* @param parentSpan the parent span (the chat completion span)
*/
private void createToolCallSpan(ChatCompletionMessageFunctionToolCall functionToolCall, Span parentSpan) {
try {
io.opentelemetry.api.OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
io.opentelemetry.api.trace.Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
// Extract tool call information directly from the SDK objects
String toolCallId = functionToolCall.id();
com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall.Function function =
functionToolCall.function();
String toolName = function.name();
String toolArguments = function.arguments();
// Create span name
String spanName = toolName != null ? "tool_call " + toolName : "tool_call";
// Create tool call span as a child of the parent span
io.opentelemetry.api.trace.Span toolCallSpan = tracer.spanBuilder(spanName)
.setSpanKind(io.opentelemetry.api.trace.SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "tool_call")
.setAttribute("langsmith.span.kind", "tool")
.setAttribute("gen_ai.system", "openai")
.setAttribute("gen_ai.provider.name", "openai")
.startSpan();
try (io.opentelemetry.context.Scope toolScope = toolCallSpan.makeCurrent()) {
// Set tool call attributes
if (toolCallId != null) {
toolCallSpan.setAttribute("gen_ai.tool.call.id", toolCallId);
}
if (toolName != null) {
toolCallSpan.setAttribute("gen_ai.tool.name", toolName);
toolCallSpan.setAttribute("langsmith.trace.name", "Tool Call: " + toolName);
}
if (toolArguments != null && !toolArguments.isEmpty()) {
toolCallSpan.setAttribute("gen_ai.tool.arguments", toolArguments);
}
if (logger.isDebugEnabled()) {
logger.debug(
"[WrappedChatService] Created tool call span: {}, tool={}, arguments={}, parent={}",
toolCallSpan.getSpanContext().getSpanId(),
toolName,
toolArguments,
parentSpan.getSpanContext().getSpanId());
}
// Note: Tool call result would be set when the tool is actually executed
// This span represents the tool call request, not the execution
} finally {
toolCallSpan.end();
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("[WrappedChatService] Error creating tool call span", e);
}
}
}
/**
* Formats output messages from ChatCompletion as a JSON array string.
*/
private String formatOutputMessages(ChatCompletion completion) {
if (completion.choices() == null || completion.choices().isEmpty()) {
return "[]";
}
StringBuilder json = new StringBuilder("[");
boolean first = true;
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
if (!first) {
json.append(",");
}
first = false;
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
json.append("{");
// Add role
json.append("\"role\":\"assistant\"");
// Add content if present
message.content().ifPresent(content -> {
json.append(",\"content\":\"")
.append(TracingUtils.escapeJsonString(content))
.append("\"");
});
json.append("}");
}
json.append("]");
return json.toString();
}
@Override
public <T> StructuredChatCompletion<T> create(StructuredChatCompletionCreateParams<T> params) {
return create(params, null);
}
@Override
public <T> StructuredChatCompletion<T> create(
StructuredChatCompletionCreateParams<T> params, RequestOptions requestOptions) {
// Get model from the underlying params - StructuredChatCompletionCreateParams
// wraps ChatCompletionCreateParams
String model = params != null
&& params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: null;
Span span = TracingUtils.createSpanBuilder(model, "chat").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available
if (params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
Long maxTokens = params.rawParams().maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params.rawParams());
TracingUtils.setInputMessages(span, inputMessagesJson);
}
StructuredChatCompletion<T> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// For structured completions, we'll just set basic model info
// The actual structured output is in the parsed result
TracingUtils.setResponseMetadata(span, model, null);
result.usage().ifPresent(usage -> {
TracingUtils.setResponseAttributes(
span, (long) usage.promptTokens(), (long) usage.completionTokens(), (long)
usage.totalTokens());
});
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public StreamResponse<ChatCompletionChunk> createStreaming(ChatCompletionCreateParams params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ChatCompletionChunk> createStreaming(
ChatCompletionCreateParams params, RequestOptions requestOptions) {
String model = params.model() != null ? params.model().toString() : null;
Span span = TracingUtils.createSpanBuilder(model, "chat").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
Long maxTokens = params.maxCompletionTokens().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
// Capture input messages in JSON format
String inputMessagesJson = formatInputMessages(params);
TracingUtils.setInputMessages(span, inputMessagesJson);
// For streaming, we can't easily extract usage info, so we'll just mark it as
// streaming
StreamResponse<ChatCompletionChunk> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
// Note: For streaming, the span will end immediately
// This is a simplified implementation - in production you might want to
// wrap the stream to collect the full response
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
// Delegate other methods without tracing (for now)
@Override
public ChatCompletion retrieve(String completionId) {
return delegate.retrieve(completionId);
}
@Override
public ChatCompletion retrieve(
String completionId, com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
return delegate.retrieve(completionId, params);
}
@Override
public ChatCompletion retrieve(String completionId, RequestOptions requestOptions) {
return delegate.retrieve(completionId, requestOptions);
}
@Override
public ChatCompletion retrieve(
String completionId,
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(completionId, params, requestOptions);
}
@Override
public ChatCompletion retrieve(com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
return delegate.retrieve(params);
}
@Override
public ChatCompletion retrieve(
com.openai.models.chat.completions.ChatCompletionRetrieveParams params, RequestOptions requestOptions) {
return delegate.retrieve(params, requestOptions);
}
@Override
public ChatCompletion update(
String completionId, com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
return delegate.update(completionId, params);
}
@Override
public ChatCompletion update(
String completionId,
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
RequestOptions requestOptions) {
return delegate.update(completionId, params, requestOptions);
}
@Override
public ChatCompletion update(com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
return delegate.update(params);
}
@Override
public ChatCompletion update(
com.openai.models.chat.completions.ChatCompletionUpdateParams params, RequestOptions requestOptions) {
return delegate.update(params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list() {
return delegate.list();
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(RequestOptions requestOptions) {
return delegate.list(requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(
com.openai.models.chat.completions.ChatCompletionListParams params) {
return delegate.list(params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionListPage list(
com.openai.models.chat.completions.ChatCompletionListParams params, RequestOptions requestOptions) {
return delegate.list(params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId) {
return delegate.delete(completionId);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
String completionId, RequestOptions requestOptions) {
return delegate.delete(completionId, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
String completionId, com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
return delegate.delete(completionId, params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
String completionId,
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
RequestOptions requestOptions) {
return delegate.delete(completionId, params, requestOptions);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
return delegate.delete(params);
}
@Override
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
com.openai.models.chat.completions.ChatCompletionDeleteParams params, RequestOptions requestOptions) {
return delegate.delete(params, requestOptions);
}
}
}
@@ -1,290 +0,0 @@
package com.langchain.smith.wrappers.openai;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.ClientOptions;
import java.util.function.Consumer;
/**
* Wrapped OpenAI client that maintains the same developer experience as the
* original client
* while adding LangSmith tracing capabilities.
*
* <p>
* This wrapper delegates all calls to the underlying OpenAI client, allowing
* all
* configuration options and methods to work exactly as they would with the
* original client.
*/
public class WrappedOpenAIClient implements OpenAIClient {
private final OpenAIClient delegate;
/**
* Creates a new wrapped client that delegates to the provided client.
*
* @param delegate the underlying OpenAI client to wrap
*/
public WrappedOpenAIClient(OpenAIClient delegate) {
if (delegate == null) {
throw new IllegalArgumentException("Delegate client cannot be null");
}
this.delegate = delegate;
}
/**
* Gets the underlying delegate client.
*
* @return the wrapped OpenAI client
*/
public OpenAIClient getDelegate() {
return delegate;
}
@Override
public com.openai.client.OpenAIClientAsync async() {
return delegate.async();
}
@Override
public OpenAIClient.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public OpenAIClient withOptions(Consumer<ClientOptions.Builder> options) {
return delegate.withOptions(options);
}
@Override
public com.openai.services.blocking.CompletionService completions() {
return delegate.completions();
}
@Override
public com.openai.services.blocking.ChatService chat() {
return new WrappedChatService(delegate.chat());
}
@Override
public com.openai.services.blocking.EmbeddingService embeddings() {
return delegate.embeddings();
}
@Override
public com.openai.services.blocking.FileService files() {
return delegate.files();
}
@Override
public com.openai.services.blocking.ImageService images() {
return delegate.images();
}
@Override
public com.openai.services.blocking.AudioService audio() {
return delegate.audio();
}
@Override
public com.openai.services.blocking.ModerationService moderations() {
return delegate.moderations();
}
@Override
public com.openai.services.blocking.ModelService models() {
return delegate.models();
}
@Override
public com.openai.services.blocking.FineTuningService fineTuning() {
return delegate.fineTuning();
}
@Override
public com.openai.services.blocking.GraderService graders() {
return delegate.graders();
}
@Override
public com.openai.services.blocking.VectorStoreService vectorStores() {
return delegate.vectorStores();
}
@Override
public com.openai.services.blocking.WebhookService webhooks() {
return delegate.webhooks();
}
@Override
public com.openai.services.blocking.BetaService beta() {
return delegate.beta();
}
@Override
public com.openai.services.blocking.BatchService batches() {
return delegate.batches();
}
@Override
public com.openai.services.blocking.UploadService uploads() {
return delegate.uploads();
}
@Override
public com.openai.services.blocking.ResponseService responses() {
return new WrappedResponseService(delegate.responses());
}
@Override
public com.openai.services.blocking.RealtimeService realtime() {
return delegate.realtime();
}
@Override
public com.openai.services.blocking.ConversationService conversations() {
return delegate.conversations();
}
@Override
public com.openai.services.blocking.EvalService evals() {
return delegate.evals();
}
@Override
public com.openai.services.blocking.ContainerService containers() {
return delegate.containers();
}
@Override
public com.openai.services.blocking.VideoService videos() {
return delegate.videos();
}
@Override
public void close() {
delegate.close();
}
/**
* Builder for creating wrapped OpenAI clients with the same configuration
* options
* as the original client builder.
*/
public static class Builder {
private final OpenAIOkHttpClient.Builder delegateBuilder;
/**
* Creates a new builder that wraps the OpenAI client builder.
*/
public Builder() {
this.delegateBuilder = OpenAIOkHttpClient.builder();
}
/**
* Creates a new builder that wraps the OpenAI client builder, starting from
* environment variables.
*
* @return this builder for method chaining
*/
public Builder fromEnv() {
delegateBuilder.fromEnv();
return this;
}
/**
* Sets the API key.
*
* @param apiKey the OpenAI API key
* @return this builder for method chaining
*/
public Builder apiKey(String apiKey) {
delegateBuilder.apiKey(apiKey);
return this;
}
/**
* Sets the organization ID.
*
* @param organization the organization ID
* @return this builder for method chaining
*/
public Builder organization(String organization) {
delegateBuilder.organization(organization);
return this;
}
/**
* Sets the project ID.
*
* @param project the project ID
* @return this builder for method chaining
*/
public Builder project(String project) {
delegateBuilder.project(project);
return this;
}
/**
* Sets the webhook secret.
*
* @param webhookSecret the webhook secret
* @return this builder for method chaining
*/
public Builder webhookSecret(String webhookSecret) {
delegateBuilder.webhookSecret(webhookSecret);
return this;
}
/**
* Sets the base URL.
*
* @param baseUrl the base URL
* @return this builder for method chaining
*/
public Builder baseUrl(String baseUrl) {
delegateBuilder.baseUrl(baseUrl);
return this;
}
/**
* Builds the wrapped OpenAI client.
*
* @return a new wrapped OpenAI client
*/
public WrappedOpenAIClient build() {
return new WrappedOpenAIClient(delegateBuilder.build());
}
}
/**
* Creates a new builder for constructing wrapped OpenAI clients.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Wraps an existing OpenAI client to add LangSmith tracing capabilities.
*
* <p>This is a convenience method equivalent to using the constructor directly.
*
* @param client the OpenAI client to wrap
* @return a wrapped client that delegates to the original client
* @throws IllegalArgumentException if client is null
*/
public static WrappedOpenAIClient wrap(OpenAIClient client) {
return new WrappedOpenAIClient(client);
}
/**
* Creates a wrapped OpenAI client from environment variables.
*
* @return a new wrapped OpenAI client configured from environment variables
*/
public static WrappedOpenAIClient fromEnv() {
return builder().fromEnv().build();
}
}
@@ -1,433 +0,0 @@
package com.langchain.smith.wrappers.openai;
import com.openai.core.ClientOptions;
import com.openai.core.RequestOptions;
import com.openai.core.http.StreamResponse;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStreamEvent;
import com.openai.models.responses.StructuredResponse;
import com.openai.models.responses.StructuredResponseCreateParams;
import com.openai.services.blocking.ResponseService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import java.util.function.Consumer;
/**
* Wrapped ResponseService that adds OpenTelemetry tracing to response operations.
*/
class WrappedResponseService implements ResponseService {
private final ResponseService delegate;
WrappedResponseService(ResponseService delegate) {
this.delegate = delegate;
}
@Override
public ResponseService.WithRawResponse withRawResponse() {
return delegate.withRawResponse();
}
@Override
public ResponseService withOptions(Consumer<ClientOptions.Builder> options) {
return new WrappedResponseService(delegate.withOptions(options));
}
@Override
public com.openai.services.blocking.responses.InputItemService inputItems() {
return delegate.inputItems();
}
@Override
public com.openai.services.blocking.responses.InputTokenService inputTokens() {
return delegate.inputTokens();
}
@Override
public Response create() {
return create((ResponseCreateParams) null, null);
}
@Override
public Response create(RequestOptions requestOptions) {
return create((ResponseCreateParams) null, requestOptions);
}
@Override
public Response create(ResponseCreateParams params) {
return create(params, null);
}
@Override
public Response create(ResponseCreateParams params, RequestOptions requestOptions) {
// Extract model from params
String model =
params != null && params.model().isPresent() ? params.model().toString() : null;
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
if (params != null) {
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
Response result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model (simplified - just use the request model)
TracingUtils.setResponseMetadata(span, model, null);
// Extract usage information from result
if (result.usage().isPresent()) {
com.openai.models.responses.ResponseUsage usage = result.usage().get();
TracingUtils.setResponseAttributes(
span, (long) usage.inputTokens(), (long) usage.outputTokens(), (long) usage.totalTokens());
}
if (result.status() != null) {
span.setAttribute("gen_ai.response.status", result.status().toString());
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params) {
return create(params, null);
}
@Override
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params, RequestOptions requestOptions) {
// Get model from rawParams
String model = params != null
&& params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: null;
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
// Set request parameters if available (handle Optional types)
if (params != null && params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StructuredResponse<T> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.create(params);
} else {
result = delegate.create(params, requestOptions);
}
// Extract response model (simplified - just use the request model)
TracingUtils.setResponseMetadata(span, model, null);
if (result.usage().isPresent()) {
com.openai.models.responses.ResponseUsage usage = result.usage().get();
TracingUtils.setResponseAttributes(
span, (long) usage.inputTokens(), (long) usage.outputTokens(), (long) usage.totalTokens());
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming() {
return createStreaming((ResponseCreateParams) null, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(RequestOptions requestOptions) {
return createStreaming((ResponseCreateParams) null, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(
ResponseCreateParams params, RequestOptions requestOptions) {
String model = params != null && params.model() != null ? params.model().toString() : null;
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
if (params != null) {
Double temperature = params.temperature().orElse(null);
Double topP = params.topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StreamResponse<ResponseStreamEvent> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
// Note: For streaming, the span will end immediately
span.end();
}
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(StructuredResponseCreateParams<?> params) {
return createStreaming(params, null);
}
@Override
public StreamResponse<ResponseStreamEvent> createStreaming(
StructuredResponseCreateParams<?> params, RequestOptions requestOptions) {
// Get model from rawParams
String model = params != null
&& params.rawParams() != null
&& params.rawParams().model() != null
? params.rawParams().model().toString()
: null;
Span span = TracingUtils.createSpanBuilder(model, "response").startSpan();
try (Scope scope = span.makeCurrent()) {
// Set experiment context attributes if present
setExperimentContextAttributes(span);
// Set request attributes (core attributes already set on builder)
TracingUtils.setRequestAttributes(span, model);
span.setAttribute("gen_ai.streaming", true);
// Set request parameters if available (handle Optional types)
if (params != null && params.rawParams() != null) {
Double temperature = params.rawParams().temperature().orElse(null);
Double topP = params.rawParams().topP().orElse(null);
TracingUtils.setRequestParameters(span, temperature, topP, null);
}
StreamResponse<ResponseStreamEvent> result;
// If requestOptions is null, use the single-parameter version
if (requestOptions == null) {
result = delegate.createStreaming(params);
} else {
result = delegate.createStreaming(params, requestOptions);
}
return result;
} catch (Exception e) {
TracingUtils.recordException(span, e);
throw e;
} finally {
span.end();
}
}
/**
* Sets experiment context attributes on the span if they are present.
* This includes reference example ID, session ID, and metadata.
*
* @param span the span to set attributes on
*/
private void setExperimentContextAttributes(Span span) {
// Set reference example ID if present
ExperimentContext.current()
.getReferenceExampleId()
.filter(id -> !id.isEmpty())
.ifPresent(id -> span.setAttribute("langsmith.reference_example_id", id));
// Set session ID (experiment ID) if present
// This is critical for linking runs to experiments in the dataset's Experiments tab
ExperimentContext.current()
.getSessionId()
.filter(id -> !id.isEmpty())
.ifPresent(id -> span.setAttribute("langsmith.trace.session_id", id));
// Set custom metadata
java.util.Map<String, String> metadata = ExperimentContext.current().getMetadata();
for (java.util.Map.Entry<String, String> entry : metadata.entrySet()) {
span.setAttribute("langsmith.metadata." + entry.getKey(), entry.getValue());
}
}
// Delegate other methods without tracing (for now)
@Override
public Response retrieve(String responseId) {
return delegate.retrieve(responseId);
}
@Override
public Response retrieve(String responseId, RequestOptions requestOptions) {
return delegate.retrieve(responseId, requestOptions);
}
@Override
public Response retrieve(String responseId, com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieve(responseId, params);
}
@Override
public Response retrieve(
String responseId,
com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieve(responseId, params, requestOptions);
}
@Override
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieve(params);
}
@Override
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params, RequestOptions requestOptions) {
return delegate.retrieve(params, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId) {
return delegate.retrieveStreaming(responseId);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId, RequestOptions requestOptions) {
return delegate.retrieveStreaming(responseId, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
String responseId, com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieveStreaming(responseId, params);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
String responseId,
com.openai.models.responses.ResponseRetrieveParams params,
RequestOptions requestOptions) {
return delegate.retrieveStreaming(responseId, params, requestOptions);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
com.openai.models.responses.ResponseRetrieveParams params) {
return delegate.retrieveStreaming(params);
}
@Override
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
com.openai.models.responses.ResponseRetrieveParams params, RequestOptions requestOptions) {
return delegate.retrieveStreaming(params, requestOptions);
}
@Override
public void delete(String responseId) {
delegate.delete(responseId);
}
@Override
public void delete(String responseId, RequestOptions requestOptions) {
delegate.delete(responseId, requestOptions);
}
@Override
public void delete(String responseId, com.openai.models.responses.ResponseDeleteParams params) {
delegate.delete(responseId, params);
}
@Override
public void delete(
String responseId, com.openai.models.responses.ResponseDeleteParams params, RequestOptions requestOptions) {
delegate.delete(responseId, params, requestOptions);
}
@Override
public void delete(com.openai.models.responses.ResponseDeleteParams params) {
delegate.delete(params);
}
@Override
public void delete(com.openai.models.responses.ResponseDeleteParams params, RequestOptions requestOptions) {
delegate.delete(params, requestOptions);
}
@Override
public Response cancel(String responseId) {
return delegate.cancel(responseId);
}
@Override
public Response cancel(String responseId, RequestOptions requestOptions) {
return delegate.cancel(responseId, requestOptions);
}
@Override
public Response cancel(String responseId, com.openai.models.responses.ResponseCancelParams params) {
return delegate.cancel(responseId, params);
}
@Override
public Response cancel(
String responseId, com.openai.models.responses.ResponseCancelParams params, RequestOptions requestOptions) {
return delegate.cancel(responseId, params, requestOptions);
}
@Override
public Response cancel(com.openai.models.responses.ResponseCancelParams params) {
return delegate.cancel(params);
}
@Override
public Response cancel(com.openai.models.responses.ResponseCancelParams params, RequestOptions requestOptions) {
return delegate.cancel(params, requestOptions);
}
}
@@ -0,0 +1,21 @@
package com.langchain.smith.client
import com.langchain.smith.models.info.InfoListResponse.BatchIngestConfig
import kotlin.jvm.optionals.getOrNull
internal data class AutoBatchIngestLimits(
val batchSizeLimit: Int = AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT,
val batchSizeLimitBytes: Int = AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT_BYTES,
val useMultipartEndpoint: Boolean = true,
)
internal fun BatchIngestConfig?.toAutoBatchIngestLimits(): AutoBatchIngestLimits =
AutoBatchIngestLimits(
batchSizeLimit =
this?.sizeLimit()?.getOrNull()?.takeIf { it > 0 && it <= Int.MAX_VALUE }?.toInt()
?: AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT,
batchSizeLimitBytes =
this?.sizeLimitBytes()?.getOrNull()?.takeIf { it > 0 && it <= Int.MAX_VALUE }?.toInt()
?: AutoBatchQueue.DEFAULT_BATCH_SIZE_LIMIT_BYTES,
useMultipartEndpoint = this?.useMultipartEndpoint()?.getOrNull() ?: true,
)
@@ -0,0 +1,519 @@
package com.langchain.smith.client
import com.fasterxml.jackson.databind.node.ObjectNode
import com.langchain.smith.core.RequestOptions
import com.langchain.smith.core.Timeout
import com.langchain.smith.core.http.Headers
import com.langchain.smith.core.http.QueryParams
import com.langchain.smith.core.jsonMapper
import com.langchain.smith.models.runs.RunIngest
import com.langchain.smith.models.runs.RunIngestBatchParams
import java.util.concurrent.CompletionException
import java.util.concurrent.CompletionStage
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Phaser
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.jvm.optionals.getOrNull
import org.slf4j.LoggerFactory
/**
* Batches run create/update operations and sends them to LangSmith in a single `ingestBatch`
* request, reducing HTTP overhead.
*
* Operations are buffered and flushed either:
* - When the buffer reaches [batchSizeLimit] operations
* - After [aggregationDelayMs] milliseconds of inactivity (timer-based drain)
* - When [flush] is called explicitly
*
* @param sendBatch sends a batch and completes when the send has finished
* @param batchSizeLimit max operations before auto-flush (default 100)
* @param aggregationDelayMs inactivity delay before timer-based flush (default 250ms, matching JS
* auto-batch aggregation and Python's initial queue wait)
* @param sendParallelism max number of batch requests to send concurrently (default 4)
* @param maxAggregationDelayMs max delay before timer-based flush after first item in a burst
* (default 500ms, matching Python's background flush interval)
* @param batchSizeLimitBytes max serialized JSON body size per batch (default 20 MiB)
*/
class AutoBatchQueue(
private val sendBatch: (RunIngestBatchParams, RequestOptions) -> CompletionStage<Void?>,
private val batchSizeLimit: Int = DEFAULT_BATCH_SIZE_LIMIT,
private val aggregationDelayMs: Long = DEFAULT_AGGREGATION_DELAY_MS,
private val sendParallelism: Int = DEFAULT_SEND_PARALLELISM,
private val maxAggregationDelayMs: Long = DEFAULT_MAX_AGGREGATION_DELAY_MS,
private val batchSizeLimitBytes: Int,
) {
private val items = ConcurrentLinkedQueue<BatchItem>()
private val queuedCount = AtomicInteger(0)
private val shutdown = AtomicBoolean(false)
private val enqueueShutdownLock = ReentrantLock()
private val delayedFlushLock = ReentrantLock()
private var delayedFlushFuture: ScheduledFuture<*>? = null
private var firstQueuedAtNanos: Long? = null
private val activeSends =
object : Phaser(0) {
override fun onAdvance(phase: Int, registeredParties: Int): Boolean = false
}
private val coordinator: ScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor { r ->
Thread(r, "langsmith-batch-coordinator").apply { isDaemon = true }
}
private val sendExecutor: ExecutorService =
Executors.newFixedThreadPool(sendParallelism) { r ->
Thread(r, "langsmith-batch-sender").apply { isDaemon = true }
}
/** Enqueues a run create operation. */
fun post(
run: RunIngest,
headers: Headers = Headers.builder().build(),
queryParams: QueryParams = QueryParams.builder().build(),
requestOptions: RequestOptions = RequestOptions.none(),
) {
enqueue(BatchOp.Post, run, headers, queryParams, requestOptions)
}
/** Enqueues a run update (patch) operation. */
fun patch(
run: RunIngest,
headers: Headers = Headers.builder().build(),
queryParams: QueryParams = QueryParams.builder().build(),
requestOptions: RequestOptions = RequestOptions.none(),
) {
enqueue(BatchOp.Patch, run, headers, queryParams, requestOptions)
}
/**
* Flushes all queued operations immediately, blocking until batch requests that were queued or
* already in-flight have completed.
*
* Safe to call from any thread. No-op if the queue is empty.
*/
fun flush() {
cancelDelayedFlush()
while (true) {
if (queuedCount.get() > 0 && !drainOnCoordinator()) {
return
}
if (!waitForActiveSends()) {
return
}
if (queuedCount.get() == 0 && !hasActiveSends()) {
return
}
}
}
/**
* Flushes remaining operations and shuts down the background executors.
*
* After calling this, the queue will no longer accept new operations.
*/
fun shutdown() {
val startedShutdown =
enqueueShutdownLock.withLock {
// Serialize with enqueue's check-and-add so flush cannot miss an item that observed
// shutdown=false but has not yet been queued.
shutdown.compareAndSet(false, true)
}
if (!startedShutdown) return
flush()
coordinator.shutdown()
sendExecutor.shutdown()
try {
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
coordinator.shutdownNow()
}
if (!sendExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
sendExecutor.shutdownNow()
}
} catch (_: InterruptedException) {
coordinator.shutdownNow()
sendExecutor.shutdownNow()
Thread.currentThread().interrupt()
}
}
constructor(
sendBatch: (RunIngestBatchParams, RequestOptions) -> CompletionStage<Void?>,
batchSizeLimit: Int = DEFAULT_BATCH_SIZE_LIMIT,
aggregationDelayMs: Long = DEFAULT_AGGREGATION_DELAY_MS,
sendParallelism: Int = DEFAULT_SEND_PARALLELISM,
) : this(
sendBatch = sendBatch,
batchSizeLimit = batchSizeLimit,
aggregationDelayMs = aggregationDelayMs,
sendParallelism = sendParallelism,
maxAggregationDelayMs = DEFAULT_MAX_AGGREGATION_DELAY_MS,
batchSizeLimitBytes = DEFAULT_BATCH_SIZE_LIMIT_BYTES,
)
/** Returns the number of queued operations (for testing). */
internal fun size(): Int = queuedCount.get()
private fun enqueue(
op: BatchOp,
run: RunIngest,
headers: Headers,
queryParams: QueryParams,
requestOptions: RequestOptions,
) {
val count =
enqueueShutdownLock.withLock {
check(!shutdown.get()) { "AutoBatchQueue is shut down" }
items.add(
BatchItem(
op = op,
run = run,
headers = headers,
queryParams = queryParams,
requestOptions = requestOptions,
)
)
queuedCount.incrementAndGet()
}
afterEnqueue(count)
}
private fun afterEnqueue(count: Int) {
if (count >= batchSizeLimit) {
triggerFlush()
} else {
scheduleFlush()
}
}
private fun scheduleFlush() {
delayedFlushLock.withLock {
try {
val now = System.nanoTime()
val firstQueuedAt = firstQueuedAtNanos ?: now.also { firstQueuedAtNanos = it }
val elapsedMs = TimeUnit.NANOSECONDS.toMillis(now - firstQueuedAt)
val remainingMaxDelayMs = (maxAggregationDelayMs - elapsedMs).coerceAtLeast(0)
val delayMs = minOf(aggregationDelayMs, remainingMaxDelayMs)
delayedFlushFuture?.cancel(false)
delayedFlushFuture =
coordinator.schedule(
{
clearDelayedFlushState()
drainAndSubmitSends()
},
delayMs,
TimeUnit.MILLISECONDS,
)
} catch (e: RejectedExecutionException) {
delayedFlushFuture = null
firstQueuedAtNanos = null
logger.warn("Batch queue coordinator rejected delayed flush", e)
}
}
}
private fun clearDelayedFlushState() {
delayedFlushLock.withLock {
delayedFlushFuture = null
firstQueuedAtNanos = null
}
}
private fun triggerFlush() {
cancelDelayedFlush()
try {
coordinator.execute { drainAndSubmitSends() }
} catch (e: RejectedExecutionException) {
logger.warn("Batch queue coordinator rejected flush", e)
}
}
private fun cancelDelayedFlush() {
delayedFlushLock.withLock {
delayedFlushFuture?.cancel(false)
delayedFlushFuture = null
firstQueuedAtNanos = null
}
}
private fun drainOnCoordinator(): Boolean {
val drainFuture =
try {
coordinator.submit { drainAndSubmitSends() }
} catch (e: RejectedExecutionException) {
throw IllegalStateException("Batch queue coordinator rejected flush", e)
}
try {
drainFuture.get()
return true
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
return false
} catch (e: ExecutionException) {
throw RuntimeException("Failed to flush batch queue", e.cause)
}
}
private fun drainAndSubmitSends() {
var remainingToDrain = queuedCount.get()
while (remainingToDrain > 0) {
val drainResult = drainUpTo(minOf(batchSizeLimit, remainingToDrain))
if (drainResult.itemCount == 0) break
drainResult.batches.forEach(::submitBatch)
remainingToDrain -= drainResult.itemCount
}
when {
queuedCount.get() >= batchSizeLimit -> triggerFlush()
queuedCount.get() > 0 && !shutdown.get() -> scheduleFlush()
queuedCount.get() > 0 && shutdown.get() -> triggerFlush()
}
}
/**
* Drains up to [maxItems] queued operations and returns batch params grouped by request
* options.
*
* Multipart ingest compression is applied at send time; legacy `/runs/batch` remains
* uncompressed.
*
* TODO: Support attachment parts in multipart ingest.
*/
private fun drainUpTo(maxItems: Int): DrainResult {
val openGroups = linkedMapOf<RequestOptionsKey, BatchGroup>()
var drained = 0
val batches = buildList {
while (drained < maxItems) {
val item = items.poll() ?: break
queuedCount.decrementAndGet()
drained++
val key = item.requestOptions.key()
val itemSerializedSize = objectMapper.writeValueAsBytes(item.run).size
var group = openGroups.getOrPut(key) { BatchGroup(item.requestOptions) }
if (
group.isNotEmpty() &&
group.serializedSizeWith(item, itemSerializedSize) > batchSizeLimitBytes
) {
add(group.toBatch())
group = BatchGroup(item.requestOptions)
openGroups[key] = group
}
group.add(item, itemSerializedSize)
}
addAll(openGroups.values.map { it.toBatch() })
}
return DrainResult(batches = batches, itemCount = drained)
}
private fun submitBatch(batch: Batch) {
activeSends.register()
try {
sendExecutor.execute {
try {
sendBatch(batch.params, batch.requestOptions).toCompletableFuture().join()
} catch (e: CompletionException) {
logger.warn("Failed to send batch of runs", e.cause ?: e)
} catch (e: Exception) {
logger.warn("Failed to send batch of runs", e)
} finally {
activeSends.arriveAndDeregister()
}
}
} catch (e: RejectedExecutionException) {
activeSends.arriveAndDeregister()
logger.warn(
"Batch queue sender rejected a batch; dropping {} run operations",
operationCount(batch.params),
e,
)
}
}
private fun waitForActiveSends(): Boolean {
while (hasActiveSends()) {
val phase = activeSends.phase
try {
activeSends.awaitAdvanceInterruptibly(phase)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
return false
}
}
return true
}
private fun hasActiveSends(): Boolean = activeSends.registeredParties > 0
private fun operationCount(params: RunIngestBatchParams): Int =
params.post().orElse(emptyList()).size + params.patch().orElse(emptyList()).size
private enum class BatchOp {
Post,
Patch,
}
private data class DrainResult(val batches: List<Batch>, val itemCount: Int)
private data class Batch(val params: RunIngestBatchParams, val requestOptions: RequestOptions)
private class BatchGroup(
val requestOptions: RequestOptions,
val posts: MutableList<RunIngest> = mutableListOf(),
val patches: MutableList<RunIngest> = mutableListOf(),
val headers: Headers.Builder = Headers.builder(),
val queryParams: QueryParams.Builder = QueryParams.builder(),
) {
private var postSerializedSizeBytes = 0
private var patchSerializedSizeBytes = 0
fun add(item: BatchItem, serializedSizeBytes: Int) {
when (item.op) {
BatchOp.Post -> {
posts.add(item.run)
postSerializedSizeBytes += serializedSizeBytes
}
BatchOp.Patch -> {
patches.add(item.run)
patchSerializedSizeBytes += serializedSizeBytes
}
}
headers.putAll(item.headers)
queryParams.putAll(item.queryParams)
}
fun isNotEmpty(): Boolean = posts.isNotEmpty() || patches.isNotEmpty()
fun serializedSizeWith(item: BatchItem, itemSerializedSizeBytes: Int): Int =
estimatedBodySizeBytes(
postCount = posts.size + if (item.op == BatchOp.Post) 1 else 0,
postItemsSizeBytes =
postSerializedSizeBytes +
if (item.op == BatchOp.Post) itemSerializedSizeBytes else 0,
patchCount = patches.size + if (item.op == BatchOp.Patch) 1 else 0,
patchItemsSizeBytes =
patchSerializedSizeBytes +
if (item.op == BatchOp.Patch) itemSerializedSizeBytes else 0,
)
private fun estimatedBodySizeBytes(
postCount: Int,
postItemsSizeBytes: Int,
patchCount: Int,
patchItemsSizeBytes: Int,
): Int {
var size = 2 // Opening and closing braces.
var fieldCount = 0
if (patchCount > 0) {
size += fieldSizeBytes("patch", patchCount, patchItemsSizeBytes)
fieldCount++
}
if (postCount > 0) {
if (fieldCount > 0) size++ // Comma between top-level fields.
size += fieldSizeBytes("post", postCount, postItemsSizeBytes)
}
return size
}
private fun fieldSizeBytes(name: String, count: Int, itemsSizeBytes: Int): Int =
name.length + 5 + itemsSizeBytes + (count - 1) // `"name":[]` + item commas.
fun toBatch(): Batch {
val mergeResult = mergePostsAndPatches()
val builder = RunIngestBatchParams.builder()
if (mergeResult.posts.isNotEmpty()) builder.post(mergeResult.posts)
if (mergeResult.patches.isNotEmpty()) builder.patch(mergeResult.patches)
builder.additionalHeaders(headers.build())
builder.additionalQueryParams(queryParams.build())
return Batch(params = builder.build(), requestOptions = requestOptions)
}
private fun mergePostsAndPatches(): MergeResult {
if (posts.isEmpty() || patches.isEmpty()) {
return MergeResult(posts = posts, patches = patches)
}
val postsById =
posts.mapNotNull { post -> post.id().getOrNull()?.let { it to post } }.toMap()
val postsWithoutId = posts.filter { it.id().getOrNull() == null }
val patchesByPostId =
patches.mapNotNull { patch ->
patch.id().getOrNull()?.takeIf(postsById::containsKey)?.let { it to patch }
}
val patchesByPostIdMap = patchesByPostId.toMap()
val standalonePatches =
patches.filter { patch ->
patch.id().getOrNull()?.let(postsById::containsKey) != true
}
return MergeResult(
posts =
postsWithoutId +
postsById.map { (id, post) ->
patchesByPostIdMap[id]?.let { mergePostAndPatch(post, it) } ?: post
},
patches = standalonePatches,
mergedRunIds = patchesByPostId.map { it.first },
)
}
private fun mergePostAndPatch(post: RunIngest, patch: RunIngest): RunIngest {
val merged = objectMapper.valueToTree<ObjectNode>(post)
val patchFields = objectMapper.valueToTree<ObjectNode>(patch)
patchFields.fields().forEach { (field, value) -> merged.set<ObjectNode>(field, value) }
val mergedRun = objectMapper.treeToValue(merged, RunIngest::class.java)
return mergedRun
.toBuilder()
.attachments(post.attachments() + patch.attachments())
.build()
}
}
private data class MergeResult(
val posts: List<RunIngest>,
val patches: List<RunIngest>,
val mergedRunIds: List<String> = emptyList(),
)
private data class BatchItem(
val op: BatchOp,
val run: RunIngest,
val headers: Headers,
val queryParams: QueryParams,
val requestOptions: RequestOptions,
)
private data class RequestOptionsKey(val responseValidation: Boolean?, val timeout: Timeout?)
private fun RequestOptions.key(): RequestOptionsKey =
RequestOptionsKey(responseValidation = responseValidation, timeout = timeout)
companion object {
private val logger = LoggerFactory.getLogger(AutoBatchQueue::class.java)
private val objectMapper = jsonMapper()
const val DEFAULT_BATCH_SIZE_LIMIT = 100
const val DEFAULT_BATCH_SIZE_LIMIT_BYTES = 20_971_520
const val DEFAULT_AGGREGATION_DELAY_MS = 250L
const val DEFAULT_MAX_AGGREGATION_DELAY_MS = 500L
const val DEFAULT_SEND_PARALLELISM = 4
}
}
@@ -6,13 +6,21 @@ import com.langchain.smith.core.ClientOptions
import com.langchain.smith.services.blocking.AnnotationQueueService
import com.langchain.smith.services.blocking.CommitService
import com.langchain.smith.services.blocking.DatasetService
import com.langchain.smith.services.blocking.EvaluatorService
import com.langchain.smith.services.blocking.ExampleService
import com.langchain.smith.services.blocking.FeedbackService
import com.langchain.smith.services.blocking.InfoService
import com.langchain.smith.services.blocking.IssueService
import com.langchain.smith.services.blocking.OnlineEvaluatorService
import com.langchain.smith.services.blocking.PublicService
import com.langchain.smith.services.blocking.RepoService
import com.langchain.smith.services.blocking.RunService
import com.langchain.smith.services.blocking.SandboxService
import com.langchain.smith.services.blocking.SessionService
import com.langchain.smith.services.blocking.SettingService
import com.langchain.smith.services.blocking.ThreadService
import com.langchain.smith.services.blocking.TraceService
import com.langchain.smith.services.blocking.WorkspaceService
import java.util.function.Consumer
/**
@@ -59,18 +67,34 @@ interface LangsmithClient {
fun runs(): RunService
fun threads(): ThreadService
fun traces(): TraceService
fun evaluators(): EvaluatorService
fun onlineEvaluators(): OnlineEvaluatorService
fun feedback(): FeedbackService
fun public_(): PublicService
fun annotationQueues(): AnnotationQueueService
fun info(): InfoService
fun workspaces(): WorkspaceService
fun repos(): RepoService
fun commits(): CommitService
fun settings(): SettingService
fun issues(): IssueService
fun sandboxes(): SandboxService
/**
* Closes this client, relinquishing any underlying resources.
*
@@ -102,16 +126,32 @@ interface LangsmithClient {
fun runs(): RunService.WithRawResponse
fun threads(): ThreadService.WithRawResponse
fun traces(): TraceService.WithRawResponse
fun evaluators(): EvaluatorService.WithRawResponse
fun onlineEvaluators(): OnlineEvaluatorService.WithRawResponse
fun feedback(): FeedbackService.WithRawResponse
fun public_(): PublicService.WithRawResponse
fun annotationQueues(): AnnotationQueueService.WithRawResponse
fun info(): InfoService.WithRawResponse
fun workspaces(): WorkspaceService.WithRawResponse
fun repos(): RepoService.WithRawResponse
fun commits(): CommitService.WithRawResponse
fun settings(): SettingService.WithRawResponse
fun issues(): IssueService.WithRawResponse
fun sandboxes(): SandboxService.WithRawResponse
}
}
@@ -6,13 +6,21 @@ import com.langchain.smith.core.ClientOptions
import com.langchain.smith.services.async.AnnotationQueueServiceAsync
import com.langchain.smith.services.async.CommitServiceAsync
import com.langchain.smith.services.async.DatasetServiceAsync
import com.langchain.smith.services.async.EvaluatorServiceAsync
import com.langchain.smith.services.async.ExampleServiceAsync
import com.langchain.smith.services.async.FeedbackServiceAsync
import com.langchain.smith.services.async.InfoServiceAsync
import com.langchain.smith.services.async.IssueServiceAsync
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsync
import com.langchain.smith.services.async.PublicServiceAsync
import com.langchain.smith.services.async.RepoServiceAsync
import com.langchain.smith.services.async.RunServiceAsync
import com.langchain.smith.services.async.SandboxServiceAsync
import com.langchain.smith.services.async.SessionServiceAsync
import com.langchain.smith.services.async.SettingServiceAsync
import com.langchain.smith.services.async.ThreadServiceAsync
import com.langchain.smith.services.async.TraceServiceAsync
import com.langchain.smith.services.async.WorkspaceServiceAsync
import java.util.function.Consumer
/**
@@ -59,18 +67,34 @@ interface LangsmithClientAsync {
fun runs(): RunServiceAsync
fun threads(): ThreadServiceAsync
fun traces(): TraceServiceAsync
fun evaluators(): EvaluatorServiceAsync
fun onlineEvaluators(): OnlineEvaluatorServiceAsync
fun feedback(): FeedbackServiceAsync
fun public_(): PublicServiceAsync
fun annotationQueues(): AnnotationQueueServiceAsync
fun info(): InfoServiceAsync
fun workspaces(): WorkspaceServiceAsync
fun repos(): RepoServiceAsync
fun commits(): CommitServiceAsync
fun settings(): SettingServiceAsync
fun issues(): IssueServiceAsync
fun sandboxes(): SandboxServiceAsync
/**
* Closes this client, relinquishing any underlying resources.
*
@@ -106,16 +130,32 @@ interface LangsmithClientAsync {
fun runs(): RunServiceAsync.WithRawResponse
fun threads(): ThreadServiceAsync.WithRawResponse
fun traces(): TraceServiceAsync.WithRawResponse
fun evaluators(): EvaluatorServiceAsync.WithRawResponse
fun onlineEvaluators(): OnlineEvaluatorServiceAsync.WithRawResponse
fun feedback(): FeedbackServiceAsync.WithRawResponse
fun public_(): PublicServiceAsync.WithRawResponse
fun annotationQueues(): AnnotationQueueServiceAsync.WithRawResponse
fun info(): InfoServiceAsync.WithRawResponse
fun workspaces(): WorkspaceServiceAsync.WithRawResponse
fun repos(): RepoServiceAsync.WithRawResponse
fun commits(): CommitServiceAsync.WithRawResponse
fun settings(): SettingServiceAsync.WithRawResponse
fun issues(): IssueServiceAsync.WithRawResponse
fun sandboxes(): SandboxServiceAsync.WithRawResponse
}
}
@@ -10,20 +10,36 @@ import com.langchain.smith.services.async.CommitServiceAsync
import com.langchain.smith.services.async.CommitServiceAsyncImpl
import com.langchain.smith.services.async.DatasetServiceAsync
import com.langchain.smith.services.async.DatasetServiceAsyncImpl
import com.langchain.smith.services.async.EvaluatorServiceAsync
import com.langchain.smith.services.async.EvaluatorServiceAsyncImpl
import com.langchain.smith.services.async.ExampleServiceAsync
import com.langchain.smith.services.async.ExampleServiceAsyncImpl
import com.langchain.smith.services.async.FeedbackServiceAsync
import com.langchain.smith.services.async.FeedbackServiceAsyncImpl
import com.langchain.smith.services.async.InfoServiceAsync
import com.langchain.smith.services.async.InfoServiceAsyncImpl
import com.langchain.smith.services.async.IssueServiceAsync
import com.langchain.smith.services.async.IssueServiceAsyncImpl
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsync
import com.langchain.smith.services.async.OnlineEvaluatorServiceAsyncImpl
import com.langchain.smith.services.async.PublicServiceAsync
import com.langchain.smith.services.async.PublicServiceAsyncImpl
import com.langchain.smith.services.async.RepoServiceAsync
import com.langchain.smith.services.async.RepoServiceAsyncImpl
import com.langchain.smith.services.async.RunServiceAsync
import com.langchain.smith.services.async.RunServiceAsyncImpl
import com.langchain.smith.services.async.SandboxServiceAsync
import com.langchain.smith.services.async.SandboxServiceAsyncImpl
import com.langchain.smith.services.async.SessionServiceAsync
import com.langchain.smith.services.async.SessionServiceAsyncImpl
import com.langchain.smith.services.async.SettingServiceAsync
import com.langchain.smith.services.async.SettingServiceAsyncImpl
import com.langchain.smith.services.async.ThreadServiceAsync
import com.langchain.smith.services.async.ThreadServiceAsyncImpl
import com.langchain.smith.services.async.TraceServiceAsync
import com.langchain.smith.services.async.TraceServiceAsyncImpl
import com.langchain.smith.services.async.WorkspaceServiceAsync
import com.langchain.smith.services.async.WorkspaceServiceAsyncImpl
import java.util.function.Consumer
class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : LangsmithClientAsync {
@@ -33,11 +49,11 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
else
clientOptions
.toBuilder()
.putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
.putHeader("User-Agent", "langsmith-java/${getPackageVersion()}")
.build()
// Pass the original clientOptions so that this client sets its own User-Agent.
private val sync: LangsmithClient by lazy { LangsmithClientImpl(clientOptions) }
private val sync = lazy { LangsmithClientImpl(clientOptions) }
private val withRawResponse: LangsmithClientAsync.WithRawResponse by lazy {
WithRawResponseImpl(clientOptions)
@@ -55,7 +71,23 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
DatasetServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val runs: RunServiceAsync by lazy { RunServiceAsyncImpl(clientOptionsWithUserAgent) }
private val runs = lazy { RunServiceAsyncImpl(clientOptionsWithUserAgent) }
private val threads: ThreadServiceAsync by lazy {
ThreadServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val traces: TraceServiceAsync by lazy {
TraceServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val evaluators: EvaluatorServiceAsync by lazy {
EvaluatorServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val onlineEvaluators: OnlineEvaluatorServiceAsync by lazy {
OnlineEvaluatorServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val feedback: FeedbackServiceAsync by lazy {
FeedbackServiceAsyncImpl(clientOptionsWithUserAgent)
@@ -69,6 +101,12 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
AnnotationQueueServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val info: InfoServiceAsync by lazy { InfoServiceAsyncImpl(clientOptionsWithUserAgent) }
private val workspaces: WorkspaceServiceAsync by lazy {
WorkspaceServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val repos: RepoServiceAsync by lazy { RepoServiceAsyncImpl(clientOptionsWithUserAgent) }
private val commits: CommitServiceAsync by lazy {
@@ -79,7 +117,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
SettingServiceAsyncImpl(clientOptionsWithUserAgent)
}
override fun sync(): LangsmithClient = sync
private val issues: IssueServiceAsync by lazy {
IssueServiceAsyncImpl(clientOptionsWithUserAgent)
}
private val sandboxes: SandboxServiceAsync by lazy {
SandboxServiceAsyncImpl(clientOptionsWithUserAgent)
}
override fun sync(): LangsmithClient = sync.value
override fun withRawResponse(): LangsmithClientAsync.WithRawResponse = withRawResponse
@@ -92,7 +138,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
override fun datasets(): DatasetServiceAsync = datasets
override fun runs(): RunServiceAsync = runs
override fun runs(): RunServiceAsync = runs.value
override fun threads(): ThreadServiceAsync = threads
override fun traces(): TraceServiceAsync = traces
override fun evaluators(): EvaluatorServiceAsync = evaluators
override fun onlineEvaluators(): OnlineEvaluatorServiceAsync = onlineEvaluators
override fun feedback(): FeedbackServiceAsync = feedback
@@ -100,13 +154,30 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
override fun annotationQueues(): AnnotationQueueServiceAsync = annotationQueues
override fun info(): InfoServiceAsync = info
override fun workspaces(): WorkspaceServiceAsync = workspaces
override fun repos(): RepoServiceAsync = repos
override fun commits(): CommitServiceAsync = commits
override fun settings(): SettingServiceAsync = settings
override fun close() = clientOptions.close()
override fun issues(): IssueServiceAsync = issues
override fun sandboxes(): SandboxServiceAsync = sandboxes
override fun close() {
if (runs.isInitialized()) {
runs.value.shutdown()
}
if (sync.isInitialized()) {
sync.value.close()
} else {
clientOptions.close()
}
}
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
LangsmithClientAsync.WithRawResponse {
@@ -127,6 +198,22 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
RunServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val threads: ThreadServiceAsync.WithRawResponse by lazy {
ThreadServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val traces: TraceServiceAsync.WithRawResponse by lazy {
TraceServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val evaluators: EvaluatorServiceAsync.WithRawResponse by lazy {
EvaluatorServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val onlineEvaluators: OnlineEvaluatorServiceAsync.WithRawResponse by lazy {
OnlineEvaluatorServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val feedback: FeedbackServiceAsync.WithRawResponse by lazy {
FeedbackServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
@@ -139,6 +226,14 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
AnnotationQueueServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val info: InfoServiceAsync.WithRawResponse by lazy {
InfoServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val workspaces: WorkspaceServiceAsync.WithRawResponse by lazy {
WorkspaceServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val repos: RepoServiceAsync.WithRawResponse by lazy {
RepoServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
@@ -151,6 +246,14 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
SettingServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val issues: IssueServiceAsync.WithRawResponse by lazy {
IssueServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
private val sandboxes: SandboxServiceAsync.WithRawResponse by lazy {
SandboxServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
override fun withOptions(
modifier: Consumer<ClientOptions.Builder>
): LangsmithClientAsync.WithRawResponse =
@@ -166,6 +269,15 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
override fun runs(): RunServiceAsync.WithRawResponse = runs
override fun threads(): ThreadServiceAsync.WithRawResponse = threads
override fun traces(): TraceServiceAsync.WithRawResponse = traces
override fun evaluators(): EvaluatorServiceAsync.WithRawResponse = evaluators
override fun onlineEvaluators(): OnlineEvaluatorServiceAsync.WithRawResponse =
onlineEvaluators
override fun feedback(): FeedbackServiceAsync.WithRawResponse = feedback
override fun public_(): PublicServiceAsync.WithRawResponse = public_
@@ -173,10 +285,18 @@ class LangsmithClientAsyncImpl(private val clientOptions: ClientOptions) : Langs
override fun annotationQueues(): AnnotationQueueServiceAsync.WithRawResponse =
annotationQueues
override fun info(): InfoServiceAsync.WithRawResponse = info
override fun workspaces(): WorkspaceServiceAsync.WithRawResponse = workspaces
override fun repos(): RepoServiceAsync.WithRawResponse = repos
override fun commits(): CommitServiceAsync.WithRawResponse = commits
override fun settings(): SettingServiceAsync.WithRawResponse = settings
override fun issues(): IssueServiceAsync.WithRawResponse = issues
override fun sandboxes(): SandboxServiceAsync.WithRawResponse = sandboxes
}
}
@@ -10,20 +10,36 @@ import com.langchain.smith.services.blocking.CommitService
import com.langchain.smith.services.blocking.CommitServiceImpl
import com.langchain.smith.services.blocking.DatasetService
import com.langchain.smith.services.blocking.DatasetServiceImpl
import com.langchain.smith.services.blocking.EvaluatorService
import com.langchain.smith.services.blocking.EvaluatorServiceImpl
import com.langchain.smith.services.blocking.ExampleService
import com.langchain.smith.services.blocking.ExampleServiceImpl
import com.langchain.smith.services.blocking.FeedbackService
import com.langchain.smith.services.blocking.FeedbackServiceImpl
import com.langchain.smith.services.blocking.InfoService
import com.langchain.smith.services.blocking.InfoServiceImpl
import com.langchain.smith.services.blocking.IssueService
import com.langchain.smith.services.blocking.IssueServiceImpl
import com.langchain.smith.services.blocking.OnlineEvaluatorService
import com.langchain.smith.services.blocking.OnlineEvaluatorServiceImpl
import com.langchain.smith.services.blocking.PublicService
import com.langchain.smith.services.blocking.PublicServiceImpl
import com.langchain.smith.services.blocking.RepoService
import com.langchain.smith.services.blocking.RepoServiceImpl
import com.langchain.smith.services.blocking.RunService
import com.langchain.smith.services.blocking.RunServiceImpl
import com.langchain.smith.services.blocking.SandboxService
import com.langchain.smith.services.blocking.SandboxServiceImpl
import com.langchain.smith.services.blocking.SessionService
import com.langchain.smith.services.blocking.SessionServiceImpl
import com.langchain.smith.services.blocking.SettingService
import com.langchain.smith.services.blocking.SettingServiceImpl
import com.langchain.smith.services.blocking.ThreadService
import com.langchain.smith.services.blocking.ThreadServiceImpl
import com.langchain.smith.services.blocking.TraceService
import com.langchain.smith.services.blocking.TraceServiceImpl
import com.langchain.smith.services.blocking.WorkspaceService
import com.langchain.smith.services.blocking.WorkspaceServiceImpl
import java.util.function.Consumer
class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithClient {
@@ -33,7 +49,7 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
else
clientOptions
.toBuilder()
.putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
.putHeader("User-Agent", "langsmith-java/${getPackageVersion()}")
.build()
// Pass the original clientOptions so that this client sets its own User-Agent.
@@ -49,7 +65,19 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
private val datasets: DatasetService by lazy { DatasetServiceImpl(clientOptionsWithUserAgent) }
private val runs: RunService by lazy { RunServiceImpl(clientOptionsWithUserAgent) }
private val runs = lazy { RunServiceImpl(clientOptionsWithUserAgent) }
private val threads: ThreadService by lazy { ThreadServiceImpl(clientOptionsWithUserAgent) }
private val traces: TraceService by lazy { TraceServiceImpl(clientOptionsWithUserAgent) }
private val evaluators: EvaluatorService by lazy {
EvaluatorServiceImpl(clientOptionsWithUserAgent)
}
private val onlineEvaluators: OnlineEvaluatorService by lazy {
OnlineEvaluatorServiceImpl(clientOptionsWithUserAgent)
}
private val feedback: FeedbackService by lazy {
FeedbackServiceImpl(clientOptionsWithUserAgent)
@@ -61,12 +89,22 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
AnnotationQueueServiceImpl(clientOptionsWithUserAgent)
}
private val info: InfoService by lazy { InfoServiceImpl(clientOptionsWithUserAgent) }
private val workspaces: WorkspaceService by lazy {
WorkspaceServiceImpl(clientOptionsWithUserAgent)
}
private val repos: RepoService by lazy { RepoServiceImpl(clientOptionsWithUserAgent) }
private val commits: CommitService by lazy { CommitServiceImpl(clientOptionsWithUserAgent) }
private val settings: SettingService by lazy { SettingServiceImpl(clientOptionsWithUserAgent) }
private val issues: IssueService by lazy { IssueServiceImpl(clientOptionsWithUserAgent) }
private val sandboxes: SandboxService by lazy { SandboxServiceImpl(clientOptionsWithUserAgent) }
override fun async(): LangsmithClientAsync = async
override fun withRawResponse(): LangsmithClient.WithRawResponse = withRawResponse
@@ -80,7 +118,15 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
override fun datasets(): DatasetService = datasets
override fun runs(): RunService = runs
override fun runs(): RunService = runs.value
override fun threads(): ThreadService = threads
override fun traces(): TraceService = traces
override fun evaluators(): EvaluatorService = evaluators
override fun onlineEvaluators(): OnlineEvaluatorService = onlineEvaluators
override fun feedback(): FeedbackService = feedback
@@ -88,13 +134,26 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
override fun annotationQueues(): AnnotationQueueService = annotationQueues
override fun info(): InfoService = info
override fun workspaces(): WorkspaceService = workspaces
override fun repos(): RepoService = repos
override fun commits(): CommitService = commits
override fun settings(): SettingService = settings
override fun close() = clientOptions.close()
override fun issues(): IssueService = issues
override fun sandboxes(): SandboxService = sandboxes
override fun close() {
if (runs.isInitialized()) {
runs.value.shutdown()
}
clientOptions.close()
}
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
LangsmithClient.WithRawResponse {
@@ -115,6 +174,22 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
RunServiceImpl.WithRawResponseImpl(clientOptions)
}
private val threads: ThreadService.WithRawResponse by lazy {
ThreadServiceImpl.WithRawResponseImpl(clientOptions)
}
private val traces: TraceService.WithRawResponse by lazy {
TraceServiceImpl.WithRawResponseImpl(clientOptions)
}
private val evaluators: EvaluatorService.WithRawResponse by lazy {
EvaluatorServiceImpl.WithRawResponseImpl(clientOptions)
}
private val onlineEvaluators: OnlineEvaluatorService.WithRawResponse by lazy {
OnlineEvaluatorServiceImpl.WithRawResponseImpl(clientOptions)
}
private val feedback: FeedbackService.WithRawResponse by lazy {
FeedbackServiceImpl.WithRawResponseImpl(clientOptions)
}
@@ -127,6 +202,14 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
AnnotationQueueServiceImpl.WithRawResponseImpl(clientOptions)
}
private val info: InfoService.WithRawResponse by lazy {
InfoServiceImpl.WithRawResponseImpl(clientOptions)
}
private val workspaces: WorkspaceService.WithRawResponse by lazy {
WorkspaceServiceImpl.WithRawResponseImpl(clientOptions)
}
private val repos: RepoService.WithRawResponse by lazy {
RepoServiceImpl.WithRawResponseImpl(clientOptions)
}
@@ -139,6 +222,14 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
SettingServiceImpl.WithRawResponseImpl(clientOptions)
}
private val issues: IssueService.WithRawResponse by lazy {
IssueServiceImpl.WithRawResponseImpl(clientOptions)
}
private val sandboxes: SandboxService.WithRawResponse by lazy {
SandboxServiceImpl.WithRawResponseImpl(clientOptions)
}
override fun withOptions(
modifier: Consumer<ClientOptions.Builder>
): LangsmithClient.WithRawResponse =
@@ -154,16 +245,32 @@ class LangsmithClientImpl(private val clientOptions: ClientOptions) : LangsmithC
override fun runs(): RunService.WithRawResponse = runs
override fun threads(): ThreadService.WithRawResponse = threads
override fun traces(): TraceService.WithRawResponse = traces
override fun evaluators(): EvaluatorService.WithRawResponse = evaluators
override fun onlineEvaluators(): OnlineEvaluatorService.WithRawResponse = onlineEvaluators
override fun feedback(): FeedbackService.WithRawResponse = feedback
override fun public_(): PublicService.WithRawResponse = public_
override fun annotationQueues(): AnnotationQueueService.WithRawResponse = annotationQueues
override fun info(): InfoService.WithRawResponse = info
override fun workspaces(): WorkspaceService.WithRawResponse = workspaces
override fun repos(): RepoService.WithRawResponse = repos
override fun commits(): CommitService.WithRawResponse = commits
override fun settings(): SettingService.WithRawResponse = settings
override fun issues(): IssueService.WithRawResponse = issues
override fun sandboxes(): SandboxService.WithRawResponse = sandboxes
}
}
@@ -0,0 +1,123 @@
package com.langchain.smith.client
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.langchain.smith.core.MultipartField
import com.langchain.smith.core.http.HttpRequestBody
import com.langchain.smith.core.http.multipartFormData
import com.langchain.smith.models.runs.RunAttachment
import com.langchain.smith.models.runs.RunIngest
import com.langchain.smith.models.runs.RunIngestBatchParams
import kotlin.jvm.optionals.getOrNull
import org.slf4j.LoggerFactory
internal fun RunIngestBatchParams.toRunMultipartFormData(jsonMapper: JsonMapper): HttpRequestBody? {
// Multipart requires run id, trace_id, and dotted_order to address each part correctly.
// If a queued run is missing those fields, callers fall back to legacy JSON batch ingest
// instead of failing the entire auto-batch.
val fields =
listOf(
multipartFieldsForRuns(
jsonMapper,
operation = "post",
runs = post().orElse(emptyList()),
) ?: return null,
multipartFieldsForRuns(
jsonMapper,
operation = "patch",
runs = patch().orElse(emptyList()),
) ?: return null,
)
.flatten()
.toMap()
return fields.takeIf { it.isNotEmpty() }?.let { multipartFormData(jsonMapper, it) }
}
private fun multipartFieldsForRuns(
jsonMapper: JsonMapper,
operation: String,
runs: List<RunIngest>,
): List<Pair<String, MultipartField<*>>>? =
runs.flatMap { run -> multipartFieldsForRun(jsonMapper, operation, run) ?: return null }
private fun multipartFieldsForRun(
jsonMapper: JsonMapper,
operation: String,
run: RunIngest,
): List<Pair<String, MultipartField<*>>>? {
val runId = run.id().getOrNull() ?: return null
run.traceId().getOrNull() ?: return null
run.dottedOrder().getOrNull() ?: return null
val runFields =
jsonMapper
.valueToTree<ObjectNode>(run)
.fields()
.asSequence()
.map { it.key to it.value }
.toList()
val main =
jsonMapper
.createObjectNode()
.setAll<ObjectNode>(
runFields
.filterNot { (fieldName, _) -> fieldName in MULTIPART_EXCLUDED_FIELDS }
.toMap()
)
val separateFields =
MULTIPART_SEPARATE_FIELDS.mapNotNull { fieldName ->
runFields
.firstOrNull { (name, _) -> name == fieldName }
?.second
?.takeUnless { it.isNull || it.isMissingNode }
?.let { fieldName to it }
}
return listOf("$operation.$runId" to jsonField(jsonMapper, main)) +
separateFields.map { (fieldName, value) ->
"$operation.$runId.$fieldName" to jsonField(jsonMapper, value)
} +
attachmentFields(runId, run.attachments())
}
private fun jsonField(jsonMapper: JsonMapper, value: JsonNode): MultipartField<ByteArray> {
val bytes = jsonMapper.writeValueAsBytes(value)
return MultipartField.builder<ByteArray>()
.value(bytes)
.contentType("application/json; length=${bytes.size}")
.build()
}
private fun attachmentFields(
runId: String,
attachments: Map<String, RunAttachment>,
): List<Pair<String, MultipartField<*>>> =
attachments.mapNotNull { (name, attachment) ->
if (name.contains('.')) {
logger.warn(
"Skipping attachment '{}' for run {}: attachment names must not contain periods ('.')",
name,
runId,
)
return@mapNotNull null
}
val contentType =
attachment.length().getOrNull()?.let { "${attachment.contentType()}; length=$it" }
?: attachment.contentType()
"attachment.$runId.$name" to
MultipartField.builder<Any>()
.value(attachment.multipartValue())
.contentType(contentType)
.filename(attachment.filename())
.build()
}
private val MULTIPART_SEPARATE_FIELDS =
listOf("inputs", "outputs", "events", "extra", "error", "serialized")
private val MULTIPART_EXCLUDED_FIELDS = MULTIPART_SEPARATE_FIELDS + "attachments"
private val logger = LoggerFactory.getLogger("com.langchain.smith.client.RunMultipartBatch")
@@ -0,0 +1,21 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core
import java.util.stream.Stream
import java.util.stream.StreamSupport
class AutoPager<T> private constructor(private val firstPage: Page<T>) : Iterable<T> {
companion object {
fun <T> from(firstPage: Page<T>): AutoPager<T> = AutoPager(firstPage)
}
override fun iterator(): Iterator<T> =
generateSequence(firstPage) { if (it.hasNextPage()) it.nextPage() else null }
.flatMap { it.items() }
.iterator()
fun stream(): Stream<T> = StreamSupport.stream(spliterator(), false)
}
@@ -0,0 +1,88 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core
import com.langchain.smith.core.http.AsyncStreamResponse
import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicReference
class AutoPagerAsync<T>
private constructor(private val firstPage: PageAsync<T>, private val defaultExecutor: Executor) :
AsyncStreamResponse<T> {
companion object {
fun <T> from(firstPage: PageAsync<T>, defaultExecutor: Executor): AutoPagerAsync<T> =
AutoPagerAsync(firstPage, defaultExecutor)
}
private val onCompleteFuture = CompletableFuture<Void?>()
private val state = AtomicReference(State.NEW)
override fun subscribe(handler: AsyncStreamResponse.Handler<T>): AsyncStreamResponse<T> =
subscribe(handler, defaultExecutor)
override fun subscribe(
handler: AsyncStreamResponse.Handler<T>,
executor: Executor,
): AsyncStreamResponse<T> = apply {
// TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
else "Cannot subscribe after the response is closed"
}
fun PageAsync<T>.handle(): CompletableFuture<Void?> {
if (state.get() == State.CLOSED) {
return CompletableFuture.completedFuture(null)
}
items().forEach { handler.onNext(it) }
return if (hasNextPage()) nextPage().thenCompose { it.handle() }
else CompletableFuture.completedFuture(null)
}
executor.execute {
firstPage.handle().whenComplete { _, error ->
val actualError =
if (error is CompletionException && error.cause != null) error.cause else error
try {
handler.onComplete(Optional.ofNullable(actualError))
} finally {
try {
if (actualError == null) {
onCompleteFuture.complete(null)
} else {
onCompleteFuture.completeExceptionally(actualError)
}
} finally {
close()
}
}
}
}
}
override fun onCompleteFuture(): CompletableFuture<Void?> = onCompleteFuture
override fun close() {
val previousState = state.getAndSet(State.CLOSED)
if (previousState == State.CLOSED) {
return
}
// When the stream is closed, we should always consider it closed. If it closed due
// to an error, then we will have already completed the future earlier, and this
// will be a no-op.
onCompleteFuture.complete(null)
}
}
private enum class State {
NEW,
SUBSCRIBED,
CLOSED,
}
@@ -3,14 +3,21 @@
package com.langchain.smith.core
import com.fasterxml.jackson.databind.json.JsonMapper
import com.langchain.smith.core.http.AsyncStreamResponse
import com.langchain.smith.core.http.Headers
import com.langchain.smith.core.http.HttpClient
import com.langchain.smith.core.http.LoggingHttpClient
import com.langchain.smith.core.http.PhantomReachableClosingHttpClient
import com.langchain.smith.core.http.QueryParams
import com.langchain.smith.core.http.RetryingHttpClient
import java.time.Clock
import java.time.Duration
import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicLong
import kotlin.jvm.optionals.getOrNull
/** A class representing the SDK client configuration. */
@@ -40,6 +47,14 @@ private constructor(
* rarely needs to be overridden.
*/
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
/**
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
*
* Defaults to a dedicated cached thread pool.
*
* This class takes ownership of the executor and shuts it down, if possible, when closed.
*/
@get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor,
/**
* The interface to use for delaying execution, like during retries.
*
@@ -66,6 +81,9 @@ private constructor(
/**
* Whether to call `validate` on every response before returning it.
*
* Setting this to `true` is _not_ forwards compatible with new types from the API for existing
* fields.
*
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -93,10 +111,20 @@ private constructor(
* Defaults to 2.
*/
@get:JvmName("maxRetries") val maxRetries: Int,
/** Whether run create/update calls should be automatically batched for tracing. */
@get:JvmName("autoBatchTracing") val autoBatchTracing: Boolean,
/**
* The level at which to log request and response information.
*
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
*
* Defaults to [LogLevel.fromEnv].
*/
@get:JvmName("logLevel") val logLevel: LogLevel,
private val apiKey: String?,
private val tenantId: String?,
private val bearerToken: String?,
private val organizationId: String?,
private val oauthAccessToken: String?,
private val profileAuth: ProfileAuth?,
) {
init {
@@ -116,14 +144,6 @@ private constructor(
fun tenantId(): Optional<String> = Optional.ofNullable(tenantId)
/**
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
* x-organization-id (for org scoped apis).
*/
fun bearerToken(): Optional<String> = Optional.ofNullable(bearerToken)
fun organizationId(): Optional<String> = Optional.ofNullable(organizationId)
fun toBuilder() = Builder().from(this)
companion object {
@@ -154,6 +174,7 @@ private constructor(
private var httpClient: HttpClient? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var streamHandlerExecutor: Executor? = null
private var sleeper: Sleeper? = null
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String? = null
@@ -162,16 +183,19 @@ private constructor(
private var responseValidation: Boolean = false
private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var autoBatchTracing: Boolean = true
private var logLevel: LogLevel = LogLevel.fromEnv()
private var apiKey: String? = null
private var tenantId: String? = null
private var bearerToken: String? = null
private var organizationId: String? = null
private var oauthAccessToken: String? = null
private var profileAuth: ProfileAuth? = null
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
streamHandlerExecutor = clientOptions.streamHandlerExecutor
sleeper = clientOptions.sleeper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
@@ -180,10 +204,12 @@ private constructor(
responseValidation = clientOptions.responseValidation
timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
autoBatchTracing = clientOptions.autoBatchTracing
logLevel = clientOptions.logLevel
apiKey = clientOptions.apiKey
tenantId = clientOptions.tenantId
bearerToken = clientOptions.bearerToken
organizationId = clientOptions.organizationId
oauthAccessToken = clientOptions.oauthAccessToken
profileAuth = clientOptions.profileAuth
}
/**
@@ -216,6 +242,20 @@ private constructor(
*/
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
/**
* The executor to use for running [AsyncStreamResponse.Handler] callbacks.
*
* Defaults to a dedicated cached thread pool.
*
* This class takes ownership of the executor and shuts it down, if possible, when closed.
*/
fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
this.streamHandlerExecutor =
if (streamHandlerExecutor is ExecutorService)
PhantomReachableExecutorService(streamHandlerExecutor)
else streamHandlerExecutor
}
/**
* The interface to use for delaying execution, like during retries.
*
@@ -249,6 +289,9 @@ private constructor(
/**
* Whether to call `validate` on every response before returning it.
*
* Setting this to `true` is _not_ forwards compatible with new types from the API for
* existing fields.
*
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -290,6 +333,25 @@ private constructor(
*/
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
/**
* Whether run create/update calls should be automatically batched for tracing.
*
* Defaults to true. Set to false to send run create/update calls synchronously through the
* single-run endpoints.
*/
fun autoBatchTracing(autoBatchTracing: Boolean) = apply {
this.autoBatchTracing = autoBatchTracing
}
/**
* The level at which to log request and response information.
*
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
*
* Defaults to [LogLevel.fromEnv].
*/
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
/** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
@@ -300,21 +362,6 @@ private constructor(
/** Alias for calling [Builder.tenantId] with `tenantId.orElse(null)`. */
fun tenantId(tenantId: Optional<String>) = tenantId(tenantId.getOrNull())
/**
* Bearer tokens are used to authenticate from the UI. Must also specify x-tenant-id or
* x-organization-id (for org scoped apis).
*/
fun bearerToken(bearerToken: String?) = apply { this.bearerToken = bearerToken }
/** Alias for calling [Builder.bearerToken] with `bearerToken.orElse(null)`. */
fun bearerToken(bearerToken: Optional<String>) = bearerToken(bearerToken.getOrNull())
fun organizationId(organizationId: String?) = apply { this.organizationId = organizationId }
/** Alias for calling [Builder.organizationId] with `organizationId.orElse(null)`. */
fun organizationId(organizationId: Optional<String>) =
organizationId(organizationId.getOrNull())
fun headers(headers: Headers) = apply {
this.headers.clear()
putAllHeaders(headers)
@@ -402,31 +449,30 @@ private constructor(
*
* See this table for the available options:
*
* |Setter |System property |Environment variable |Required|Default value |
* |----------------|-----------------------------------|---------------------------|--------|------------------------------------|
* |`apiKey` |`langchain.langsmithApiKey` |`LANGSMITH_API_KEY` |false |- |
* |`tenantId` |`langchain.langsmithTenantId` |`LANGSMITH_TENANT_ID` |false |- |
* |`bearerToken` |`langchain.langsmithBearerToken` |`LANGSMITH_BEARER_TOKEN` |false |- |
* |`organizationId`|`langchain.langsmithOrganizationId`|`LANGSMITH_ORGANIZATION_ID`|false |- |
* |`baseUrl` |`langchain.baseUrl` |`LANGSMITH_ENDPOINT` |true |`"https://api.smith.langchain.com/"`|
* |Setter |System property |Environment variable |Required|Default value |
* |----------|-----------------------------|---------------------|--------|------------------------------------|
* |`apiKey` |`langchain.langsmithApiKey` |`LANGSMITH_API_KEY` |false |- |
* |`tenantId`|`langchain.langsmithTenantId`|`LANGSMITH_TENANT_ID`|false |- |
* |`baseUrl` |`langchain.baseUrl` |`LANGSMITH_ENDPOINT` |true |`"https://api.smith.langchain.com/"`|
*
* System properties take precedence over environment variables.
*/
fun fromEnv() = apply {
(System.getProperty("langchain.baseUrl") ?: System.getenv("LANGSMITH_ENDPOINT"))?.let {
baseUrl(it)
logLevel(LogLevel.fromEnv())
langsmithEndpoint()?.let { baseUrl(it) }
langsmithApiKey()?.let { apiKey(it) }
langsmithTenantId()?.let { tenantId(it) }
System.getenv("LANGCHAIN_CUSTOM_HEADERS")?.let { customHeadersEnv ->
for (line in customHeadersEnv.split("\n")) {
val colon = line.indexOf(':')
if (colon >= 0) {
putHeader(line.substring(0, colon).trim(), line.substring(colon + 1).trim())
}
}
}
loadProfileClientConfig(jsonMapper, clock, profileNameOverride = profileName())?.let {
applyProfileConfig(it)
}
(System.getProperty("langchain.langsmithApiKey") ?: System.getenv("LANGSMITH_API_KEY"))
?.let { apiKey(it) }
(System.getProperty("langchain.langsmithTenantId")
?: System.getenv("LANGSMITH_TENANT_ID"))
?.let { tenantId(it) }
(System.getProperty("langchain.langsmithBearerToken")
?: System.getenv("LANGSMITH_BEARER_TOKEN"))
?.let { bearerToken(it) }
(System.getProperty("langchain.langsmithOrganizationId")
?: System.getenv("LANGSMITH_ORGANIZATION_ID"))
?.let { organizationId(it) }
}
/**
@@ -443,6 +489,24 @@ private constructor(
*/
fun build(): ClientOptions {
val httpClient = checkRequired("httpClient", httpClient)
val streamHandlerExecutor =
streamHandlerExecutor
?: PhantomReachableExecutorService(
Executors.newCachedThreadPool(
object : ThreadFactory {
private val threadFactory: ThreadFactory =
Executors.defaultThreadFactory()
private val count = AtomicLong(0)
override fun newThread(runnable: Runnable): Thread =
threadFactory.newThread(runnable).also {
it.name =
"langchain-stream-handler-thread-${count.getAndIncrement()}"
}
}
)
)
val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
val headers = Headers.builder()
@@ -454,39 +518,52 @@ private constructor(
headers.put("X-Stainless-Package-Version", getPackageVersion())
headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
apiKey?.let {
if (!it.isEmpty()) {
headers.put("X-API-Key", it)
}
}
tenantId?.let {
if (!it.isEmpty()) {
headers.put("X-Tenant-Id", it)
}
}
bearerToken?.let {
if (!it.isEmpty()) {
headers.put("Authorization", "Bearer $it")
}
}
organizationId?.let {
if (!it.isEmpty()) {
headers.put("X-Organization-Id", it)
}
}
headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
// We replace after all the default headers to allow end-users to overwrite them.
headers.replaceAll(this.headers.build())
queryParams.replaceAll(this.queryParams.build())
apiKey
?.takeIf { it.isNotEmpty() }
?.let {
headers.remove("Authorization")
headers.replace("X-API-Key", it)
}
if (apiKey.isNullOrEmpty()) {
oauthAccessToken
?.takeIf { it.isNotEmpty() }
?.let {
val currentHeaders = headers.build()
if (
currentHeaders.values("X-API-Key").all(String::isBlank) &&
currentHeaders.values("Authorization").all(String::isBlank)
) {
headers.replace("Authorization", "Bearer $it")
}
}
}
tenantId?.takeIf { it.isNotEmpty() }?.let { headers.replace("X-Tenant-Id", it) }
return ClientOptions(
httpClient,
val retryingHttpClient =
RetryingHttpClient.builder()
.httpClient(httpClient)
.httpClient(
LoggingHttpClient.builder()
.httpClient(httpClient)
.clock(clock)
.level(logLevel)
.build()
)
.sleeper(sleeper)
.clock(clock)
.maxRetries(maxRetries)
.build(),
.build()
return ClientOptions(
httpClient,
profileAuth?.let { ProfileAuthHttpClient(retryingHttpClient, it) }
?: retryingHttpClient,
checkJacksonVersionCompatibility,
jsonMapper,
streamHandlerExecutor,
sleeper,
clock,
baseUrl,
@@ -495,12 +572,56 @@ private constructor(
responseValidation,
timeout,
maxRetries,
autoBatchTracing,
logLevel,
apiKey,
tenantId,
bearerToken,
organizationId,
oauthAccessToken,
profileAuth,
)
}
private fun applyProfileConfig(profile: ProfileClientConfig) {
if (baseUrl.isNullOrBlank()) {
profile.baseUrl?.let { baseUrl(it) }
}
if (tenantId.isNullOrBlank()) {
profile.tenantId?.let { tenantId(it) }
}
if (!apiKey.isNullOrBlank() || hasAuthHeader()) {
return
}
if (!profile.oauthAccessToken.isNullOrBlank()) {
oauthAccessToken = profile.oauthAccessToken
profileAuth = profile.profileAuth
} else if (profile.profileAuth != null) {
profileAuth = profile.profileAuth
} else if (!profile.apiKey.isNullOrBlank()) {
apiKey(profile.apiKey)
}
}
private fun hasAuthHeader(): Boolean {
val currentHeaders = headers.build()
return currentHeaders.values("X-API-Key").any { it.isNotBlank() } ||
currentHeaders.values("Authorization").any { it.isNotBlank() }
}
private fun langsmithEndpoint(): String? =
System.getProperty("langchain.baseUrl")
?: System.getenv("LANGSMITH_ENDPOINT")
?: System.getenv("LANGCHAIN_ENDPOINT")
private fun langsmithApiKey(): String? =
System.getProperty("langchain.langsmithApiKey")
?: System.getenv("LANGSMITH_API_KEY")
?: System.getenv("LANGCHAIN_API_KEY")
private fun langsmithTenantId(): String? =
System.getProperty("langchain.langsmithTenantId")
?: System.getenv("LANGSMITH_TENANT_ID")
?: System.getenv("LANGSMITH_WORKSPACE_ID")
?: System.getenv("LANGCHAIN_WORKSPACE_ID")
}
/**
@@ -515,6 +636,7 @@ private constructor(
*/
fun close() {
httpClient.close()
(streamHandlerExecutor as? ExecutorService)?.shutdown()
sleeper.close()
}
}
@@ -0,0 +1,33 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core
/** The level at which to log request and response information. */
enum class LogLevel {
/** No logging. */
OFF,
/** Minimal request and response summary logs. No headers or bodies are logged. */
INFO,
/** [INFO] logs plus details about request failures. */
ERROR,
/**
* Full request and response logs. Sensitive headers are redacted, but sensitive data in request
* and response bodies may still be visible.
*/
DEBUG;
/** Returns whether this level is at or higher than the given [level]. */
fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
companion object {
/** Returns a [LogLevel] based on the `LANGCHAIN_LOG` environment variable. */
fun fromEnv() =
when (System.getenv("LANGCHAIN_LOG")?.lowercase()) {
"info" -> INFO
"error" -> ERROR
"debug" -> DEBUG
else -> OFF
}
}
}
@@ -24,11 +24,14 @@ import java.io.InputStream
import java.time.DateTimeException
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
fun jsonMapper(): JsonMapper = JSON_MAPPER
private val JSON_MAPPER: JsonMapper =
JsonMapper.builder()
.addModule(kotlinModule())
.addModule(Jdk8Module())
@@ -36,7 +39,7 @@ fun jsonMapper(): JsonMapper =
.addModule(
SimpleModule()
.addSerializer(InputStreamSerializer)
.addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
.addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
)
.withCoercionConfig(LogicalType.Boolean) {
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -47,6 +50,7 @@ fun jsonMapper(): JsonMapper =
}
.withCoercionConfig(LogicalType.Integer) {
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
@@ -64,6 +68,12 @@ fun jsonMapper(): JsonMapper =
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
}
.withCoercionConfig(LogicalType.DateTime) {
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
}
.withCoercionConfig(LogicalType.Array) {
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -124,10 +134,10 @@ private object InputStreamSerializer : BaseSerializer<InputStream>(InputStream::
}
/**
* A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
* A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
*/
private class LenientLocalDateTimeDeserializer :
StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
private class LenientOffsetDateTimeDeserializer :
StdDeserializer<OffsetDateTime>(OffsetDateTime::class.java) {
companion object {
@@ -141,7 +151,7 @@ private class LenientLocalDateTimeDeserializer :
override fun logicalType(): LogicalType = LogicalType.DateTime
override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime {
val exceptions = mutableListOf<Exception>()
for (formatter in DATE_TIME_FORMATTERS) {
@@ -150,17 +160,20 @@ private class LenientLocalDateTimeDeserializer :
return when {
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
LocalDate.from(temporal).atStartOfDay()
LocalDate.from(temporal)
.atStartOfDay()
.atZone(ZoneId.of("UTC"))
.toOffsetDateTime()
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
LocalDateTime.from(temporal)
else -> ZonedDateTime.from(temporal).toLocalDateTime()
LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
else -> OffsetDateTime.from(temporal)
}
} catch (e: DateTimeException) {
exceptions.add(e)
}
}
throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
exceptions.forEach { addSuppressed(it) }
}
}
@@ -0,0 +1,33 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core
/**
* An interface representing a single page, with items of type [T], from a paginated endpoint
* response.
*
* Implementations of this interface are expected to request additional pages synchronously. For
* asynchronous pagination, see the [PageAsync] interface.
*/
interface Page<T> {
/**
* Returns whether there's another page after this one.
*
* The method generally doesn't make requests so the result depends entirely on the data in this
* page. If a significant amount of time has passed between requesting this page and calling
* this method, then the result could be stale.
*/
fun hasNextPage(): Boolean
/**
* Returns the page after this one by making another request.
*
* @throws IllegalStateException if it's impossible to get the next page. This exception is
* avoidable by calling [hasNextPage] first.
*/
fun nextPage(): Page<T>
/** Returns the items in this page. */
fun items(): List<T>
}
@@ -0,0 +1,35 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core
import java.util.concurrent.CompletableFuture
/**
* An interface representing a single page, with items of type [T], from a paginated endpoint
* response.
*
* Implementations of this interface are expected to request additional pages asynchronously. For
* synchronous pagination, see the [Page] interface.
*/
interface PageAsync<T> {
/**
* Returns whether there's another page after this one.
*
* The method generally doesn't make requests so the result depends entirely on the data in this
* page. If a significant amount of time has passed between requesting this page and calling
* this method, then the result could be stale.
*/
fun hasNextPage(): Boolean
/**
* Returns the page after this one by making another request.
*
* @throws IllegalStateException if it's impossible to get the next page. This exception is
* avoidable by calling [hasNextPage] first.
*/
fun nextPage(): CompletableFuture<out PageAsync<T>>
/** Returns the items in this page. */
fun items(): List<T>
}
@@ -0,0 +1,272 @@
package com.langchain.smith.core
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.langchain.smith.core.http.HttpClient
import com.langchain.smith.core.http.HttpRequest
import com.langchain.smith.core.http.HttpResponse
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.OffsetDateTime
import java.util.concurrent.CompletableFuture
internal data class ProfileClientConfig(
val baseUrl: String?,
val apiKey: String?,
val tenantId: String?,
val oauthAccessToken: String?,
val profileAuth: ProfileAuth?,
)
private const val OAUTH_CLIENT_ID = "langsmith-cli"
private val TOKEN_REFRESH_LEEWAY: Duration = Duration.ofMinutes(1)
private val TOKEN_REFRESH_TIMEOUT: Duration = Duration.ofSeconds(10)
internal fun loadProfileClientConfig(
jsonMapper: JsonMapper,
clock: Clock,
configPathOverride: Path? = null,
profileNameOverride: String? = null,
): ProfileClientConfig? {
val configPath = configPathOverride ?: profileConfigPath()
if (!Files.isRegularFile(configPath)) {
return null
}
val config =
runCatching { jsonMapper.readTree(configPath.toFile()) as? ObjectNode }.getOrNull()
?: return null
val profiles = config["profiles"] as? ObjectNode ?: return null
val profileName =
profileNameOverride?.takeIf { it.isNotBlank() }
?: text(config["current_profile"])
?: "default"
val profile = profiles[profileName] as? ObjectNode ?: return null
val oauth = profile["oauth"]
val oauthAccessToken = text(oauth?.get("access_token"))
val oauthRefreshToken = text(oauth?.get("refresh_token"))
return ProfileClientConfig(
baseUrl = text(profile["api_url"]),
apiKey = text(profile["api_key"]),
tenantId = text(profile["workspace_id"]),
oauthAccessToken = oauthAccessToken,
profileAuth =
if (oauthAccessToken != null || oauthRefreshToken != null) {
ProfileAuth(jsonMapper, clock, configPath, config, profile)
} else {
null
},
)
}
internal fun profileConfigPath(): Path {
val configuredPath =
System.getProperty("langchain.langsmithConfigFile")
?: System.getenv("LANGSMITH_CONFIG_FILE")
return configuredPath?.takeIf { it.isNotBlank() }?.let(Paths::get)
?: Paths.get(System.getProperty("user.home"), ".langsmith", "config.json")
}
internal fun profileName(): String? =
(System.getProperty("langchain.langsmithProfile") ?: System.getenv("LANGSMITH_PROFILE"))
?.takeIf { it.isNotBlank() }
private fun shouldRefreshProfileToken(oauth: ObjectNode, clock: Clock): Boolean {
if (text(oauth["refresh_token"]) == null) {
return false
}
if (text(oauth["access_token"]) == null) {
return true
}
val expiresAt = expiresAt(oauth["expires_at"]) ?: return false
return !expiresAt.isAfter(clock.instant().plus(TOKEN_REFRESH_LEEWAY))
}
private fun refreshProfileOAuthToken(
jsonMapper: JsonMapper,
baseUrl: String,
refreshToken: String,
): OAuthTokenResponse? {
return runCatching {
val connection =
(URL("${normalizeConfigUrl(baseUrl)}/oauth/token").openConnection()
as HttpURLConnection)
.apply {
requestMethod = "POST"
connectTimeout = TOKEN_REFRESH_TIMEOUT.toMillis().toInt()
readTimeout = TOKEN_REFRESH_TIMEOUT.toMillis().toInt()
doOutput = true
setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
}
val body =
formEncode(
mapOf(
"grant_type" to "refresh_token",
"client_id" to OAUTH_CLIENT_ID,
"refresh_token" to refreshToken,
)
)
OutputStreamWriter(connection.outputStream, StandardCharsets.UTF_8).use {
it.write(body)
}
if (connection.responseCode !in 200..299) {
return@runCatching null
}
val response = connection.inputStream.use { jsonMapper.readTree(it) }
val accessToken = text(response["access_token"]) ?: return@runCatching null
OAuthTokenResponse(
accessToken = accessToken,
refreshToken = text(response["refresh_token"]),
expiresInSeconds = response["expires_in"]?.takeIf { it.isNumber }?.asLong(),
)
}
.getOrNull()
}
private fun normalizeConfigUrl(baseUrl: String): String =
baseUrl.trimEnd('/').removeSuffix("/api/v1")
private fun formEncode(values: Map<String, String>): String =
values.entries.joinToString("&") { (key, value) -> "${urlEncode(key)}=${urlEncode(value)}" }
private fun urlEncode(value: String): String =
URLEncoder.encode(value, StandardCharsets.UTF_8.name())
private fun text(node: JsonNode?): String? =
node?.takeIf { it.isTextual }?.asText()?.trimQuotes()?.takeIf { it.isNotBlank() }
private fun expiresAt(node: JsonNode?): Instant? =
when {
node == null -> null
node.isNumber -> Instant.ofEpochSecond(node.asLong())
node.isTextual ->
runCatching { Instant.parse(node.asText()) }
.recoverCatching { OffsetDateTime.parse(node.asText()).toInstant() }
.getOrNull()
else -> null
}
private fun String.trimQuotes(): String = trim().trim('"', '\'')
internal class ProfileAuth(
private val jsonMapper: JsonMapper,
private val clock: Clock,
private val configPath: Path,
private val config: ObjectNode,
private val profile: ObjectNode,
) {
private val managedAuthorizationValues = mutableSetOf<String>()
init {
rememberProfileAuthHeader(authHeaderFromProfile())
}
@Synchronized
fun currentAuthHeader(): Pair<String, String>? =
authHeaderFromProfile().also { rememberProfileAuthHeader(it) }
@Synchronized
fun isProfileAuthorizationHeader(value: String): Boolean =
managedAuthorizationValues.contains(value)
@Synchronized
fun authHeader(): Pair<String, String>? {
val oauth = profile["oauth"] as? ObjectNode
if (oauth != null && shouldRefreshProfileToken(oauth, clock)) {
val refreshToken = text(oauth["refresh_token"])
if (refreshToken != null) {
val tokenEndpointBaseUrl = text(profile["api_url"]) ?: ClientOptions.PRODUCTION_URL
val tokenResponse =
refreshProfileOAuthToken(jsonMapper, tokenEndpointBaseUrl, refreshToken)
if (tokenResponse != null) {
oauth.put("access_token", tokenResponse.accessToken)
tokenResponse.refreshToken?.let { oauth.put("refresh_token", it) }
tokenResponse.expiresInSeconds?.let {
oauth.put("expires_at", clock.instant().plusSeconds(it).toString())
}
oauth.remove("token_type")
oauth.remove("bearer_token")
saveConfig()
}
}
}
return authHeaderFromProfile().also { rememberProfileAuthHeader(it) }
}
private fun authHeaderFromProfile(): Pair<String, String>? {
val oauth = profile["oauth"] as? ObjectNode
text(oauth?.get("access_token"))?.let {
return "Authorization" to "Bearer $it"
}
text(profile["api_key"])?.let {
return "X-API-Key" to it
}
return null
}
private fun rememberProfileAuthHeader(header: Pair<String, String>?) {
if (header?.first?.equals("Authorization", ignoreCase = true) == true) {
managedAuthorizationValues.add(header.second)
}
}
private fun saveConfig() {
runCatching {
configPath.parent?.let { Files.createDirectories(it) }
jsonMapper.writerWithDefaultPrettyPrinter().writeValue(configPath.toFile(), config)
}
}
}
internal class ProfileAuthHttpClient(
private val delegate: HttpClient,
private val profileAuth: ProfileAuth,
) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse =
delegate.execute(prepareProfileAuthRequest(request), requestOptions)
override fun executeAsync(
request: HttpRequest,
requestOptions: RequestOptions,
): CompletableFuture<HttpResponse> =
delegate.executeAsync(prepareProfileAuthRequest(request), requestOptions)
override fun close() = delegate.close()
private fun prepareProfileAuthRequest(request: HttpRequest): HttpRequest {
if (request.headers.values("X-API-Key").any(String::isNotBlank)) {
return request.toBuilder().removeHeaders("Authorization").build()
}
val authorizationValues = request.headers.values("Authorization").filter(String::isNotBlank)
if (
authorizationValues.isNotEmpty() &&
authorizationValues.none { profileAuth.isProfileAuthorizationHeader(it) }
) {
return request
}
val (name, value) = profileAuth.authHeader() ?: return request
val builder = request.toBuilder()
if (name.equals("X-API-Key", ignoreCase = true)) {
builder.removeHeaders("Authorization")
}
return builder.replaceHeaders(name, value).build()
}
}
private data class OAuthTokenResponse(
val accessToken: String,
val refreshToken: String?,
val expiresInSeconds: Long?,
)
@@ -3,6 +3,9 @@
package com.langchain.smith.core
import com.langchain.smith.client.LangsmithClient
import org.slf4j.LoggerFactory
private val logger = LoggerFactory.getLogger("com.langchain.smith.core.Properties")
fun getOsArch(): String {
val osArch = System.getProperty("os.arch")
@@ -12,8 +15,10 @@ fun getOsArch(): String {
"i386",
"x32",
"x86" -> "x32"
"amd64",
"x86_64" -> "x64"
"arm" -> "arm"
"aarch64" -> "arm64"
else -> "other:$osArch"
@@ -34,9 +39,23 @@ fun getOsName(): String {
}
}
fun getOsVersion(): String = System.getProperty("os.version", "unknown")
fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown"
fun getPackageVersion(): String =
LangsmithClient::class.java.`package`.implementationVersion ?: "unknown"
fun getPackageVersion(): String {
try {
val props = java.util.Properties()
LangsmithClient::class
.java
.classLoader
.getResourceAsStream("com/langchain/smith/version.properties")
?.use { props.load(it) }
props.getProperty("version")?.let {
return it
}
} catch (e: Exception) {
logger.warn("Failed to read version from version.properties", e)
}
return LangsmithClient::class.java.`package`.implementationVersion ?: "unknown"
}
fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown"
@@ -33,6 +33,15 @@ class RequestOptions private constructor(val responseValidation: Boolean?, val t
private var responseValidation: Boolean? = null
private var timeout: Timeout? = null
/**
* Whether to call `validate` on the response before returning it.
*
* Setting this to `true` is _not_ forwards compatible with new types from the API for
* existing fields.
*
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
@@ -5,6 +5,7 @@ package com.langchain.smith.core
import com.langchain.smith.errors.LangChainInvalidDataException
import java.util.Collections
import java.util.SortedMap
import java.util.SortedSet
import java.util.concurrent.CompletableFuture
import java.util.concurrent.locks.Lock
@@ -16,6 +17,11 @@ internal fun <T : Any> T?.getOrThrow(name: String): T =
internal fun <T> List<T>.toImmutable(): List<T> =
if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
@JvmSynthetic
internal fun <V : Comparable<V>> SortedSet<V>.toImmutable(): SortedSet<V> =
if (isEmpty()) Collections.emptySortedSet()
else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
@JvmSynthetic
internal fun <K, V> Map<K, V>.toImmutable(): Map<K, V> =
if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
@@ -589,7 +589,7 @@ private constructor(
override fun hashCode() = values.hashCode()
override fun toString() = values.toString()
override fun toString() = "[object Object]"
companion object {
@@ -0,0 +1,12 @@
@file:JvmName("EmptyHandler")
package com.langchain.smith.core.handlers
import com.langchain.smith.core.http.HttpResponse
import com.langchain.smith.core.http.HttpResponse.Handler
@JvmSynthetic internal fun emptyHandler(): Handler<Void?> = EmptyHandlerInternal
private object EmptyHandlerInternal : Handler<Void?> {
override fun handle(response: HttpResponse): Void? = null
}
@@ -2,6 +2,7 @@ package com.langchain.smith.core.http
import com.langchain.smith.core.checkRequired
import com.langchain.smith.core.toImmutable
import java.net.URLEncoder
class HttpRequest
private constructor(
@@ -13,6 +14,35 @@ private constructor(
@get:JvmName("body") val body: HttpRequestBody?,
) {
fun url(): String = buildString {
append(baseUrl)
pathSegments.forEach { segment ->
if (!endsWith("/")) {
append("/")
}
append(URLEncoder.encode(segment, "UTF-8"))
}
if (queryParams.isEmpty()) {
return@buildString
}
append("?")
var isFirst = true
queryParams.keys().forEach { key ->
queryParams.values(key).forEach { value ->
if (!isFirst) {
append("&")
}
append(URLEncoder.encode(key, "UTF-8"))
append("=")
append(URLEncoder.encode(value, "UTF-8"))
isFirst = false
}
}
}
fun toBuilder(): Builder = Builder().from(this)
override fun toString(): String =
@@ -7,14 +7,15 @@ package com.langchain.smith.core.http
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.JsonNodeType
import com.github.luben.zstd.ZstdOutputStream
import com.langchain.smith.core.MultipartField
import com.langchain.smith.core.toImmutable
import com.langchain.smith.errors.LangChainInvalidDataException
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID
import kotlin.jvm.optionals.getOrNull
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
import org.apache.hc.core5.http.ContentType
import org.apache.hc.core5.http.HttpEntity
@JvmSynthetic
internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
@@ -32,99 +33,243 @@ internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequ
override fun close() {}
}
@JvmSynthetic
internal fun zstd(body: HttpRequestBody): HttpRequestBody =
object : HttpRequestBody {
override fun writeTo(outputStream: OutputStream) {
ZstdOutputStream(NonClosingOutputStream(outputStream)).setCloseFrameOnFlush(true).use {
body.writeTo(it)
}
}
override fun contentType(): String? = body.contentType()
override fun contentLength(): Long = -1L
override fun repeatable(): Boolean = body.repeatable()
override fun close() = body.close()
}
@JvmSynthetic
internal fun multipartFormData(
jsonMapper: JsonMapper,
fields: Map<String, MultipartField<*>>,
): HttpRequestBody =
object : HttpRequestBody {
private val entity: HttpEntity by lazy {
MultipartEntityBuilder.create()
.apply {
fields.forEach { (name, field) ->
val knownValue = field.value.asKnown().getOrNull()
val parts =
if (knownValue is InputStream) {
// Read directly from the `InputStream` instead of reading it all
// into memory due to the `jsonMapper` serialization below.
sequenceOf(name to knownValue)
} else {
val node = jsonMapper.valueToTree<JsonNode>(field.value)
serializePart(name, node)
}
parts.forEach { (name, bytes) ->
addBinaryBody(
name,
bytes,
ContentType.parseLenient(field.contentType),
field.filename().getOrNull(),
)
}
MultipartBody.Builder()
.apply {
fields.forEach { (name, field) ->
val knownValue = field.value.asKnown().getOrNull()
val parts =
if (knownValue is InputStream) {
// Read directly from the `InputStream` instead of reading it all
// into memory due to the `jsonMapper` serialization below.
sequenceOf(name to knownValue)
} else {
val node = jsonMapper.valueToTree<JsonNode>(field.value)
serializePart(name, node)
}
parts.forEach { (name, bytes) ->
val partBody =
if (bytes is ByteArrayInputStream) {
val byteArray = bytes.readBytes()
object : HttpRequestBody {
override fun writeTo(outputStream: OutputStream) {
outputStream.write(byteArray)
}
override fun contentType(): String = field.contentType
override fun contentLength(): Long = byteArray.size.toLong()
override fun repeatable(): Boolean = true
override fun close() {}
}
} else {
object : HttpRequestBody {
override fun writeTo(outputStream: OutputStream) {
bytes.copyTo(outputStream)
}
override fun contentType(): String = field.contentType
override fun contentLength(): Long = -1L
override fun repeatable(): Boolean = false
override fun close() = bytes.close()
}
}
addPart(
MultipartBody.Part.create(
name,
field.filename().getOrNull(),
field.contentType,
partBody,
)
)
}
.build()
}
}
.build()
private class NonClosingOutputStream(private val delegate: OutputStream) : OutputStream() {
override fun write(b: Int) = delegate.write(b)
override fun write(b: ByteArray) = delegate.write(b)
override fun write(b: ByteArray, off: Int, len: Int) = delegate.write(b, off, len)
override fun flush() = delegate.flush()
override fun close() = Unit
}
private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, InputStream>> =
when (node.nodeType) {
JsonNodeType.MISSING,
JsonNodeType.NULL -> emptySequence()
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream())
JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
JsonNodeType.ARRAY ->
node.elements().asSequence().flatMap { element -> serializePart(name, element) }
JsonNodeType.OBJECT ->
node.fields().asSequence().flatMap { (key, value) ->
serializePart("$name[$key]", value)
}
JsonNodeType.POJO,
null -> throw LangChainInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
}
private class MultipartBody
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
private val boundaryBytes: ByteArray = boundary.toByteArray()
private val contentType = "multipart/form-data; boundary=$boundary"
// This must remain in sync with `contentLength`.
override fun writeTo(outputStream: OutputStream) {
parts.forEach { part ->
outputStream.write(DASHDASH)
outputStream.write(boundaryBytes)
outputStream.write(CRLF)
outputStream.write(CONTENT_DISPOSITION)
outputStream.write(part.contentDisposition.toByteArray())
outputStream.write(CRLF)
outputStream.write(CONTENT_TYPE)
outputStream.write(part.contentType.toByteArray())
outputStream.write(CRLF)
outputStream.write(CRLF)
part.body.writeTo(outputStream)
outputStream.write(CRLF)
}
private fun serializePart(
name: String,
node: JsonNode,
): Sequence<Pair<String, InputStream>> =
when (node.nodeType) {
JsonNodeType.MISSING,
JsonNodeType.NULL -> emptySequence()
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream())
JsonNodeType.BOOLEAN ->
sequenceOf(name to node.booleanValue().toString().inputStream())
JsonNodeType.NUMBER ->
sequenceOf(name to node.numberValue().toString().inputStream())
JsonNodeType.ARRAY ->
sequenceOf(
name to
node
.elements()
.asSequence()
.mapNotNull { element ->
when (element.nodeType) {
JsonNodeType.MISSING,
JsonNodeType.NULL -> null
JsonNodeType.STRING -> node.textValue()
JsonNodeType.BOOLEAN -> node.booleanValue().toString()
JsonNodeType.NUMBER -> node.numberValue().toString()
null,
JsonNodeType.BINARY,
JsonNodeType.ARRAY,
JsonNodeType.OBJECT,
JsonNodeType.POJO ->
throw LangChainInvalidDataException(
"Unexpected JsonNode type in array: ${node.nodeType}"
)
}
}
.joinToString(",")
.inputStream()
)
JsonNodeType.OBJECT ->
node.fields().asSequence().flatMap { (key, value) ->
serializePart("$name[$key]", value)
}
JsonNodeType.POJO,
null ->
throw LangChainInvalidDataException(
"Unexpected JsonNode type: ${node.nodeType}"
)
outputStream.write(DASHDASH)
outputStream.write(boundaryBytes)
outputStream.write(DASHDASH)
outputStream.write(CRLF)
}
override fun contentType(): String = contentType
// This must remain in sync with `writeTo`.
override fun contentLength(): Long {
var byteCount = 0L
parts.forEach { part ->
val contentLength = part.body.contentLength()
if (contentLength == -1L) {
return -1L
}
private fun String.inputStream(): InputStream = toByteArray().inputStream()
byteCount +=
DASHDASH.size +
boundaryBytes.size +
CRLF.size +
CONTENT_DISPOSITION.size +
part.contentDisposition.toByteArray().size +
CRLF.size +
CONTENT_TYPE.size +
part.contentType.toByteArray().size +
CRLF.size +
CRLF.size +
contentLength +
CRLF.size
}
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
override fun contentType(): String = entity.contentType
override fun contentLength(): Long = entity.contentLength
override fun repeatable(): Boolean = entity.isRepeatable
override fun close() = entity.close()
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
return byteCount
}
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
override fun close() {
parts.forEach { it.body.close() }
}
class Builder {
private val boundary = UUID.randomUUID().toString()
private val parts: MutableList<Part> = mutableListOf()
fun addPart(part: Part) = apply { parts.add(part) }
fun build() = MultipartBody(boundary, parts.toImmutable())
}
class Part
private constructor(
val contentDisposition: String,
val contentType: String,
val body: HttpRequestBody,
) {
companion object {
fun create(
name: String,
filename: String?,
contentType: String,
body: HttpRequestBody,
): Part {
val disposition = buildString {
append("form-data; name=")
appendQuotedString(name)
if (filename != null) {
append("; filename=")
appendQuotedString(filename)
}
}
return Part(disposition, contentType, body)
}
}
}
companion object {
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
private val CONTENT_TYPE = "Content-Type: ".toByteArray()
private fun StringBuilder.appendQuotedString(key: String) {
append('"')
for (ch in key) {
when (ch) {
'\n' -> append("%0A")
'\r' -> append("%0D")
'"' -> append("%22")
else -> append(ch)
}
}
append('"')
}
}
}
@@ -0,0 +1,630 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core.http
import com.langchain.smith.core.LogLevel
import com.langchain.smith.core.RequestOptions
import com.langchain.smith.core.checkRequired
import com.langchain.smith.core.toImmutable
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.charset.CharacterCodingException
import java.nio.charset.Charset
import java.nio.charset.CharsetDecoder
import java.nio.charset.CodingErrorAction
import java.nio.charset.StandardCharsets
import java.time.Clock
import java.time.Duration
import java.time.OffsetDateTime
import java.util.SortedSet
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import kotlin.time.toKotlinDuration
/** A wrapper [HttpClient] around [httpClient] that logs request and response information. */
class LoggingHttpClient
private constructor(
/** The underlying [HttpClient] for making requests. */
@get:JvmName("httpClient") val httpClient: HttpClient,
/**
* Sensitive headers to redact from logs.
*
* Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie",
* "x-tenant-id")`.
*/
@get:JvmName("redactedHeaders") val redactedHeaders: SortedSet<String>,
/**
* The clock to use for measuring request and response durations.
*
* This is primarily useful for using a fake clock in tests.
*
* Defaults to [Clock.systemUTC].
*/
@get:JvmName("clock") val clock: Clock,
/**
* The log level to use.
*
* Pass [LogLevel.fromEnv] to read from environment variables.
*/
@get:JvmName("level") val level: LogLevel,
) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
val loggingRequest = logRequest(request)
val before = OffsetDateTime.now(clock)
val response =
try {
httpClient.execute(loggingRequest, requestOptions)
} catch (e: Throwable) {
logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
throw e
}
val took = Duration.between(before, OffsetDateTime.now(clock))
return logResponse(response, took)
}
override fun executeAsync(
request: HttpRequest,
requestOptions: RequestOptions,
): CompletableFuture<HttpResponse> {
val loggingRequest = logRequest(request)
val before = OffsetDateTime.now(clock)
val future =
try {
httpClient.executeAsync(loggingRequest, requestOptions)
} catch (e: Throwable) {
logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
throw e
}
return future.handle { response, error ->
val took = Duration.between(before, OffsetDateTime.now(clock))
if (error != null) {
logFailure(unwrapCompletionException(error), took)
throw error
}
logResponse(response, took)
}
}
private fun logRequest(request: HttpRequest): HttpRequest {
if (!level.shouldLog(LogLevel.INFO)) {
return request
}
System.err.println(
buildString {
append("--> ${request.method} ${request.url()}")
request.body?.let {
val length = it.contentLength()
append(if (length >= 0) " ($length-byte body)" else " (unknown-length body)")
}
}
)
if (!level.shouldLog(LogLevel.DEBUG)) {
return request
}
logHeaders(request.headers)
if (request.body == null) {
System.err.println("--> END ${request.method}")
System.err.println()
return request
}
return request
.toBuilder()
.body(LoggingHttpRequestBody(request.method, request.body))
.build()
}
private fun logResponse(response: HttpResponse, took: Duration): HttpResponse {
if (!level.shouldLog(LogLevel.INFO)) {
return response
}
val contentLength = response.headers().values("Content-Length").firstOrNull()?.toIntOrNull()
System.err.println(
"<-- ${response.statusCode()} (${
buildString {
append(took.format())
contentLength?.let { append(", $contentLength-byte body") }
}
})"
)
if (!level.shouldLog(LogLevel.DEBUG)) {
return response
}
logHeaders(response.headers())
return LoggingHttpResponse(response)
}
private fun logFailure(error: Throwable, took: Duration) {
if (!level.shouldLog(LogLevel.ERROR)) {
return
}
System.err.println(
buildString {
append("<-- !! ${error.javaClass.simpleName}")
error.message?.let { append(": $it") }
append(" (${took.format()})")
}
)
}
private fun unwrapCompletionException(error: Throwable): Throwable =
if (error is CompletionException && error.cause != null) error.cause!! else error
private fun logHeaders(headers: Headers) =
headers.names().forEach { name ->
headers.values(name).forEach { value ->
System.err.println("$name: ${if (redactedHeaders.contains(name)) "██" else value}")
}
}
override fun close() = httpClient.close()
fun toBuilder() = Builder().from(this)
companion object {
/**
* Returns a mutable builder for constructing an instance of [LoggingHttpClient].
*
* The following fields are required:
* ```java
* .httpClient()
* .level()
* ```
*/
@JvmStatic fun builder() = Builder()
}
/** A builder for [LoggingHttpClient]. */
class Builder internal constructor() {
private var httpClient: HttpClient? = null
private var redactedHeaders: Set<String> =
setOf("authorization", "api-key", "x-api-key", "cookie", "set-cookie", "x-tenant-id")
private var clock: Clock = Clock.systemUTC()
private var level: LogLevel? = null
@JvmSynthetic
internal fun from(loggingHttpClient: LoggingHttpClient) = apply {
httpClient = loggingHttpClient.httpClient
redactedHeaders = loggingHttpClient.redactedHeaders
clock = loggingHttpClient.clock
level = loggingHttpClient.level
}
/** The underlying [HttpClient] for making requests. */
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
/**
* Sensitive headers to redact from logs.
*
* Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie",
* "x-tenant-id")`.
*/
fun redactedHeaders(redactedHeaders: Set<String>) = apply {
this.redactedHeaders = redactedHeaders
}
/**
* The clock to use for measuring request and response durations.
*
* This is primarily useful for using a fake clock in tests.
*
* Defaults to [Clock.systemUTC].
*/
fun clock(clock: Clock) = apply { this.clock = clock }
/**
* The log level to use.
*
* Pass [LogLevel.fromEnv] to read from environment variables.
*/
fun level(level: LogLevel) = apply { this.level = level }
/**
* Returns an immutable instance of [LoggingHttpClient].
*
* Further updates to this [Builder] will not mutate the returned instance.
*
* The following fields are required:
* ```java
* .httpClient()
* .level()
* ```
*
* @throws IllegalStateException if any required field is unset.
*/
fun build(): LoggingHttpClient =
LoggingHttpClient(
checkRequired("httpClient", httpClient),
redactedHeaders.toSortedSet(String.CASE_INSENSITIVE_ORDER).toImmutable(),
clock,
checkRequired("level", level),
)
}
}
/**
* An [HttpRequestBody] wrapper that delegates to [body] while also logging line by line as it's
* written.
*
* The logging occurs in a streaming manner with minimal buffering.
*/
private class LoggingHttpRequestBody(
private val method: HttpMethod,
private val body: HttpRequestBody,
) : HttpRequestBody {
private val charset by lazy { parseCharset(body.contentType()) }
override fun writeTo(outputStream: OutputStream) {
val loggingOutputStream = LoggingOutputStream(outputStream, charset)
body.writeTo(loggingOutputStream)
loggingOutputStream.flush()
System.err.println("--> END $method (${loggingOutputStream.writeCount()}-byte body)")
System.err.println()
}
override fun contentType(): String? = body.contentType()
override fun contentLength(): Long = body.contentLength()
override fun repeatable(): Boolean = body.repeatable()
override fun close() = body.close()
}
/**
* An [OutputStream] wrapper that delegates to [outputStream] while also logging bytes line by line
* as it's written to.
*
* The written content is assumed to be in the given [charset] and the logging occurs in a streaming
* manner with minimal buffering.
*/
private class LoggingOutputStream(private val outputStream: OutputStream, charset: Charset?) :
OutputStream() {
private val buffer = LoggingBuffer(charset)
fun writeCount() = buffer.writeCount()
override fun write(b: Int) {
outputStream.write(b)
buffer.write(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
outputStream.write(b, off, len)
for (i in off until off + len) {
buffer.write(b[i].toInt() and 0xFF)
}
}
/** Prints any currently buffered content. */
override fun flush() {
buffer.flush()
outputStream.flush()
}
override fun close() = outputStream.close()
}
/**
* An [HttpResponse] wrapper that delegates to [response] while also logging line-by-line as it's
* read.
*
* The logging occurs in a streaming manner with minimal buffering.
*/
private class LoggingHttpResponse(private val response: HttpResponse) : HttpResponse {
private val loggingBody: Lazy<InputStream> = lazy {
LoggingInputStream(
response.body(),
parseCharset(response.headers().values("Content-Type").firstOrNull()),
)
}
override fun statusCode(): Int = response.statusCode()
override fun headers(): Headers = response.headers()
override fun body(): InputStream = loggingBody.value
override fun close() {
if (loggingBody.isInitialized()) {
loggingBody.value.close()
}
response.close()
}
}
/**
* An [InputStream] wrapper that delegates to [inputStream] while also logging bytes line by line as
* it's read.
*
* The contents of [inputStream] are assumed to be in the given [charset] and the logging occurs in
* a streaming manner with minimal buffering.
*/
private class LoggingInputStream(private val inputStream: InputStream, charset: Charset?) :
InputStream() {
private var isDone = false
private val buffer = LoggingBuffer(charset)
override fun read(): Int {
if (isDone) {
return -1
}
val b = inputStream.read()
if (b == -1) {
markDone()
return b
}
buffer.write(b)
return b
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
if (isDone) {
return -1
}
val bytesRead = inputStream.read(b, off, len)
if (bytesRead == -1) {
markDone()
return bytesRead
}
for (i in off until off + bytesRead) {
buffer.write(b[i].toInt() and 0xFF)
}
return bytesRead
}
override fun close() {
if (!isDone) {
markDone(closedEarly = true)
}
inputStream.close()
}
private fun markDone(closedEarly: Boolean = false) {
isDone = true
buffer.flush()
val suffix = if (closedEarly) ", closed early" else ""
System.err.println("<-- END HTTP (${buffer.writeCount()}-byte body$suffix)")
System.err.println()
}
}
/**
* A byte buffer that prints line by line, using the given [charset], as bytes are written to it.
*
* When [charset] is `null`, the buffer performs an upfront check to detect binary content. If
* non-whitespace ISO control characters are found in the first [PROBABLY_UTF8_CODE_POINT_LIMIT]
* code points, body logging is suppressed entirely.
*/
private class LoggingBuffer(charset: Charset?) {
private val charset = charset ?: StandardCharsets.UTF_8
private val decoder: CharsetDecoder =
this.charset
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
private var writeCount = 0
private val buffer = ByteArrayOutputStream(128)
/**
* Whether logging has been suppressed because the content doesn't appear to be readable text.
*
* This is only set when [charset] is `null` and the content fails the [isProbablyUtf8] check.
*/
private var suppressed = false
/**
* Bytes accumulated for the [isProbablyUtf8] check before any lines are printed.
*
* Once the check passes (or [charset] is non-null), this is set to `null` and bytes flow
* directly to [buffer].
*/
private var prefetchBuffer: ByteArrayOutputStream? =
if (charset != null) null else ByteArrayOutputStream(128)
fun writeCount() = writeCount
fun write(b: Int) {
if (writeCount == 0) {
// Print a newline before we start printing anything to separate the printed content
// from previous content.
System.err.println()
}
writeCount++
if (suppressed) {
return
}
val prefetch = prefetchBuffer
if (prefetch != null) {
prefetch.write(b)
// Continue accumulating until we have enough bytes to decide.
if (prefetch.size() < PROBABLY_UTF8_BYTE_LIMIT && b != '\n'.code) {
return
}
// We have enough bytes. Check if the content is probably UTF-8.
prefetchBuffer = null
val bytes = prefetch.toByteArray()
if (!isProbablyUtf8(bytes)) {
suppressed = true
System.err.println("(binary body omitted)")
return
}
// Content looks like UTF-8. Feed the accumulated bytes into the normal buffer.
for (byte in bytes) {
writeToBuffer(byte.toInt() and 0xFF)
}
return
}
writeToBuffer(b)
}
private fun writeToBuffer(b: Int) {
if (b == '\n'.code) {
flush()
return
}
buffer.write(b)
}
/** Prints any currently buffered content. */
fun flush() {
if (suppressed) {
return
}
// If we still have a prefetch buffer when flush is called (body was shorter than the
// limit), run the check now.
val prefetch = prefetchBuffer
if (prefetch != null) {
prefetchBuffer = null
val bytes = prefetch.toByteArray()
if (bytes.isEmpty()) {
return
}
if (!isProbablyUtf8(bytes)) {
suppressed = true
System.err.println("(binary body omitted)")
return
}
for (byte in bytes) {
writeToBuffer(byte.toInt() and 0xFF)
}
}
if (buffer.size() == 0) {
return
}
val line =
try {
decoder.decode(ByteBuffer.wrap(buffer.toByteArray()))
} catch (e: CharacterCodingException) {
"(omitted line is not valid $charset)"
}
buffer.reset()
System.err.println(line)
}
}
/** The maximum number of code points to sample when checking if content is probably UTF-8. */
private const val PROBABLY_UTF8_CODE_POINT_LIMIT = 64
/**
* The maximum number of bytes to accumulate before running the [isProbablyUtf8] check. UTF-8 code
* points are at most 4 bytes, so this accommodates [PROBABLY_UTF8_CODE_POINT_LIMIT] code points.
*/
private const val PROBABLY_UTF8_BYTE_LIMIT = PROBABLY_UTF8_CODE_POINT_LIMIT * 4
/**
* Returns `true` if the given [bytes] probably contain human-readable UTF-8 text.
*
* Decodes up to [PROBABLY_UTF8_CODE_POINT_LIMIT] code points and returns `false` if any
* non-whitespace ISO control characters are found, or if the bytes are not valid UTF-8.
*/
private fun isProbablyUtf8(bytes: ByteArray): Boolean {
try {
val decoder =
StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
val charBuffer = decoder.decode(ByteBuffer.wrap(bytes))
var codePointCount = 0
var i = 0
while (i < charBuffer.length && codePointCount < PROBABLY_UTF8_CODE_POINT_LIMIT) {
val codePoint = Character.codePointAt(charBuffer, i)
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false
}
i += Character.charCount(codePoint)
codePointCount++
}
return true
} catch (e: CharacterCodingException) {
return false
}
}
/** Returns the [Charset] in the given [contentType] string, or `null` if unspecified. */
private fun parseCharset(contentType: String?): Charset? =
contentType
?.split(";")
?.drop(1)
?.map { it.trim() }
?.firstOrNull { it.startsWith("charset=", ignoreCase = true) }
?.substringAfter("=")
?.trim()
?.removeSurrounding("\"")
?.let { runCatching { charset(it) }.getOrNull() }
/** Formats the [Duration] into a string like "1m 40s 467ms". */
private fun Duration.format(): String =
toKotlinDuration().toComponents { days, hours, minutes, seconds, nanoseconds ->
buildString {
val milliseconds = nanoseconds / 1_000_000
if (days > 0) {
append("${days}d")
}
if (hours > 0) {
if (isNotEmpty()) {
append(" ")
}
append("${hours}h")
}
if (minutes > 0) {
if (isNotEmpty()) {
append(" ")
}
append("${minutes}m")
}
if (seconds > 0) {
if (isNotEmpty()) {
append(" ")
}
append("${seconds}s")
}
if (milliseconds > 0) {
if (isNotEmpty()) {
append(" ")
}
append("${milliseconds}ms")
}
if (isEmpty()) {
append("0s")
}
}
}
@@ -0,0 +1,59 @@
package com.langchain.smith.core.http
import java.net.Proxy
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.Base64
import java.util.Optional
/**
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication Required`.
*
* Implementations inspect the 407 [response] (typically its `Proxy-Authenticate` header) and return
* the request to retry with a `Proxy-Authorization` header set, or [Optional.empty] to abandon
* authentication and surface the 407 to the caller.
*
* Implementations must be thread-safe; they may be invoked concurrently from multiple HTTP calls.
*/
fun interface ProxyAuthenticator {
/**
* @param proxy the proxy that produced the challenge, or [Proxy.NO_PROXY] if the route is not
* yet established
* @param request the request that produced [response]
* @param response the 407 challenge response
* @return the retry request to send (typically [request] with a `Proxy-Authorization` header
* added), or [Optional.empty] to abandon authentication
*/
fun authenticate(
proxy: Proxy,
request: HttpRequest,
response: HttpResponse,
): Optional<HttpRequest>
companion object {
/**
* A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the ISO-8859-1
* charset.
*/
@JvmStatic
fun basic(username: String, password: String): ProxyAuthenticator =
basic(username, password, StandardCharsets.ISO_8859_1)
/**
* A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the given [charset].
*/
@JvmStatic
fun basic(username: String, password: String, charset: Charset): ProxyAuthenticator {
val token =
Base64.getEncoder().encodeToString("$username:$password".toByteArray(charset))
val headerValue = "Basic $token"
return ProxyAuthenticator { _, request, _ ->
Optional.of(
request.toBuilder().putHeader("Proxy-Authorization", headerValue).build()
)
}
}
}
}
@@ -43,27 +43,7 @@ private constructor(
is JsonBoolean -> put(key, value.value.toString())
is JsonNumber -> put(key, value.value.toString())
is JsonString -> put(key, value.value)
is JsonArray ->
put(
key,
value.values
.asSequence()
.mapNotNull {
when (it) {
is JsonMissing,
is JsonNull -> null
is JsonBoolean -> it.value.toString()
is JsonNumber -> it.value.toString()
is JsonString -> it.value
is JsonArray,
is JsonObject ->
throw IllegalArgumentException(
"Cannot comma separate non-primitives in query params"
)
}
}
.joinToString(","),
)
is JsonArray -> value.values.forEach { put(key, it) }
is JsonObject ->
value.values.forEach { (nestedKey, value) -> put("$key[$nestedKey]", value) }
}
@@ -1,3 +1,5 @@
// File generated from our OpenAPI spec by Stainless.
package com.langchain.smith.core.http
import com.langchain.smith.core.DefaultSleeper
@@ -31,10 +33,6 @@ private constructor(
) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
if (!isRetryable(request) || maxRetries <= 0) {
return httpClient.execute(request, requestOptions)
}
var modifiedRequest = maybeAddIdempotencyHeader(request)
// Don't send the current retry count in the headers if the caller set their own value.
@@ -48,6 +46,10 @@ private constructor(
modifiedRequest = setRetryCountHeader(modifiedRequest, retries)
}
if (!isRetryable(modifiedRequest)) {
return httpClient.execute(modifiedRequest, requestOptions)
}
val response =
try {
val response = httpClient.execute(modifiedRequest, requestOptions)
@@ -75,10 +77,6 @@ private constructor(
request: HttpRequest,
requestOptions: RequestOptions,
): CompletableFuture<HttpResponse> {
if (!isRetryable(request) || maxRetries <= 0) {
return httpClient.executeAsync(request, requestOptions)
}
val modifiedRequest = maybeAddIdempotencyHeader(request)
// Don't send the current retry count in the headers if the caller set their own value.
@@ -94,8 +92,12 @@ private constructor(
val requestWithRetryCount =
if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request
return httpClient
.executeAsync(requestWithRetryCount, requestOptions)
val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions)
if (!isRetryable(requestWithRetryCount)) {
return responseFuture
}
return responseFuture
.handleAsync(
fun(
response: HttpResponse?,
@@ -199,7 +201,7 @@ private constructor(
?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter ->
retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1))
?: try {
ChronoUnit.MILLIS.between(
ChronoUnit.NANOS.between(
OffsetDateTime.now(clock),
OffsetDateTime.parse(
retryAfter,
@@ -212,13 +214,8 @@ private constructor(
}
}
?.let { retryAfterNanos ->
// If the API asks us to wait a certain amount of time (and it's a reasonable
// amount), just
// do what it says.
val retryAfter = Duration.ofNanos(retryAfterNanos.toLong())
if (retryAfter in Duration.ofNanos(0)..Duration.ofMinutes(1)) {
return retryAfter
}
// If the API asks us to wait a certain amount of time, do what it says.
return Duration.ofNanos(retryAfterNanos.toLong())
}
// Apply exponential backoff, but not more than the max.
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class BadRequestException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("400: $body", cause) {
LangChainServiceException(formatMessage(400, body), cause) {
override fun statusCode(): Int = 400
@@ -14,7 +14,7 @@ private constructor(
private val headers: Headers,
private val body: JsonValue,
cause: Throwable?,
) : LangChainServiceException("$statusCode: $body", cause) {
) : LangChainServiceException(formatMessage(statusCode, body), cause) {
override fun statusCode(): Int = statusCode
@@ -2,6 +2,7 @@
package com.langchain.smith.errors
import com.fasterxml.jackson.databind.ObjectMapper
import com.langchain.smith.core.JsonValue
import com.langchain.smith.core.http.Headers
@@ -14,4 +15,19 @@ protected constructor(message: String, cause: Throwable? = null) :
abstract fun headers(): Headers
abstract fun body(): JsonValue
companion object {
/** Formats a status code and [JsonValue] error body into an error message. */
@JvmStatic
fun formatMessage(statusCode: Int, body: JsonValue): String {
val bodyStr =
try {
ObjectMapper().writeValueAsString(body)
} catch (_: Exception) {
body.toString()
}
return "$statusCode: $bodyStr"
}
}
}
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class NotFoundException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("404: $body", cause) {
LangChainServiceException(formatMessage(404, body), cause) {
override fun statusCode(): Int = 404
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class PermissionDeniedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("403: $body", cause) {
LangChainServiceException(formatMessage(403, body), cause) {
override fun statusCode(): Int = 403
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class RateLimitException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("429: $body", cause) {
LangChainServiceException(formatMessage(429, body), cause) {
override fun statusCode(): Int = 429
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class UnauthorizedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("401: $body", cause) {
LangChainServiceException(formatMessage(401, body), cause) {
override fun statusCode(): Int = 401
@@ -14,7 +14,7 @@ private constructor(
private val headers: Headers,
private val body: JsonValue,
cause: Throwable?,
) : LangChainServiceException("$statusCode: $body", cause) {
) : LangChainServiceException(formatMessage(statusCode, body), cause) {
override fun statusCode(): Int = statusCode
@@ -10,7 +10,7 @@ import kotlin.jvm.optionals.getOrNull
class UnprocessableEntityException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
LangChainServiceException("422: $body", cause) {
LangChainServiceException(formatMessage(422, body), cause) {
override fun statusCode(): Int = 422
@@ -0,0 +1,22 @@
package com.langchain.smith.evaluation
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
/** Pairwise score emitted by a [ComparativeEvaluator] for one or more runs. */
data class ComparisonEvaluationResult(
val key: String,
val scores: Map<String, Number>,
val comment: String? = null,
val sourceRunId: String? = null,
)
/** Compares runs from two or more experiments on the same dataset example. */
fun interface ComparativeEvaluator {
fun compareRuns(runs: List<RunIngest>, example: Example): ComparisonEvaluationResult
}
/** Creates a [ComparativeEvaluator] from a comparison function. */
fun comparativeEvaluator(
func: (List<RunIngest>, Example) -> ComparisonEvaluationResult
): ComparativeEvaluator = ComparativeEvaluator { runs, example -> func(runs, example) }
@@ -0,0 +1,22 @@
package com.langchain.smith.evaluation
import com.langchain.smith.models.datasets.comparative.ComparativeCreateResponse
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
/** One row from a comparative evaluation for a shared dataset example. */
data class ComparativeResultRow(
val example: Example,
val runs: List<RunIngest>,
val evaluationResults: Map<String, ComparisonEvaluationResult>,
)
/** Results returned by [evaluateComparative]. */
class ComparativeExperimentResults
internal constructor(
val rows: List<ComparativeResultRow>,
val comparativeExperiment: ComparativeCreateResponse?,
val url: String?,
) {
override fun toString(): String = "ComparativeExperimentResults{rows=${rows.size}, url=$url}"
}
@@ -0,0 +1,98 @@
package com.langchain.smith.evaluation
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
/**
* Wraps a function as a [RunEvaluator], normalizing common evaluator signatures to `(Run,
* Example?)`.
*/
class DynamicRunEvaluator
private constructor(private val evaluator: (RunIngest, Example?) -> EvaluatorOutput) :
RunEvaluator {
override val feedbackKeys: List<String> = emptyList()
override fun evaluateRun(
run: RunIngest,
example: Example?,
evaluatorRunId: String?,
): EvaluationOutput {
val sourceRunId = evaluatorRunId ?: newEvaluatorRunId()
val raw = evaluator(run, example)
return coerceEvaluatorOutput(raw, sourceRunId, defaultKey = "evaluator")
}
override fun toString(): String = "DynamicRunEvaluator{feedbackKeys=$feedbackKeys}"
companion object {
internal fun fromRunExample(
func: (RunIngest, Example?) -> EvaluatorOutput
): DynamicRunEvaluator = DynamicRunEvaluator(func)
internal fun fromOutputsReference(
func:
(outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>) -> EvaluatorOutput
): DynamicRunEvaluator = DynamicRunEvaluator { run, example ->
func(runOutputsMap(run), exampleOutputs(example))
}
internal fun fromInputsOutputsReference(
func:
(
inputs: Map<String, Any?>,
outputs: Map<String, Any?>,
referenceOutputs: Map<String, Any?>,
) -> EvaluatorOutput
): DynamicRunEvaluator = DynamicRunEvaluator { run, example ->
func(exampleInputs(example), runOutputsMap(run), exampleOutputs(example))
}
}
}
/** Creates a [RunEvaluator] from a function that receives the full [Run] and [Example]. */
fun runEvaluator(func: (RunIngest, Example?) -> EvaluatorOutput): RunEvaluator =
DynamicRunEvaluator.fromRunExample(func)
/** Creates a [RunEvaluator] from a function that receives model outputs and reference outputs. */
@JvmName("runEvaluatorFromOutputsReference")
fun runEvaluator(
func: (outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>) -> EvaluatorOutput
): RunEvaluator = DynamicRunEvaluator.fromOutputsReference(func)
/**
* Creates a [RunEvaluator] from a function that receives inputs, outputs, and reference outputs.
*/
@JvmName("runEvaluatorFromInputsOutputsReference")
fun runEvaluator(
func:
(
inputs: Map<String, Any?>,
outputs: Map<String, Any?>,
referenceOutputs: Map<String, Any?>,
) -> EvaluatorOutput
): RunEvaluator = DynamicRunEvaluator.fromInputsOutputsReference(func)
/** We pass the full run and dataset row for evaluators that need trace-level details. */
fun interface RunExampleEvaluator {
fun evaluate(run: RunIngest, example: Example?): EvaluatorOutput
}
/**
* Wraps a [RunExampleEvaluator] so the evaluate runner can call it like any other [RunEvaluator].
*/
fun runEvaluator(func: RunExampleEvaluator): RunEvaluator =
DynamicRunEvaluator.fromRunExample(func::evaluate)
/** We pass the app's answer and expected answer for simple match-style evaluators. */
fun interface OutputReferenceEvaluator {
fun evaluate(outputs: Map<String, Any?>, referenceOutputs: Map<String, Any?>): EvaluatorOutput
}
/**
* Wraps an [OutputReferenceEvaluator] so the evaluate runner can call it like any other
* [RunEvaluator].
*/
@JvmName("runEvaluatorFromOutputReferenceEvaluator")
fun runEvaluator(func: OutputReferenceEvaluator): RunEvaluator =
DynamicRunEvaluator.fromOutputsReference(func::evaluate)
@@ -0,0 +1,165 @@
@file:JvmName("Evaluation")
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.function.Function
private val evaluateExecutor =
Executors.newCachedThreadPool { runnable ->
Thread(runnable, "langsmith-evaluate").apply { isDaemon = true }
}
/**
* Runs [target] on a dataset, traces each prediction, scores it with [params.evaluators], and
* uploads feedback to LangSmith.
*
* @param blocking When `true` (default), waits for the evaluation to finish before returning. When
* `false`, use [evaluateAsync] instead — Java cannot return in-progress [ExperimentResults].
*/
@JvmOverloads
fun evaluate(
client: LangsmithClient,
target: (Map<String, Any?>) -> Any?,
params: EvaluateParams,
blocking: Boolean = true,
): ExperimentResults {
if (!blocking) {
throw IllegalArgumentException(
"Non-blocking evaluation requires evaluateAsync(). " +
"Call evaluateAsync(client, target, params) instead."
)
}
return EvaluateRunner(client, params).run(target)
}
/** Java-friendly overload using [Function]. */
@JvmOverloads
fun evaluate(
client: LangsmithClient,
target: Function<Map<String, Any?>, Any?>,
params: EvaluateParams,
blocking: Boolean = true,
): ExperimentResults = evaluate(client, { inputs -> target.apply(inputs) }, params, blocking)
/** Runs an [EvaluateTarget] (Runnable-style) on a dataset. */
@JvmOverloads
fun evaluate(
client: LangsmithClient,
target: EvaluateTarget,
params: EvaluateParams,
blocking: Boolean = true,
): ExperimentResults = evaluate(client, target::invoke, params, blocking)
/**
* Runs [target] asynchronously. Equivalent to Python `evaluate(..., blocking=False)` — call
* [CompletableFuture.join] or [CompletableFuture.get] when results are needed.
*/
fun evaluateAsync(
client: LangsmithClient,
target: (Map<String, Any?>) -> Any?,
params: EvaluateParams,
): CompletableFuture<ExperimentResults> =
CompletableFuture.supplyAsync({ EvaluateRunner(client, params).run(target) }, evaluateExecutor)
/** Async overload for [EvaluateTarget]. */
fun evaluateAsync(
client: LangsmithClient,
target: EvaluateTarget,
params: EvaluateParams,
): CompletableFuture<ExperimentResults> = evaluateAsync(client, target::invoke, params)
/** Async overload for Java [Function]. */
fun evaluateAsync(
client: LangsmithClient,
target: Function<Map<String, Any?>, Any?>,
params: EvaluateParams,
): CompletableFuture<ExperimentResults> =
evaluateAsync(client, { inputs -> target.apply(inputs) }, params)
/**
* Evaluates an existing experiment's runs without re-running the target. Mirrors Python
* `evaluate_existing` and `evaluate(existing_experiment_id, ...)`.
*/
@JvmOverloads
fun evaluateExisting(
client: LangsmithClient,
experimentId: String,
params: EvaluateExistingParams,
blocking: Boolean = true,
): ExperimentResults {
if (!blocking) {
throw IllegalArgumentException(
"Non-blocking evaluation requires evaluateExistingAsync(). " +
"Call evaluateExistingAsync(client, experimentId, params) instead."
)
}
return EvaluateExistingRunner(client, experimentId, params).run()
}
fun evaluateExistingAsync(
client: LangsmithClient,
experimentId: String,
params: EvaluateExistingParams,
): CompletableFuture<ExperimentResults> =
CompletableFuture.supplyAsync(
{ EvaluateExistingRunner(client, experimentId, params).run() },
evaluateExecutor,
)
/**
* Compares two existing experiments with pairwise evaluators. Mirrors Python `evaluate_comparative`
* and `evaluate((experiment_a, experiment_b), ...)`.
*/
fun evaluateComparative(
client: LangsmithClient,
experiments: Pair<String, String>,
params: EvaluateComparativeParams,
): ComparativeExperimentResults = EvaluateComparativeRunner(client, experiments, params).run()
/** Convenience overload accepting experiment ids as a list. */
fun evaluateComparative(
client: LangsmithClient,
experiments: List<String>,
params: EvaluateComparativeParams,
): ComparativeExperimentResults {
require(experiments.size == 2) {
"Comparative evaluation requires exactly 2 experiments, got ${experiments.size}"
}
return evaluateComparative(client, experiments[0] to experiments[1], params)
}
/**
* Re-scores an existing experiment using evaluators from [EvaluateParams]. Convenience wrapper
* around [evaluateExisting] for callers that already hold [EvaluateParams].
*/
@JvmOverloads
fun evaluate(
client: LangsmithClient,
experimentId: String,
params: EvaluateParams,
blocking: Boolean = true,
): ExperimentResults =
evaluateExisting(
client,
experimentId,
EvaluateExistingParams.builder()
.evaluators(params.evaluators)
.summaryEvaluators(params.summaryEvaluators)
.metadata(params.metadata)
.maxConcurrency(params.maxConcurrency)
.build(),
blocking,
)
/**
* Compares two existing experiments. Convenience wrapper when callers pass a pair of session ids as
* the evaluation target (Python tuple-target style).
*/
fun evaluate(
client: LangsmithClient,
experiments: Pair<String, String>,
params: EvaluateComparativeParams,
): ComparativeExperimentResults = evaluateComparative(client, experiments, params)
@@ -0,0 +1,63 @@
package com.langchain.smith.evaluation
/** Configuration for [evaluateComparative]. */
class EvaluateComparativeParams
private constructor(
val evaluators: List<ComparativeEvaluator>,
val experimentPrefix: String?,
val description: String?,
val metadata: Map<String, Any>,
val maxConcurrency: Int,
val loadNested: Boolean,
val randomizeOrder: Boolean,
) {
companion object {
@JvmStatic fun builder() = Builder()
}
class Builder internal constructor() {
private var evaluators: MutableList<ComparativeEvaluator> = mutableListOf()
private var experimentPrefix: String? = null
private var description: String? = null
private var metadata: Map<String, Any> = emptyMap()
private var maxConcurrency: Int = 5
private var loadNested: Boolean = false
private var randomizeOrder: Boolean = false
fun addEvaluator(evaluator: ComparativeEvaluator) = apply { evaluators.add(evaluator) }
fun evaluators(evaluators: List<ComparativeEvaluator>) = apply {
this.evaluators = evaluators.toMutableList()
}
fun experimentPrefix(experimentPrefix: String) = apply {
this.experimentPrefix = experimentPrefix
}
fun description(description: String) = apply { this.description = description }
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
fun maxConcurrency(maxConcurrency: Int) = apply {
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
this.maxConcurrency = maxConcurrency
}
fun loadNested(loadNested: Boolean) = apply { this.loadNested = loadNested }
fun randomizeOrder(randomizeOrder: Boolean) = apply { this.randomizeOrder = randomizeOrder }
fun build(): EvaluateComparativeParams {
require(evaluators.isNotEmpty()) { "at least one comparative evaluator is required" }
return EvaluateComparativeParams(
evaluators = evaluators.toList(),
experimentPrefix = experimentPrefix,
description = description,
metadata = metadata,
maxConcurrency = maxConcurrency,
loadNested = loadNested,
randomizeOrder = randomizeOrder,
)
}
}
}
@@ -0,0 +1,192 @@
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.datasets.comparative.ComparativeCreateParams
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
import com.langchain.smith.models.sessions.TracerSession
import java.util.UUID
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.jvm.optionals.getOrNull
/** Runs pairwise evaluators over two existing experiments. */
internal class EvaluateComparativeRunner(
private val client: LangsmithClient,
private val experimentIds: Pair<String, String>,
private val params: EvaluateComparativeParams,
) {
private val logger = Logger.getLogger(EvaluateComparativeRunner::class.java.name)
fun run(): ComparativeExperimentResults {
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
val projects =
listOf(
loadExperiment(client, experimentIds.first),
loadExperiment(client, experimentIds.second),
)
val datasetIds =
projects.map { project ->
project.referenceDatasetId().orElseThrow {
IllegalStateException("Experiment ${project.id()} has no reference dataset")
}
}
require(datasetIds.distinct().size == 1) {
"All experiments must have the same reference dataset"
}
val datasetId = datasetIds.first()
val experimentName = resolveComparativeName(projects)
val comparativeExperimentId = UUID.randomUUID().toString()
val comparativeExperiment =
client
.datasets()
.comparative()
.create(
ComparativeCreateParams.builder()
.id(comparativeExperimentId)
.name(experimentName)
.description(params.description)
.experimentIds(projects.map { it.id() })
.referenceDatasetId(datasetId)
.apply {
if (params.metadata.isNotEmpty()) {
val extraBuilder = ComparativeCreateParams.Extra.builder()
params.metadata.forEach { (key, value) ->
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
}
extra(extraBuilder.build())
}
}
.build()
)
val runsByExperiment =
projects.map { project ->
loadRunsForExperiment(client, project.id(), params.loadNested)
}
val exampleIntersection =
runsByExperiment
.map { runs ->
runs.mapNotNull { run -> run.referenceExampleId().getOrNull() }.toSet()
}
.reduce { acc, ids -> acc.intersect(ids) }
val examplesById =
loadExamplesByIds(
client,
datasetId,
exampleIntersection.toList(),
experimentDatasetVersion(projects.first()),
)
val runsByExample = linkedMapOf<String, MutableList<RunIngest>>()
runsByExperiment.forEach { runs ->
runs.forEach { run ->
val exampleId = run.referenceExampleId().getOrNull() ?: return@forEach
if (exampleId !in exampleIntersection) {
return@forEach
}
runsByExample.getOrPut(exampleId) { mutableListOf() }.add(run)
}
}
val rows = compareExamples(runsByExample, examplesById, comparativeExperiment.id())
val url = buildComparativeUrl(projects, comparativeExperiment.id(), datasetId)
return ComparativeExperimentResults(
rows = rows,
comparativeExperiment = comparativeExperiment,
url = url,
)
}
private fun resolveComparativeName(projects: List<TracerSession>): String {
params.experimentPrefix?.let { prefix ->
return "$prefix-${UUID.randomUUID().toString().take(8)}"
}
val names = projects.mapNotNull { it.name().orElse(null) }
return if (names.size == projects.size) {
names.joinToString(" vs. ") + "-" + UUID.randomUUID().toString().take(4)
} else {
"comparative-${UUID.randomUUID().toString().take(8)}"
}
}
private fun compareExamples(
runsByExample: Map<String, List<RunIngest>>,
examplesById: Map<String, Example>,
comparativeExperimentId: String,
): List<ComparativeResultRow> {
if (params.maxConcurrency == 0 || runsByExample.size <= 1) {
return runsByExample.mapNotNull { (exampleId, runs) ->
val example = examplesById[exampleId] ?: return@mapNotNull null
compareExample(example, runs, comparativeExperimentId)
}
}
val poolSize = minOf(params.maxConcurrency, runsByExample.size)
val executor = Executors.newFixedThreadPool(poolSize)
return try {
runsByExample
.map { (exampleId, runs) ->
executor.submit(
Callable {
val example = examplesById[exampleId] ?: return@Callable null
compareExample(example, runs, comparativeExperimentId)
}
)
}
.mapNotNull { it.get() }
} finally {
executor.shutdown()
}
}
private fun compareExample(
example: Example,
runs: List<RunIngest>,
comparativeExperimentId: String,
): ComparativeResultRow {
val orderedRuns =
if (params.randomizeOrder) {
runs.shuffled()
} else {
runs
}
val evaluationResults = linkedMapOf<String, ComparisonEvaluationResult>()
for (evaluator in params.evaluators) {
try {
val result = evaluator.compareRuns(orderedRuns, example)
evaluationResults["feedback.${result.key}"] = result
val feedbackGroupId = UUID.randomUUID().toString()
logComparativeEvaluationFeedback(
client,
result,
orderedRuns,
comparativeExperimentId,
feedbackGroupId,
)
} catch (e: Exception) {
logger.log(
Level.SEVERE,
"Error running comparative evaluator $evaluator on example ${example.id()}",
e,
)
}
}
return ComparativeResultRow(
example = example,
runs = orderedRuns,
evaluationResults = evaluationResults,
)
}
}
@@ -0,0 +1,10 @@
package com.langchain.smith.evaluation
/** How to handle errors when the target function fails on a dataset row. */
enum class EvaluateErrorHandling {
/** Log the error and include the run in the experiment (default). */
LOG,
/** Log the error and omit the run from the experiment. */
IGNORE,
}
@@ -0,0 +1,59 @@
package com.langchain.smith.evaluation
/** Configuration for [evaluateExisting]. */
class EvaluateExistingParams
private constructor(
val evaluators: List<RunEvaluator>,
val summaryEvaluators: List<SummaryEvaluator>,
val metadata: Map<String, Any>,
val maxConcurrency: Int,
val loadNested: Boolean,
) {
companion object {
@JvmStatic fun builder() = Builder()
}
class Builder internal constructor() {
private var evaluators: MutableList<RunEvaluator> = mutableListOf()
private var summaryEvaluators: MutableList<SummaryEvaluator> = mutableListOf()
private var metadata: Map<String, Any> = emptyMap()
private var maxConcurrency: Int = 0
private var loadNested: Boolean = false
fun addEvaluator(evaluator: RunEvaluator) = apply { evaluators.add(evaluator) }
fun evaluators(evaluators: List<RunEvaluator>) = apply {
this.evaluators = evaluators.toMutableList()
}
fun addSummaryEvaluator(evaluator: SummaryEvaluator) = apply {
summaryEvaluators.add(evaluator)
}
fun summaryEvaluators(evaluators: List<SummaryEvaluator>) = apply {
summaryEvaluators = evaluators.toMutableList()
}
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
fun maxConcurrency(maxConcurrency: Int) = apply {
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
this.maxConcurrency = maxConcurrency
}
fun loadNested(loadNested: Boolean) = apply { this.loadNested = loadNested }
fun build(): EvaluateExistingParams {
require(evaluators.isNotEmpty() || summaryEvaluators.isNotEmpty()) {
"at least one evaluator or summary evaluator is required"
}
return EvaluateExistingParams(
evaluators = evaluators.toList(),
summaryEvaluators = summaryEvaluators.toList(),
metadata = metadata,
maxConcurrency = maxConcurrency,
loadNested = loadNested,
)
}
}
}
@@ -0,0 +1,140 @@
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.jvm.optionals.getOrNull
/** Scores existing experiment runs without re-running the target. */
internal class EvaluateExistingRunner(
private val client: LangsmithClient,
private val experimentId: String,
private val params: EvaluateExistingParams,
) {
private val logger = Logger.getLogger(EvaluateExistingRunner::class.java.name)
fun run(): ExperimentResults {
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0 (0 = sequential)" }
val session = loadExperiment(client, experimentId)
val experimentName = session.name().orElse(experimentId)
val datasetId =
session.referenceDatasetId().orElseThrow {
IllegalStateException("Experiment $experimentId has no reference dataset")
}
val runs = loadRunsForExperiment(client, experimentId, params.loadNested)
require(runs.isNotEmpty()) { "No runs found for experiment $experimentId" }
val exampleIds = runs.mapNotNull { run -> run.referenceExampleId().getOrNull() }.distinct()
val examplesById =
loadExamplesByIds(client, datasetId, exampleIds, experimentDatasetVersion(session))
val rows = scoreRuns(runs, examplesById)
val summaryResults = applySummaryEvaluators(rows, experimentId)
val url = buildExperimentUrl(session, datasetId)
return ExperimentResults(
experimentName = experimentName,
experimentId = experimentId,
datasetId = datasetId,
rows = rows,
summaryResults = summaryResults,
url = url,
)
}
private fun scoreRuns(
runs: List<RunIngest>,
examplesById: Map<String, Example>,
): List<ExperimentResultRow> {
if (params.maxConcurrency == 0 || runs.size <= 1) {
return runs.mapNotNull { run -> scoreRun(run, examplesById) }
}
val poolSize = minOf(params.maxConcurrency, runs.size)
val executor = Executors.newFixedThreadPool(poolSize)
return try {
runs
.map { run -> executor.submit(Callable { scoreRun(run, examplesById) }) }
.mapNotNull { it.get() }
} finally {
executor.shutdown()
}
}
private fun scoreRun(run: RunIngest, examplesById: Map<String, Example>): ExperimentResultRow? {
val exampleId = run.referenceExampleId().getOrNull() ?: return null
val example = examplesById[exampleId] ?: return null
val evaluationResults =
params.evaluators.flatMap { evaluator -> runEvaluator(evaluator, run, example) }
return ExperimentResultRow(
run = run,
example = example,
evaluationResults = EvaluationResults(evaluationResults),
)
}
private fun runEvaluator(
evaluator: RunEvaluator,
run: RunIngest,
example: Example,
): List<EvaluationResult> {
val evaluatorRunId = newEvaluatorRunId()
return try {
val response = evaluator.evaluateRun(run, example, evaluatorRunId)
logEvaluationFeedback(client, response, run)
selectEvaluationResults(response)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running evaluator $evaluator on run ${run.id()}", e)
val keys = evaluator.feedbackKeys
if (keys.isEmpty()) {
throw e
}
val errorResponse =
EvaluationResults(
keys.map { key ->
EvaluationResult(
key = key,
sourceRunId = evaluatorRunId,
comment = e.toString(),
extra = mapOf("error" to true),
)
}
)
logEvaluationFeedback(client, errorResponse, run)
errorResponse.results
}
}
private fun applySummaryEvaluators(
rows: List<ExperimentResultRow>,
sessionId: String,
): EvaluationResults {
if (params.summaryEvaluators.isEmpty()) {
return EvaluationResults(emptyList())
}
val runs = rows.map { it.run }
val examples = rows.map { it.example }
val aggregateResults = mutableListOf<EvaluationResult>()
for (evaluator in params.summaryEvaluators) {
try {
val response = evaluator.evaluate(runs, examples)
aggregateResults.addAll(selectEvaluationResults(response))
logSummaryEvaluationFeedback(client, response, sessionId)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running summary evaluator $evaluator", e)
}
}
return EvaluationResults(aggregateResults)
}
}
@@ -0,0 +1,122 @@
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.feedback.FeedbackCreateSchema
import com.langchain.smith.models.feedback.ModelFeedbackSource
import com.langchain.smith.models.runs.RunIngest
import java.time.OffsetDateTime
import kotlin.jvm.optionals.getOrNull
/** Turns an evaluator response into a flat list of [EvaluationResult]. */
internal fun selectEvaluationResults(output: EvaluationOutput): List<EvaluationResult> =
when (output) {
is EvaluationResult -> listOf(output)
is EvaluationResults -> output.results
}
/** Posts evaluator feedback for a prediction run. */
internal fun logEvaluationFeedback(
client: LangsmithClient,
evaluatorResponse: EvaluationOutput,
run: RunIngest,
) {
for (result in selectEvaluationResults(evaluatorResponse)) {
client.feedback().create(buildFeedbackCreateSchema(result, run))
}
}
private fun buildFeedbackCreateSchema(
result: EvaluationResult,
run: RunIngest,
): FeedbackCreateSchema {
val builder = FeedbackCreateSchema.builder().key(result.key)
val runId = result.targetRunId ?: run.id().getOrNull()
if (runId != null) {
builder.runId(runId)
}
builder.feedbackSource(
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
)
val score = result.score
if (score != null) {
builder.score(score.toDouble())
}
applyFeedbackValue(builder, result.value)
val comment = result.comment
if (comment != null) {
builder.comment(comment)
}
val feedbackConfig = result.feedbackConfig
if (feedbackConfig != null) {
builder.feedbackConfig(feedbackConfig)
}
run.traceId().getOrNull()?.let { builder.traceId(it) }
run.sessionId().getOrNull()?.let { builder.sessionId(it) }
run.startTime().getOrNull()?.let { startTime ->
runStartTime(startTime)?.let { builder.startTime(it) }
}
val correction = result.correction
if (correction != null) {
builder.correction(
FeedbackCreateSchema.Correction.ofUnionMember0(
FeedbackCreateSchema.Correction.UnionMember0.builder()
.putAllAdditionalProperties(
correction.mapValues { (_, value) -> JsonValue.from(value) }
)
.build()
)
)
}
val error = result.extra?.get("error") as? Boolean
if (error == true) {
builder.error(true)
}
return builder.build()
}
internal fun applyFeedbackValue(builder: FeedbackCreateSchema.Builder, value: Any?) {
when (value) {
null -> Unit
is Number ->
builder.value(
when (value) {
is Double -> value
is Float -> value.toDouble()
else -> value.toDouble()
}
)
is Boolean -> builder.value(value)
is String -> builder.value(value)
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST") val stringMap = value as Map<String, Any?>
builder.value(
FeedbackCreateSchema.Value.ofUnionMember3(
FeedbackCreateSchema.Value.UnionMember3.builder()
.putAllAdditionalProperties(
stringMap.mapValues { (_, v) -> JsonValue.from(v) }
)
.build()
)
)
}
else -> builder.value(value.toString())
}
}
internal fun runStartTime(startTime: String): OffsetDateTime? =
try {
OffsetDateTime.parse(startTime)
} catch (_: Exception) {
null
}
@@ -0,0 +1,143 @@
package com.langchain.smith.evaluation
import com.langchain.smith.models.examples.Example
/** Configuration for [evaluate]. */
class EvaluateParams
private constructor(
internal val data: EvaluateData,
val evaluators: List<RunEvaluator>,
val summaryEvaluators: List<SummaryEvaluator>,
val experimentPrefix: String,
val experimentName: String?,
val experimentId: String?,
val description: String?,
val metadata: Map<String, Any>,
val maxConcurrency: Int,
val numRepetitions: Int,
val uploadResults: Boolean,
val errorHandling: EvaluateErrorHandling,
) {
companion object {
/** Creates a new [Builder]. */
@JvmStatic fun builder() = Builder()
}
/** Builds [EvaluateParams] with sensible defaults. */
class Builder internal constructor() {
private var data: EvaluateData? = null
private var evaluators: MutableList<RunEvaluator> = mutableListOf()
private var summaryEvaluators: MutableList<SummaryEvaluator> = mutableListOf()
private var experimentPrefix: String = DEFAULT_EXPERIMENT_PREFIX
private var experimentName: String? = null
private var experimentId: String? = null
private var description: String? = null
private var metadata: Map<String, Any> = emptyMap()
private var maxConcurrency: Int = 0
private var numRepetitions: Int = 1
private var uploadResults: Boolean = true
private var errorHandling: EvaluateErrorHandling = EvaluateErrorHandling.LOG
/** Dataset name or ID whose examples will be evaluated. */
fun data(datasetNameOrId: String) = apply {
require(datasetNameOrId.isNotBlank()) { "datasetNameOrId must not be blank" }
data = EvaluateData.Dataset(datasetNameOrId)
}
/** In-memory examples to evaluate (for example after creating a dataset locally). */
fun data(examples: List<Example>) = apply {
require(examples.isNotEmpty()) { "examples must not be empty" }
data = EvaluateData.Examples(examples)
}
fun addEvaluator(evaluator: RunEvaluator) = apply { evaluators.add(evaluator) }
fun evaluators(evaluators: List<RunEvaluator>) = apply {
this.evaluators = evaluators.toMutableList()
}
fun addSummaryEvaluator(evaluator: SummaryEvaluator) = apply {
summaryEvaluators.add(evaluator)
}
fun summaryEvaluators(evaluators: List<SummaryEvaluator>) = apply {
summaryEvaluators = evaluators.toMutableList()
}
/** Prefix used when generating a unique experiment name. */
fun experimentPrefix(experimentPrefix: String) = apply {
this.experimentPrefix = experimentPrefix
}
/** Explicit experiment name; overrides [experimentPrefix] when set. */
fun experimentName(experimentName: String) = apply { this.experimentName = experimentName }
/**
* Existing experiment session id to extend with additional runs. When set, a new session is
* not created.
*/
fun experimentId(experimentId: String) = apply { this.experimentId = experimentId }
fun description(description: String) = apply { this.description = description }
fun metadata(metadata: Map<String, Any>) = apply { this.metadata = metadata }
/**
* Maximum number of examples to evaluate concurrently. `0` runs sequentially. Values
* greater than `0` use a fixed thread pool of that size.
*/
fun maxConcurrency(maxConcurrency: Int) = apply {
require(maxConcurrency >= 0) { "maxConcurrency must be >= 0" }
this.maxConcurrency = maxConcurrency
}
/** Number of times to evaluate each dataset example. */
fun numRepetitions(numRepetitions: Int) = apply {
require(numRepetitions >= 1) { "numRepetitions must be >= 1" }
this.numRepetitions = numRepetitions
}
/** When false, runs and feedback are kept local and not uploaded to LangSmith. */
fun uploadResults(uploadResults: Boolean) = apply { this.uploadResults = uploadResults }
fun errorHandling(errorHandling: EvaluateErrorHandling) = apply {
this.errorHandling = errorHandling
}
fun build(): EvaluateParams {
val resolvedData =
data
?: throw IllegalArgumentException(
"data is required (dataset name/id or examples)"
)
require(evaluators.isNotEmpty() || summaryEvaluators.isNotEmpty()) {
"at least one evaluator or summary evaluator is required"
}
require(experimentId == null || experimentName == null) {
"experimentId and experimentName cannot both be set"
}
return EvaluateParams(
data = resolvedData,
evaluators = evaluators.toList(),
summaryEvaluators = summaryEvaluators.toList(),
experimentPrefix = experimentPrefix,
experimentName = experimentName,
experimentId = experimentId,
description = description,
metadata = metadata,
maxConcurrency = maxConcurrency,
numRepetitions = numRepetitions,
uploadResults = uploadResults,
errorHandling = errorHandling,
)
}
}
}
internal sealed class EvaluateData {
data class Dataset(val identifier: String) : EvaluateData()
data class Examples(val examples: List<Example>) : EvaluateData()
}
internal const val DEFAULT_EXPERIMENT_PREFIX = "experiment"
@@ -0,0 +1,374 @@
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.runs.RunIngest
import com.langchain.smith.models.sessions.SessionCreateParams
import com.langchain.smith.models.sessions.SessionUpdateParams
import com.langchain.smith.models.sessions.TracerSessionWithoutVirtualFields
import com.langchain.smith.tracing.RunTree
import com.langchain.smith.tracing.TraceConfig
import com.langchain.smith.tracing.getCurrentRunTree
import com.langchain.smith.tracing.traceable
import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.logging.Logger
/** Runs your app on each dataset row, scores the results, and records everything in LangSmith. */
internal class EvaluateRunner(
private val client: LangsmithClient,
private val params: EvaluateParams,
) {
private val logger = Logger.getLogger(EvaluateRunner::class.java.name)
/** End-to-end evaluate flow: load rows, open an experiment, run and score each one. */
fun run(target: (Map<String, Any?>) -> Any?): ExperimentResults {
require(params.maxConcurrency >= 0) { "maxConcurrency must be >= 0 (0 = sequential)" }
val examples = expandExamples(loadExamples(), params.numRepetitions)
require(examples.isNotEmpty()) { "No examples found to evaluate" }
val datasetId = examples.first().datasetId()
val sessionInfo = resolveExperimentSession(examples.size, datasetId)
val experimentName = sessionInfo.first
val sessionId = sessionInfo.second
val rows =
evaluateExamples(
target = target,
examples = examples,
experimentName = experimentName,
sessionId = sessionId,
)
val summaryResults = applySummaryEvaluators(rows, sessionId)
if (params.uploadResults && sessionId != null) {
updateExperimentMetadata(sessionId, examples)
}
// LangSmith UI URLs are not exposed on the Java session schema yet.
val url: String? = null
return ExperimentResults(
experimentName = experimentName,
experimentId = sessionId,
datasetId = datasetId,
rows = rows,
summaryResults = summaryResults,
url = url,
)
}
private fun resolveExperimentSession(
numExamples: Int,
datasetId: String,
): Pair<String, String?> {
params.experimentId?.let { existingId ->
val session = loadExperiment(client, existingId)
val name = session.name().orElse(existingId)
return name to existingId
}
val experimentName =
params.experimentName ?: generateExperimentName(params.experimentPrefix)
if (!params.uploadResults) {
return experimentName to null
}
val session = createExperimentSession(experimentName, datasetId, numExamples)
return experimentName to session.id()
}
private fun evaluateExamples(
target: (Map<String, Any?>) -> Any?,
examples: List<Example>,
experimentName: String,
sessionId: String?,
): List<ExperimentResultRow> {
if (params.maxConcurrency == 0 || examples.size <= 1) {
return examples.mapNotNull { example ->
evaluateExample(
target = target,
example = example,
experimentName = experimentName,
sessionId = sessionId,
)
}
}
val poolSize = minOf(params.maxConcurrency, examples.size)
val executor = Executors.newFixedThreadPool(poolSize)
return try {
val futures =
examples.map { example ->
executor.submit(
Callable {
evaluateExample(
target = target,
example = example,
experimentName = experimentName,
sessionId = sessionId,
)
}
)
}
futures.mapNotNull { it.get() }
} finally {
executor.shutdown()
}
}
private fun loadExamples(): List<Example> =
when (val data = params.data) {
is EvaluateData.Dataset -> listExamples(client, data.identifier)
is EvaluateData.Examples -> data.examples
}
private fun createExperimentSession(
experimentName: String,
datasetId: String,
numExamples: Int,
): TracerSessionWithoutVirtualFields {
val sessionMetadata = buildMap {
putAll(params.metadata)
put("__ls_runner", "java_sdk_evaluate")
put("num_repetitions", params.numRepetitions)
}
val extraBuilder = SessionCreateParams.Extra.builder()
sessionMetadata.forEach { (key, value) ->
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
}
var candidateName = experimentName
repeat(MAX_NAME_COLLISION_RETRIES) {
try {
return client
.sessions()
.create(
SessionCreateParams.builder()
.name(candidateName)
.description(params.description)
.referenceDatasetId(datasetId)
.numExamples(numExamples.toLong())
.startTime(OffsetDateTime.now())
.extra(extraBuilder.build())
.build()
)
} catch (_: Exception) {
candidateName = "$experimentName-${UUID.randomUUID().toString().take(6)}"
}
}
throw IllegalStateException(
"Could not create experiment session after $MAX_NAME_COLLISION_RETRIES attempts"
)
}
private fun evaluateExample(
target: (Map<String, Any?>) -> Any?,
example: Example,
experimentName: String,
sessionId: String?,
): ExperimentResultRow? {
val inputs = exampleInputs(example)
val runMetadata = buildMap {
putAll(params.metadata)
put("example_version", exampleVersion(example))
}
val run =
if (params.uploadResults) {
runTracedTarget(target, inputs, example, experimentName, sessionId, runMetadata)
} else {
runLocalTarget(target, inputs, example)
} ?: return null
val evaluationResults =
params.evaluators.flatMap { evaluator -> runEvaluator(evaluator, run, example) }
return ExperimentResultRow(
run = run,
example = example,
evaluationResults = EvaluationResults(evaluationResults),
)
}
private fun runTracedTarget(
target: (Map<String, Any?>) -> Any?,
inputs: Map<String, Any?>,
example: Example,
experimentName: String,
sessionId: String?,
runMetadata: Map<String, Any>,
): RunIngest? {
var runTree: RunTree? = null
val wrappedTarget: (Map<String, Any?>) -> Any? = { exampleInputs ->
target(exampleInputs).also { runTree = getCurrentRunTree() }
}
val traced =
traceable(
wrappedTarget,
TraceConfig(
name = "Target",
client = client,
projectName = experimentName,
referenceExampleId =
if (params.errorHandling == EvaluateErrorHandling.LOG) example.id()
else null,
sessionId = sessionId,
metadata = runMetadata,
tracingEnabled = true,
),
)
try {
traced(inputs)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running target function", e)
if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
return null
}
}
return runTree?.buildRunData()
?: if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
null
} else {
throw IllegalStateException("Failed to capture run for example ${example.id()}")
}
}
private fun runLocalTarget(
target: (Map<String, Any?>) -> Any?,
inputs: Map<String, Any?>,
example: Example,
): RunIngest? {
return try {
val output = target(inputs)
val outputsBuilder = RunIngest.Outputs.builder()
when (output) {
is Map<*, *> ->
output.forEach { (key, value) ->
outputsBuilder.putAdditionalProperty(key.toString(), JsonValue.from(value))
}
else -> outputsBuilder.putAdditionalProperty("output", JsonValue.from(output))
}
RunIngest.builder()
.id(UUID.randomUUID().toString())
.referenceExampleId(example.id())
.outputs(outputsBuilder.build())
.build()
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running target function", e)
if (params.errorHandling == EvaluateErrorHandling.IGNORE) {
null
} else {
throw e
}
}
}
private fun runEvaluator(
evaluator: RunEvaluator,
run: RunIngest,
example: Example,
): List<EvaluationResult> {
val evaluatorRunId = newEvaluatorRunId()
return try {
val response = evaluator.evaluateRun(run, example, evaluatorRunId)
if (params.uploadResults) {
logEvaluationFeedback(client, response, run)
}
selectEvaluationResults(response)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running evaluator $evaluator on run ${run.id()}", e)
val keys = evaluator.feedbackKeys
if (keys.isEmpty()) {
throw e
}
val errorResponse =
EvaluationResults(
keys.map { key ->
EvaluationResult(
key = key,
sourceRunId = evaluatorRunId,
comment = e.toString(),
extra = mapOf("error" to true),
)
}
)
if (params.uploadResults) {
logEvaluationFeedback(client, errorResponse, run)
}
errorResponse.results
}
}
private fun applySummaryEvaluators(
rows: List<ExperimentResultRow>,
sessionId: String?,
): EvaluationResults {
if (params.summaryEvaluators.isEmpty()) {
return EvaluationResults(emptyList())
}
val runs = rows.map { it.run }
val examples = rows.map { it.example }
val aggregateResults = mutableListOf<EvaluationResult>()
for (evaluator in params.summaryEvaluators) {
try {
val response = evaluator.evaluate(runs, examples)
aggregateResults.addAll(selectEvaluationResults(response))
if (params.uploadResults && sessionId != null) {
logSummaryEvaluationFeedback(client, response, sessionId)
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error running summary evaluator $evaluator", e)
}
}
return EvaluationResults(aggregateResults)
}
private fun updateExperimentMetadata(sessionId: String, examples: List<Example>) {
try {
val projectMetadata = buildMap {
putAll(params.metadata)
put("dataset_version", datasetVersion(examples))
}
val extraBuilder = SessionUpdateParams.Extra.builder()
projectMetadata.forEach { (key, value) ->
extraBuilder.putAdditionalProperty(key, JsonValue.from(value))
}
client
.sessions()
.update(
sessionId,
SessionUpdateParams.builder().extra(extraBuilder.build()).build(),
)
} catch (e: Exception) {
logger.log(Level.WARNING, "Failed to update experiment metadata for $sessionId", e)
}
}
private fun datasetVersion(examples: List<Example>): String? {
val modifiedAt =
examples.mapNotNull { example ->
example.modifiedAt().orElseGet { example.createdAt().orElse(null) }
}
val maxModifiedAt = modifiedAt.maxOrNull() ?: return null
return maxModifiedAt.toString()
}
private companion object {
const val MAX_NAME_COLLISION_RETRIES = 10
}
}
@@ -0,0 +1,256 @@
package com.langchain.smith.evaluation
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.datasets.DatasetListParams
import com.langchain.smith.models.examples.Example
import com.langchain.smith.models.examples.ExampleListParams
import com.langchain.smith.models.feedback.FeedbackCreateSchema
import com.langchain.smith.models.feedback.ModelFeedbackSource
import com.langchain.smith.models.runs.RunIngest
import com.langchain.smith.models.runs.RunQueryParams
import com.langchain.smith.models.runs.RunSchema
import com.langchain.smith.models.sessions.TracerSession
import java.util.UUID
import kotlin.jvm.optionals.getOrNull
internal fun expandExamples(examples: List<Example>, numRepetitions: Int): List<Example> =
if (numRepetitions <= 1) {
examples
} else {
List(numRepetitions) { examples }.flatten()
}
internal fun resolveDatasetId(client: LangsmithClient, datasetIdentifier: String): String {
if (isUuid(datasetIdentifier)) {
return datasetIdentifier
}
val matches =
client.datasets().list(DatasetListParams.builder().name(datasetIdentifier).build()).items()
require(matches.isNotEmpty()) {
"No dataset found with name '$datasetIdentifier'. Create the dataset first or pass its UUID."
}
require(matches.size == 1) {
"Multiple datasets named '$datasetIdentifier'; pass the dataset UUID instead."
}
return matches.single().id()
}
internal fun listExamples(client: LangsmithClient, datasetIdentifier: String): List<Example> {
val datasetId = resolveDatasetId(client, datasetIdentifier)
return listDatasetExamples(client, datasetId)
}
internal fun listDatasetExamples(
client: LangsmithClient,
datasetId: String,
asOf: String? = null,
): List<Example> {
val builder = ExampleListParams.builder().dataset(datasetId)
asOf?.let { builder.asOf(it) }
return client.examples().list(builder.build()).autoPager().toList()
}
internal fun experimentDatasetVersion(session: TracerSession): String? {
val extra = session.extra().orElse(null) ?: return null
val props = extra._additionalProperties()
props["dataset_version"]?.convert(String::class.java)?.let {
return it
}
return props["metadata"]?.accept(
object : JsonValue.Visitor<String?> {
override fun visitObject(values: Map<String, JsonValue>): String? =
values["dataset_version"]?.convert(String::class.java)
override fun visitDefault(): String? = null
}
)
}
internal fun loadExperiment(client: LangsmithClient, experiment: String): TracerSession =
client.sessions().retrieve(experiment)
internal fun loadRunsForExperiment(
client: LangsmithClient,
sessionId: String,
loadNested: Boolean,
): List<RunIngest> {
val deadlineMs = System.currentTimeMillis() + RUN_QUERY_TIMEOUT_MS
var delayMs = RUN_QUERY_INITIAL_DELAY_MS
while (true) {
client.runs().flush()
val runs = queryRunsForExperiment(client, sessionId, loadNested)
if (runs.isNotEmpty() || System.currentTimeMillis() >= deadlineMs) {
return runs
}
Thread.sleep(delayMs)
delayMs = minOf(delayMs * 2, RUN_QUERY_MAX_DELAY_MS)
}
}
private fun queryRunsForExperiment(
client: LangsmithClient,
sessionId: String,
loadNested: Boolean,
): List<RunIngest> {
val paramsBuilder = RunQueryParams.builder().session(listOf(sessionId)).limit(100L)
if (!loadNested) {
paramsBuilder.isRoot(true)
}
return client.runs().query(paramsBuilder.build()).autoPager().toList().map {
it.toEvaluateRun()
}
}
internal fun RunSchema.toEvaluateRun(): RunIngest {
val builder = RunIngest.builder().id(id()).sessionId(sessionId()).traceId(traceId())
referenceExampleId().getOrNull()?.let { builder.referenceExampleId(it) }
startTime().getOrNull()?.let { builder.startTime(it.toString()) }
outputs().getOrNull()?.let { schemaOutputs ->
val outputsBuilder = RunIngest.Outputs.builder()
schemaOutputs._additionalProperties().forEach { (key, value) ->
outputsBuilder.putAdditionalProperty(key, value)
}
builder.outputs(outputsBuilder.build())
}
return builder.build()
}
internal fun loadExamplesByIds(
client: LangsmithClient,
datasetId: String,
exampleIds: List<String>,
asOf: String? = null,
): Map<String, Example> {
if (exampleIds.isEmpty()) {
return emptyMap()
}
val wanted = exampleIds.toSet()
return listDatasetExamples(client, datasetId, asOf)
.filter { it.id() in wanted }
.associateBy { it.id() }
}
internal fun buildExperimentUrl(
session: TracerSession?,
@Suppress("UNUSED_PARAMETER") datasetId: String,
): String? = null
internal fun buildComparativeUrl(
experiments: List<TracerSession>,
comparativeExperimentId: String,
datasetId: String,
): String? = null
internal fun isUuid(value: String): Boolean =
try {
UUID.fromString(value)
true
} catch (_: IllegalArgumentException) {
false
}
internal fun generateExperimentName(prefix: String): String =
"$prefix-${UUID.randomUUID().toString().take(8)}"
internal fun exampleVersion(example: Example): String {
val timestamp = example.modifiedAt().orElseGet { example.createdAt().orElse(null) }
return timestamp?.toString() ?: java.time.OffsetDateTime.now().toString()
}
internal fun logSummaryEvaluationFeedback(
client: LangsmithClient,
evaluatorResponse: EvaluationOutput,
projectId: String,
) {
for (result in selectEvaluationResults(evaluatorResponse)) {
client.feedback().create(buildProjectFeedbackCreateSchema(result, projectId))
}
}
internal fun logComparativeEvaluationFeedback(
client: LangsmithClient,
result: ComparisonEvaluationResult,
runs: List<RunIngest>,
comparativeExperimentId: String,
feedbackGroupId: String,
) {
val comments =
if (result.comment != null) {
result.scores.keys.associateWith { result.comment }
} else {
emptyMap()
}
val runsById = runs.associateBy { it.id().getOrNull().orEmpty() }
for ((runId, score) in result.scores) {
val run = runsById[runId]
val builder =
FeedbackCreateSchema.builder()
.key(result.key)
.runId(runId)
.score(score.toDouble())
.comparativeExperimentId(comparativeExperimentId)
.feedbackGroupId(feedbackGroupId)
.feedbackSource(
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
)
comments[runId]?.let { builder.comment(it) }
run?.sessionId()?.getOrNull()?.let { builder.sessionId(it) }
run?.startTime()?.getOrNull()?.let { startTime ->
runStartTime(startTime)?.let { builder.startTime(it) }
}
client.feedback().create(builder.build())
}
}
private fun buildProjectFeedbackCreateSchema(
result: EvaluationResult,
projectId: String,
): FeedbackCreateSchema {
val builder = FeedbackCreateSchema.builder().key(result.key).sessionId(projectId)
builder.feedbackSource(
ModelFeedbackSource.builder().type(ModelFeedbackSource.Type.MODEL).build()
)
val score = result.score
if (score != null) {
builder.score(score.toDouble())
}
applyFeedbackValue(builder, result.value)
val comment = result.comment
if (comment != null) {
builder.comment(comment)
}
val feedbackConfig = result.feedbackConfig
if (feedbackConfig != null) {
builder.feedbackConfig(feedbackConfig)
}
val correction = result.correction
if (correction != null) {
builder.correction(
FeedbackCreateSchema.Correction.ofUnionMember0(
FeedbackCreateSchema.Correction.UnionMember0.builder()
.putAllAdditionalProperties(
correction.mapValues { (_, value) -> JsonValue.from(value) }
)
.build()
)
)
}
val error = result.extra?.get("error") as? Boolean
if (error == true) {
builder.error(true)
}
return builder.build()
}
private const val RUN_QUERY_TIMEOUT_MS = 30_000L
private const val RUN_QUERY_INITIAL_DELAY_MS = 500L
private const val RUN_QUERY_MAX_DELAY_MS = 2_000L
@@ -0,0 +1,20 @@
package com.langchain.smith.evaluation
import java.util.function.Function
/**
* Runnable-style evaluation target. Mirrors Python's `Runnable.invoke` / chain target support
* without requiring a LangChain dependency.
*/
fun interface EvaluateTarget {
fun invoke(inputs: Map<String, Any?>): Any?
}
/** Wraps a Kotlin lambda as an [EvaluateTarget]. */
fun evaluateTarget(func: (Map<String, Any?>) -> Any?): EvaluateTarget = EvaluateTarget(func::invoke)
/** Wraps a Java [Function] as an [EvaluateTarget]. */
fun evaluateTarget(func: Function<Map<String, Any?>, Any?>): EvaluateTarget =
EvaluateTarget { inputs ->
func.apply(inputs)
}
@@ -0,0 +1,27 @@
package com.langchain.smith.evaluation
import com.langchain.smith.models.feedback.FeedbackCreateSchema
/**
* Normalized output from [RunEvaluator.evaluateRun], matching Python's `Union[EvaluationResult,
* EvaluationResults]`.
*/
sealed interface EvaluationOutput
/** A single evaluation feedback metric produced by a [RunEvaluator]. */
data class EvaluationResult(
val key: String,
val score: Number? = null,
val value: Any? = null,
val metadata: Map<String, Any>? = null,
val comment: String? = null,
val correction: Map<String, Any>? = null,
val evaluatorInfo: Map<String, Any> = emptyMap(),
val feedbackConfig: FeedbackCreateSchema.FeedbackConfig? = null,
val sourceRunId: String? = null,
val targetRunId: String? = null,
val extra: Map<String, Any>? = null,
) : EvaluationOutput
/** Batch evaluation results returned when an evaluator emits multiple metrics at once. */
data class EvaluationResults(val results: List<EvaluationResult>) : EvaluationOutput

Some files were not shown because too many files have changed in this diff Show More