langgraph-api 0.2.85__tar.gz → 0.2.86__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/PKG-INFO +1 -1
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/benchmark/Makefile +3 -0
- langgraph_api-0.2.86/benchmark/README.md +72 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/benchmark/burst.js +18 -2
- langgraph_api-0.2.86/benchmark/ramp.js +178 -0
- langgraph_api-0.2.86/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/mcp.py +2 -1
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/langsmith/backend.py +29 -3
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/config.py +1 -1
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/http.py +3 -3
- langgraph_api-0.2.86/langgraph_api/utils/cache.py +58 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_license/validation.py +6 -1
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/uv.lock +3 -3
- langgraph_api-0.2.85/benchmark/README.md +0 -53
- langgraph_api-0.2.85/langgraph_api/__init__.py +0 -1
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/.gitignore +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/LICENSE +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/Makefile +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/README.md +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/benchmark/.gitignore +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/benchmark/weather.js +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/constraints.txt +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/forbidden.txt +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/healthcheck.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/meta.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/asyncio.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/cli.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/command.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/http_metrics.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/remote.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/queue_entrypoint.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/route.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/serde.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/server.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/state.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/store.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/utils/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/utils.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_api/worker.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/logging.json +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/openapi.json +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/pyproject.toml +0 -0
- {langgraph_api-0.2.85 → langgraph_api-0.2.86}/scripts/create_license.py +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# K6 Performance Testing
|
|
2
|
+
|
|
3
|
+
K6 is a modern load testing tool that allows you to test the performance and reliability of your APIs. The tests in this directory are designed to validate the performance characteristics of the LangGraph API under various load conditions.
|
|
4
|
+
|
|
5
|
+
## Test Scenarios
|
|
6
|
+
|
|
7
|
+
### Available Tests
|
|
8
|
+
|
|
9
|
+
There are two modes of testing available:
|
|
10
|
+
1. `Burst` - Kick off a burst of /run/wait requests.
|
|
11
|
+
Available Params:
|
|
12
|
+
BURST_SIZE - How many requests to run. Default: 100
|
|
13
|
+
2. `Ramp` - Scale up the number of /run/wait requests and then plateau.
|
|
14
|
+
Available Params:
|
|
15
|
+
LOAD_SIZE - How much traffic to ramp up over a 60s period. Default: 100
|
|
16
|
+
LEVELS - The number of times to ramp up. Default: 10
|
|
17
|
+
PLATEAU_DURATION - How long to sustain the max level of traffic in seconds. Default: 300
|
|
18
|
+
|
|
19
|
+
### Agent
|
|
20
|
+
|
|
21
|
+
We use a local benchmark agent that can be configured to run a number of different test scenarios to simulate a variety of graphs.
|
|
22
|
+
|
|
23
|
+
Available Params:
|
|
24
|
+
DATA_SIZE - How many characters each message should have in a parallel or sequence node. Default: 1000
|
|
25
|
+
DELAY - How long to sleep in each parallel or sequence node. Default: 0
|
|
26
|
+
EXPAND - How many nodes to run in the parallel or sequence modes. Default: 50
|
|
27
|
+
MODE - What configuration to run the graph. Default: single
|
|
28
|
+
- `single` - Run a single node
|
|
29
|
+
- `parallel` - Run EXPAND nodes in parallel
|
|
30
|
+
- `sequential` - Run EXPAND nodes in sequence
|
|
31
|
+
|
|
32
|
+
## Running Tests
|
|
33
|
+
|
|
34
|
+
### Local Prerequisites
|
|
35
|
+
|
|
36
|
+
1. Install k6: https://k6.io/docs/getting-started/installation/
|
|
37
|
+
2. Start your LangGraph API service
|
|
38
|
+
3. Ensure the API is accessible at `http://localhost:9123`
|
|
39
|
+
|
|
40
|
+
### Remote Prerequisites
|
|
41
|
+
|
|
42
|
+
1. Get a LangSmith API Key: https://docs.smith.langchain.com/administration/how_to_guides/organization_management/create_account_api_key#create-an-api-key
|
|
43
|
+
2. The deployment jdr-benchmark is setup for this. Endpoint: https://jdr-benchmark-1cfe27c4cd375e1c999f02f186f617f6.us.langgraph.app
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Run burst test with default burst size
|
|
49
|
+
make benchmark-burst
|
|
50
|
+
|
|
51
|
+
# Run burst test with custom burst size
|
|
52
|
+
BURST_SIZE=500 make benchmark-burst
|
|
53
|
+
|
|
54
|
+
# Run ramp test with a different mode and expand size
|
|
55
|
+
MODE='parallel' EXPAND=100 make benchmark-ramp
|
|
56
|
+
|
|
57
|
+
# Run burst test against a deployment
|
|
58
|
+
BASE_URL=https://jdr-benchmark-1cfe27c4cd375e1c999f02f186f617f6.us.langgraph.app make benchmark-burst
|
|
59
|
+
|
|
60
|
+
# Clean up result files
|
|
61
|
+
make benchmark-clean
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Output
|
|
65
|
+
|
|
66
|
+
Summary results are written to stdout and persisted in a summary_burst file. More detailed results for the same burst are persisted in a results_burst file.
|
|
67
|
+
|
|
68
|
+
## Resources
|
|
69
|
+
|
|
70
|
+
- [K6 Documentation](https://k6.io/docs/)
|
|
71
|
+
- [K6 JavaScript API](https://k6.io/docs/javascript-api/)
|
|
72
|
+
- [Performance Testing Best Practices](https://k6.io/docs/testing-guides/)
|
|
@@ -15,9 +15,17 @@ const burstSuccessRate = new Rate('burst_success_rate');
|
|
|
15
15
|
|
|
16
16
|
// URL of your LangGraph server
|
|
17
17
|
const BASE_URL = __ENV.BASE_URL || 'http://localhost:9123';
|
|
18
|
+
// LangSmith API key only needed with a custom server endpoint
|
|
19
|
+
const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
|
|
20
|
+
|
|
21
|
+
// Params for the runner
|
|
18
22
|
const BURST_SIZE = parseInt(__ENV.BURST_SIZE || '100');
|
|
19
|
-
|
|
23
|
+
|
|
24
|
+
// Params for the agent
|
|
25
|
+
const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
|
|
26
|
+
const DELAY = parseInt(__ENV.DELAY || '0');
|
|
20
27
|
const EXPAND = parseInt(__ENV.EXPAND || '50');
|
|
28
|
+
const MODE = __ENV.MODE || 'single';
|
|
21
29
|
|
|
22
30
|
// Burst testing configuration
|
|
23
31
|
export let options = {
|
|
@@ -48,11 +56,19 @@ export default function() {
|
|
|
48
56
|
|
|
49
57
|
// Prepare the request payload
|
|
50
58
|
const headers = { 'Content-Type': 'application/json' };
|
|
59
|
+
if (LANGSMITH_API_KEY) {
|
|
60
|
+
headers['x-api-key'] = LANGSMITH_API_KEY;
|
|
61
|
+
}
|
|
51
62
|
|
|
52
63
|
// Create a payload with the LangGraph agent configuration
|
|
53
64
|
const payload = JSON.stringify({
|
|
54
65
|
assistant_id: "benchmark",
|
|
55
|
-
input: {
|
|
66
|
+
input: {
|
|
67
|
+
data_size: DATA_SIZE,
|
|
68
|
+
delay: DELAY,
|
|
69
|
+
expand: EXPAND,
|
|
70
|
+
mode: MODE,
|
|
71
|
+
},
|
|
56
72
|
config: {
|
|
57
73
|
recursion_limit: EXPAND + 2,
|
|
58
74
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import http from 'k6/http';
|
|
2
|
+
import { check, sleep } from 'k6';
|
|
3
|
+
import { Counter, Trend } from 'k6/metrics';
|
|
4
|
+
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
|
|
5
|
+
|
|
6
|
+
// Custom metrics
|
|
7
|
+
const runDuration = new Trend('run_duration');
|
|
8
|
+
const successfulRuns = new Counter('successful_runs');
|
|
9
|
+
const failedRuns = new Counter('failed_runs');
|
|
10
|
+
const timeoutErrors = new Counter('timeout_errors');
|
|
11
|
+
const connectionErrors = new Counter('connection_errors');
|
|
12
|
+
const serverErrors = new Counter('server_errors');
|
|
13
|
+
const otherErrors = new Counter('other_errors');
|
|
14
|
+
|
|
15
|
+
// URL of your LangGraph server
|
|
16
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:9123';
|
|
17
|
+
// LangSmith API key only needed with a custom server endpoint
|
|
18
|
+
const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
|
|
19
|
+
|
|
20
|
+
// Params for the runner
|
|
21
|
+
const LOAD_SIZE = parseInt(__ENV.LOAD_SIZE || '100');
|
|
22
|
+
const LEVELS = parseInt(__ENV.LEVELS || '10');
|
|
23
|
+
const PLATEAU_DURATION = parseInt(__ENV.PLATEAU_DURATION || '300');
|
|
24
|
+
|
|
25
|
+
// Params for the agent
|
|
26
|
+
const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
|
|
27
|
+
const DELAY = parseInt(__ENV.DELAY || '0');
|
|
28
|
+
const EXPAND = parseInt(__ENV.EXPAND || '50');
|
|
29
|
+
const MODE = __ENV.MODE || 'single';
|
|
30
|
+
|
|
31
|
+
const stages = [];
|
|
32
|
+
for (let i = 1; i <= LEVELS; i++) {
|
|
33
|
+
stages.push({ duration: '60s', target: LOAD_SIZE * i });
|
|
34
|
+
}
|
|
35
|
+
stages.push({ duration: `${PLATEAU_DURATION}s`, target: LOAD_SIZE * LEVELS});
|
|
36
|
+
stages.push({ duration: '30s', target: 0 }); // Ramp down
|
|
37
|
+
|
|
38
|
+
// Test configuration
|
|
39
|
+
export let options = {
|
|
40
|
+
scenarios: {
|
|
41
|
+
constant_load: {
|
|
42
|
+
executor: 'ramping-vus',
|
|
43
|
+
startVUs: 1,
|
|
44
|
+
stages,
|
|
45
|
+
gracefulRampDown: '120s',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
thresholds: {
|
|
49
|
+
'run_duration': ['p(95)<30000'], // 95% of runs should complete within 30s
|
|
50
|
+
'successful_runs': ['count>100'], // At least 100 successful runs
|
|
51
|
+
'http_req_failed': ['rate<0.2'], // Error rate should be less than 20%
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Main test function
|
|
56
|
+
export default function() {
|
|
57
|
+
const startTime = new Date().getTime();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Prepare the request payload
|
|
61
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
62
|
+
if (LANGSMITH_API_KEY) {
|
|
63
|
+
headers['x-api-key'] = LANGSMITH_API_KEY;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create a payload with the LangGraph agent configuration
|
|
67
|
+
const payload = JSON.stringify({
|
|
68
|
+
assistant_id: "benchmark",
|
|
69
|
+
input: {
|
|
70
|
+
data_size: DATA_SIZE,
|
|
71
|
+
delay: DELAY,
|
|
72
|
+
expand: EXPAND,
|
|
73
|
+
mode: MODE,
|
|
74
|
+
},
|
|
75
|
+
config: {
|
|
76
|
+
recursion_limit: EXPAND + 2,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Make a single request to the wait endpoint
|
|
81
|
+
const response = http.post(`${BASE_URL}/runs/wait`, payload, {
|
|
82
|
+
headers,
|
|
83
|
+
timeout: '120s' // k6 request timeout slightly longer than the server timeout
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Don't include verification in the duration of the request
|
|
87
|
+
const duration = new Date().getTime() - startTime;
|
|
88
|
+
|
|
89
|
+
// Check the response
|
|
90
|
+
const expected_length = MODE === 'single' ? 1 : EXPAND + 1;
|
|
91
|
+
const success = check(response, {
|
|
92
|
+
'Run completed successfully': (r) => r.status === 200,
|
|
93
|
+
'Response contains expected number of messages': (r) => JSON.parse(r.body).messages.length === expected_length,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (success) {
|
|
97
|
+
// Record success metrics
|
|
98
|
+
runDuration.add(duration);
|
|
99
|
+
successfulRuns.add(1);
|
|
100
|
+
|
|
101
|
+
// Optional: Log successful run details
|
|
102
|
+
console.log(`Run completed successfully in ${duration/1000}s`);
|
|
103
|
+
} else {
|
|
104
|
+
// Handle failure
|
|
105
|
+
failedRuns.add(1);
|
|
106
|
+
|
|
107
|
+
// Classify error based on status code or response
|
|
108
|
+
if (response.status >= 500) {
|
|
109
|
+
serverErrors.add(1);
|
|
110
|
+
console.log(`Server error: ${response.status}`);
|
|
111
|
+
} else if (response.status === 408 || response.error === 'timeout') {
|
|
112
|
+
timeoutErrors.add(1);
|
|
113
|
+
console.log(`Timeout error: ${response.error}`);
|
|
114
|
+
} else {
|
|
115
|
+
otherErrors.add(1);
|
|
116
|
+
console.log(`Other error: Status ${response.status}, ${JSON.stringify(response)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Handle exceptions (network errors, etc.)
|
|
122
|
+
failedRuns.add(1);
|
|
123
|
+
|
|
124
|
+
if (error.message.includes('timeout')) {
|
|
125
|
+
timeoutErrors.add(1);
|
|
126
|
+
console.log(`Timeout error: ${error.message}`);
|
|
127
|
+
} else if (error.message.includes('connection') || error.message.includes('network')) {
|
|
128
|
+
connectionErrors.add(1);
|
|
129
|
+
console.log(`Connection error: ${error.message}`);
|
|
130
|
+
} else {
|
|
131
|
+
otherErrors.add(1);
|
|
132
|
+
console.log(`Unexpected error: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add a small random sleep between iterations to prevent thundering herd
|
|
137
|
+
sleep(randomIntBetween(0.2, 0.5) / 1.0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Setup function
|
|
141
|
+
export function setup() {
|
|
142
|
+
console.log(`Starting ramp benchmark`);
|
|
143
|
+
console.log(`Running on pod: ${__ENV.POD_NAME || 'local'}`);
|
|
144
|
+
console.log(`Running ${LEVELS} levels with base size ${LOAD_SIZE}`);
|
|
145
|
+
|
|
146
|
+
return { startTime: new Date().toISOString() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Handle summary
|
|
150
|
+
export function handleSummary(data) {
|
|
151
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
152
|
+
|
|
153
|
+
// Create summary information with aggregated metrics
|
|
154
|
+
const summary = {
|
|
155
|
+
timestamp: timestamp,
|
|
156
|
+
metrics: {
|
|
157
|
+
totalRuns: data.metrics.successful_runs.values.count + data.metrics.failed_runs.values.count,
|
|
158
|
+
successfulRuns: data.metrics.successful_runs.values.count,
|
|
159
|
+
failedRuns: data.metrics.failed_runs.values.count,
|
|
160
|
+
successRate: data.metrics.successful_runs.values.count /
|
|
161
|
+
(data.metrics.successful_runs.values.count + data.metrics.failed_runs.values.count) * 100,
|
|
162
|
+
averageDuration: data.metrics.run_duration.values.avg / 1000, // in seconds
|
|
163
|
+
p95Duration: data.metrics.run_duration.values["p(95)"] / 1000, // in seconds
|
|
164
|
+
errors: {
|
|
165
|
+
timeout: data.metrics.timeout_errors ? data.metrics.timeout_errors.values.count : 0,
|
|
166
|
+
connection: data.metrics.connection_errors ? data.metrics.connection_errors.values.count : 0,
|
|
167
|
+
server: data.metrics.server_errors ? data.metrics.server_errors.values.count : 0,
|
|
168
|
+
other: data.metrics.other_errors ? data.metrics.other_errors.values.count : 0
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
[`results_${timestamp}.json`]: JSON.stringify(data, null, 2),
|
|
175
|
+
[`summary_${timestamp}.json`]: JSON.stringify(summary, null, 2),
|
|
176
|
+
stdout: JSON.stringify(summary, null, 2) // Also print summary to console
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.86"
|
|
@@ -385,11 +385,12 @@ async def handle_tools_list(
|
|
|
385
385
|
seen_names.add(name)
|
|
386
386
|
|
|
387
387
|
schemas = await client.assistants.get_schemas(id_, headers=request.headers)
|
|
388
|
+
description = assistant.get("description") or ""
|
|
388
389
|
tools.append(
|
|
389
390
|
{
|
|
390
391
|
"name": name,
|
|
391
392
|
"inputSchema": schemas.get("input_schema", {}),
|
|
392
|
-
"description":
|
|
393
|
+
"description": description,
|
|
393
394
|
},
|
|
394
395
|
)
|
|
395
396
|
|
|
@@ -14,6 +14,7 @@ from langgraph_api.config import (
|
|
|
14
14
|
LANGSMITH_AUTH_VERIFY_TENANT_ID,
|
|
15
15
|
LANGSMITH_TENANT_ID,
|
|
16
16
|
)
|
|
17
|
+
from langgraph_api.utils.cache import LRUCache
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class AuthDict(TypedDict):
|
|
@@ -23,7 +24,22 @@ class AuthDict(TypedDict):
|
|
|
23
24
|
user_email: NotRequired[str]
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
class AuthCacheEntry(TypedDict):
|
|
28
|
+
credentials: AuthCredentials
|
|
29
|
+
user: StudioUser
|
|
30
|
+
|
|
31
|
+
|
|
26
32
|
class LangsmithAuthBackend(AuthenticationBackend):
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self._cache = LRUCache[AuthCacheEntry](max_size=1000, ttl=60)
|
|
35
|
+
|
|
36
|
+
def _get_cache_key(self, headers):
|
|
37
|
+
"""Generate cache key from authentication headers"""
|
|
38
|
+
relevant_headers = tuple(
|
|
39
|
+
(name, value) for name, value in headers if value is not None
|
|
40
|
+
)
|
|
41
|
+
return str(hash(relevant_headers))
|
|
42
|
+
|
|
27
43
|
async def authenticate(
|
|
28
44
|
self, conn: HTTPConnection
|
|
29
45
|
) -> tuple[AuthCredentials, BaseUser] | None:
|
|
@@ -37,6 +53,12 @@ class LangsmithAuthBackend(AuthenticationBackend):
|
|
|
37
53
|
]
|
|
38
54
|
if not any(h[1] for h in headers):
|
|
39
55
|
raise AuthenticationError("Missing authentication headers")
|
|
56
|
+
|
|
57
|
+
# Check cache first
|
|
58
|
+
cache_key = self._get_cache_key(headers)
|
|
59
|
+
if cached_entry := self._cache.get(cache_key):
|
|
60
|
+
return cached_entry["credentials"], cached_entry["user"]
|
|
61
|
+
|
|
40
62
|
async with auth_client() as auth:
|
|
41
63
|
if not LANGSMITH_AUTH_VERIFY_TENANT_ID and not conn.headers.get(
|
|
42
64
|
"x-api-key"
|
|
@@ -66,6 +88,10 @@ class LangsmithAuthBackend(AuthenticationBackend):
|
|
|
66
88
|
if auth_dict["tenant_id"] != LANGSMITH_TENANT_ID:
|
|
67
89
|
raise AuthenticationError("Invalid tenant ID")
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
credentials = AuthCredentials(["authenticated"])
|
|
92
|
+
user = StudioUser(auth_dict.get("user_id"), is_authenticated=True)
|
|
93
|
+
|
|
94
|
+
# Cache the result
|
|
95
|
+
self._cache.set(cache_key, AuthCacheEntry(credentials=credentials, user=user))
|
|
96
|
+
|
|
97
|
+
return credentials, user
|
|
@@ -173,7 +173,7 @@ LANGGRAPH_AES_KEY = env("LANGGRAPH_AES_KEY", default=None, cast=_get_encryption_
|
|
|
173
173
|
# redis
|
|
174
174
|
REDIS_URI = env("REDIS_URI", cast=str)
|
|
175
175
|
REDIS_CLUSTER = env("REDIS_CLUSTER", cast=bool, default=False)
|
|
176
|
-
REDIS_MAX_CONNECTIONS = env("REDIS_MAX_CONNECTIONS", cast=int, default=
|
|
176
|
+
REDIS_MAX_CONNECTIONS = env("REDIS_MAX_CONNECTIONS", cast=int, default=2000)
|
|
177
177
|
REDIS_CONNECT_TIMEOUT = env("REDIS_CONNECT_TIMEOUT", cast=float, default=10.0)
|
|
178
178
|
REDIS_MAX_IDLE_TIME = env("REDIS_MAX_IDLE_TIME", cast=float, default=120.0)
|
|
179
179
|
REDIS_KEY_PREFIX = env("REDIS_KEY_PREFIX", cast=str, default="")
|
|
@@ -120,9 +120,9 @@ def is_retriable_error(exception: Exception) -> bool:
|
|
|
120
120
|
return True
|
|
121
121
|
# Seems to just apply to HttpStatusError but doesn't hurt to check all
|
|
122
122
|
if isinstance(exception, httpx.HTTPError):
|
|
123
|
-
return (
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
return getattr(exception, "response", None) is not None and (
|
|
124
|
+
exception.response.status_code >= 500
|
|
125
|
+
or exception.response.status_code == 429
|
|
126
126
|
)
|
|
127
127
|
return False
|
|
128
128
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LRUCache(Generic[T]):
|
|
10
|
+
"""LRU cache with TTL support."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, max_size: int = 1000, ttl: float = 60):
|
|
13
|
+
self._cache: OrderedDict[str, tuple[T, float]] = OrderedDict()
|
|
14
|
+
self._max_size = max_size if max_size > 0 else 1000
|
|
15
|
+
self._ttl = ttl
|
|
16
|
+
|
|
17
|
+
def _get_time(self) -> float:
|
|
18
|
+
"""Get current time, using loop.time() if available for better performance."""
|
|
19
|
+
try:
|
|
20
|
+
return asyncio.get_event_loop().time()
|
|
21
|
+
except RuntimeError:
|
|
22
|
+
return time.monotonic()
|
|
23
|
+
|
|
24
|
+
def get(self, key: str) -> T | None:
|
|
25
|
+
"""Get item from cache, returning None if expired or not found."""
|
|
26
|
+
if key not in self._cache:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
value, timestamp = self._cache[key]
|
|
30
|
+
if self._get_time() - timestamp >= self._ttl:
|
|
31
|
+
# Expired, remove and return None
|
|
32
|
+
del self._cache[key]
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Move to end (most recently used)
|
|
36
|
+
self._cache.move_to_end(key)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
def set(self, key: str, value: T) -> None:
|
|
40
|
+
"""Set item in cache, evicting old entries if needed."""
|
|
41
|
+
# Remove if already exists (to update timestamp)
|
|
42
|
+
if key in self._cache:
|
|
43
|
+
del self._cache[key]
|
|
44
|
+
|
|
45
|
+
# Evict oldest entries if needed
|
|
46
|
+
while len(self._cache) >= self._max_size:
|
|
47
|
+
self._cache.popitem(last=False) # Remove oldest (FIFO)
|
|
48
|
+
|
|
49
|
+
# Add new entry
|
|
50
|
+
self._cache[key] = (value, self._get_time())
|
|
51
|
+
|
|
52
|
+
def size(self) -> int:
|
|
53
|
+
"""Return current cache size."""
|
|
54
|
+
return len(self._cache)
|
|
55
|
+
|
|
56
|
+
def clear(self) -> None:
|
|
57
|
+
"""Clear all entries from cache."""
|
|
58
|
+
self._cache.clear()
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""Noop license middleware"""
|
|
2
2
|
|
|
3
|
+
import structlog
|
|
4
|
+
|
|
5
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
6
|
+
|
|
3
7
|
|
|
4
8
|
async def get_license_status() -> bool:
|
|
5
9
|
"""Always return true"""
|
|
@@ -17,6 +21,7 @@ async def check_license_periodically(_: int = 60):
|
|
|
17
21
|
If the license ever fails, you could decide to log,
|
|
18
22
|
raise an exception, or attempt a graceful shutdown.
|
|
19
23
|
"""
|
|
20
|
-
|
|
24
|
+
await logger.ainfo(
|
|
21
25
|
"This is a noop license middleware. No license check is performed."
|
|
22
26
|
)
|
|
27
|
+
return None
|
|
@@ -580,7 +580,7 @@ wheels = [
|
|
|
580
580
|
|
|
581
581
|
[[package]]
|
|
582
582
|
name = "langsmith"
|
|
583
|
-
version = "0.4.
|
|
583
|
+
version = "0.4.5"
|
|
584
584
|
source = { registry = "https://pypi.org/simple" }
|
|
585
585
|
dependencies = [
|
|
586
586
|
{ name = "httpx" },
|
|
@@ -591,9 +591,9 @@ dependencies = [
|
|
|
591
591
|
{ name = "requests-toolbelt" },
|
|
592
592
|
{ name = "zstandard" },
|
|
593
593
|
]
|
|
594
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
594
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5c/92/7885823f3d13222f57773921f0da19b37d628c64607491233dc853a0f6ea/langsmith-0.4.5.tar.gz", hash = "sha256:49444bd8ccd4e46402f1b9ff1d686fa8e3a31b175e7085e72175ab8ec6164a34", size = 352235, upload-time = "2025-07-10T22:08:04.505Z" }
|
|
595
595
|
wheels = [
|
|
596
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
596
|
+
{ url = "https://files.pythonhosted.org/packages/c8/10/ad3107b666c3203b7938d10ea6b8746b9735c399cf737a51386d58e41d34/langsmith-0.4.5-py3-none-any.whl", hash = "sha256:4167717a2cccc4dff5809dbddc439628e836f6fd13d4fdb31ea013bc8d5cfaf5", size = 367795, upload-time = "2025-07-10T22:08:02.548Z" },
|
|
597
597
|
]
|
|
598
598
|
|
|
599
599
|
[[package]]
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# K6 Performance Testing
|
|
2
|
-
|
|
3
|
-
K6 is a modern load testing tool that allows you to test the performance and reliability of your APIs. The tests in this directory are designed to validate the performance characteristics of the LangGraph API under various load conditions.
|
|
4
|
-
|
|
5
|
-
## Test Scenarios
|
|
6
|
-
|
|
7
|
-
### Available Tests
|
|
8
|
-
|
|
9
|
-
We use a local benchmark agent that has a MODE that can be any of the following:
|
|
10
|
-
- `single` - Run a single node
|
|
11
|
-
- `parallel` - Run EXPAND nodes in parallel
|
|
12
|
-
- `sequential` - Run EXPAND nodes in sequence
|
|
13
|
-
|
|
14
|
-
By default, MODE is `single` and EXPAND is 50.
|
|
15
|
-
|
|
16
|
-
1. Burst - Kick off a burst of /run/wait requests. Default BURST_SIZE is 100.
|
|
17
|
-
|
|
18
|
-
## Running Tests Locally
|
|
19
|
-
|
|
20
|
-
### Prerequisites
|
|
21
|
-
|
|
22
|
-
1. Install k6: https://k6.io/docs/getting-started/installation/
|
|
23
|
-
2. Start your LangGraph API service
|
|
24
|
-
3. Ensure the API is accessible at `http://localhost:9123`
|
|
25
|
-
|
|
26
|
-
### Basic Usage
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
# Run burst test with default burst size
|
|
30
|
-
make benchmark-burst
|
|
31
|
-
|
|
32
|
-
# Run burst test with custom burst size
|
|
33
|
-
BURST_SIZE=500 make benchmark-burst
|
|
34
|
-
|
|
35
|
-
# Run burst test with a different mode and expand size
|
|
36
|
-
MODE='parallel' EXPAND=100 make benchmark-burst
|
|
37
|
-
|
|
38
|
-
# Run burst test against a deployment
|
|
39
|
-
BASE_URL=https://jdr-debug-31ac2c83eef557309f21c1e98d822025.us.langgraph.app make benchmark-burst
|
|
40
|
-
|
|
41
|
-
# Clean up result files
|
|
42
|
-
make benchmark-clean
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Output
|
|
46
|
-
|
|
47
|
-
Summary results are written to stdout and persisted in a summary_burst file. More detailed results for the same burst are persisted in a results_burst file.
|
|
48
|
-
|
|
49
|
-
## Resources
|
|
50
|
-
|
|
51
|
-
- [K6 Documentation](https://k6.io/docs/)
|
|
52
|
-
- [K6 JavaScript API](https://k6.io/docs/javascript-api/)
|
|
53
|
-
- [Performance Testing Best Practices](https://k6.io/docs/testing-guides/)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.85"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|