langgraph-api 0.2.85__tar.gz → 0.2.88__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.

Files changed (113) hide show
  1. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/PKG-INFO +2 -2
  2. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/benchmark/Makefile +3 -0
  3. langgraph_api-0.2.88/benchmark/README.md +72 -0
  4. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/benchmark/burst.js +18 -2
  5. langgraph_api-0.2.88/benchmark/ramp.js +178 -0
  6. langgraph_api-0.2.88/langgraph_api/__init__.py +1 -0
  7. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/mcp.py +2 -1
  8. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/langsmith/backend.py +30 -3
  9. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/config.py +5 -1
  10. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/http.py +3 -3
  11. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/client.mts +2 -1
  12. langgraph_api-0.2.88/langgraph_api/js/traceblock.mts +25 -0
  13. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/models/run.py +3 -2
  14. langgraph_api-0.2.88/langgraph_api/traceblock.py +22 -0
  15. langgraph_api-0.2.88/langgraph_api/utils/cache.py +58 -0
  16. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/worker.py +39 -27
  17. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_license/validation.py +6 -1
  18. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/pyproject.toml +1 -1
  19. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/uv.lock +3 -3
  20. langgraph_api-0.2.85/benchmark/README.md +0 -53
  21. langgraph_api-0.2.85/langgraph_api/__init__.py +0 -1
  22. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/.gitignore +0 -0
  23. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/LICENSE +0 -0
  24. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/Makefile +0 -0
  25. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/README.md +0 -0
  26. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/benchmark/.gitignore +0 -0
  27. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/benchmark/weather.js +0 -0
  28. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/constraints.txt +0 -0
  29. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/forbidden.txt +0 -0
  30. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/healthcheck.py +0 -0
  31. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/__init__.py +0 -0
  32. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/assistants.py +0 -0
  33. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/meta.py +0 -0
  34. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/openapi.py +0 -0
  35. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/runs.py +0 -0
  36. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/store.py +0 -0
  37. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/threads.py +0 -0
  38. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/api/ui.py +0 -0
  39. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/asgi_transport.py +0 -0
  40. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/asyncio.py +0 -0
  41. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/__init__.py +0 -0
  42. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/custom.py +0 -0
  43. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/langsmith/__init__.py +0 -0
  44. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/langsmith/client.py +0 -0
  45. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/middleware.py +0 -0
  46. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/noop.py +0 -0
  47. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/auth/studio_user.py +0 -0
  48. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/cli.py +0 -0
  49. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/command.py +0 -0
  50. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/cron_scheduler.py +0 -0
  51. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/errors.py +0 -0
  52. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/graph.py +0 -0
  53. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/http_metrics.py +0 -0
  54. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/.gitignore +0 -0
  55. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/.prettierrc +0 -0
  56. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/__init__.py +0 -0
  57. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/base.py +0 -0
  58. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/build.mts +0 -0
  59. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/client.http.mts +0 -0
  60. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/errors.py +0 -0
  61. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/global.d.ts +0 -0
  62. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/package.json +0 -0
  63. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/remote.py +0 -0
  64. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/schema.py +0 -0
  65. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/graph.mts +0 -0
  66. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/load.hooks.mjs +0 -0
  67. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/preload.mjs +0 -0
  68. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/utils/files.mts +0 -0
  69. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/utils/importMap.mts +0 -0
  70. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  71. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/src/utils/serde.mts +0 -0
  72. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/sse.py +0 -0
  73. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/tsconfig.json +0 -0
  74. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/ui.py +0 -0
  75. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/js/yarn.lock +0 -0
  76. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/logging.py +0 -0
  77. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/metadata.py +0 -0
  78. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/middleware/__init__.py +0 -0
  79. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/middleware/http_logger.py +0 -0
  80. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/middleware/private_network.py +0 -0
  81. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/middleware/request_id.py +0 -0
  82. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/models/__init__.py +0 -0
  83. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/patch.py +0 -0
  84. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/queue_entrypoint.py +0 -0
  85. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/route.py +0 -0
  86. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/schema.py +0 -0
  87. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/serde.py +0 -0
  88. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/server.py +0 -0
  89. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/sse.py +0 -0
  90. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/state.py +0 -0
  91. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/store.py +0 -0
  92. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/stream.py +0 -0
  93. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/thread_ttl.py +0 -0
  94. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/tunneling/cloudflare.py +0 -0
  95. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/utils/__init__.py +0 -0
  96. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/utils/config.py +0 -0
  97. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/utils/future.py +0 -0
  98. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/utils.py +0 -0
  99. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/validation.py +0 -0
  100. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_api/webhook.py +0 -0
  101. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_license/__init__.py +0 -0
  102. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/__init__.py +0 -0
  103. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/checkpoint.py +0 -0
  104. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/database.py +0 -0
  105. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/lifespan.py +0 -0
  106. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/metrics.py +0 -0
  107. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/ops.py +0 -0
  108. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/queue.py +0 -0
  109. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/retry.py +0 -0
  110. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/langgraph_runtime/store.py +0 -0
  111. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/logging.json +0 -0
  112. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/openapi.json +0 -0
  113. {langgraph_api-0.2.85 → langgraph_api-0.2.88}/scripts/create_license.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.2.85
3
+ Version: 0.2.88
4
4
  Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ Requires-Dist: httpx>=0.25.0
11
11
  Requires-Dist: jsonschema-rs<0.30,>=0.20.0
12
12
  Requires-Dist: langchain-core>=0.3.64
13
13
  Requires-Dist: langgraph-checkpoint>=2.0.23
14
- Requires-Dist: langgraph-runtime-inmem<0.4,>=0.3.0
14
+ Requires-Dist: langgraph-runtime-inmem<0.5,>=0.4.0
15
15
  Requires-Dist: langgraph-sdk>=0.1.71
16
16
  Requires-Dist: langgraph>=0.3.27
17
17
  Requires-Dist: langsmith>=0.3.45
@@ -2,5 +2,8 @@
2
2
  benchmark-burst:
3
3
  k6 run burst.js
4
4
 
5
+ benchmark-ramp:
6
+ k6 run ramp.js
7
+
5
8
  benchmark-clean:
6
9
  rm -f results_*.json summary_*.json
@@ -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
- const MODE = __ENV.MODE || 'single';
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: {mode: MODE, expand: EXPAND},
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.88"
@@ -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
 
@@ -23,7 +23,24 @@ class AuthDict(TypedDict):
23
23
  user_email: NotRequired[str]
24
24
 
25
25
 
26
+ class AuthCacheEntry(TypedDict):
27
+ credentials: AuthCredentials
28
+ user: StudioUser
29
+
30
+
26
31
  class LangsmithAuthBackend(AuthenticationBackend):
32
+ def __init__(self):
33
+ from langgraph_api.utils.cache import LRUCache
34
+
35
+ self._cache = LRUCache[AuthCacheEntry](max_size=1000, ttl=60)
36
+
37
+ def _get_cache_key(self, headers):
38
+ """Generate cache key from authentication headers"""
39
+ relevant_headers = tuple(
40
+ (name, value) for name, value in headers if value is not None
41
+ )
42
+ return str(hash(relevant_headers))
43
+
27
44
  async def authenticate(
28
45
  self, conn: HTTPConnection
29
46
  ) -> tuple[AuthCredentials, BaseUser] | None:
@@ -37,6 +54,12 @@ class LangsmithAuthBackend(AuthenticationBackend):
37
54
  ]
38
55
  if not any(h[1] for h in headers):
39
56
  raise AuthenticationError("Missing authentication headers")
57
+
58
+ # Check cache first
59
+ cache_key = self._get_cache_key(headers)
60
+ if cached_entry := self._cache.get(cache_key):
61
+ return cached_entry["credentials"], cached_entry["user"]
62
+
40
63
  async with auth_client() as auth:
41
64
  if not LANGSMITH_AUTH_VERIFY_TENANT_ID and not conn.headers.get(
42
65
  "x-api-key"
@@ -66,6 +89,10 @@ class LangsmithAuthBackend(AuthenticationBackend):
66
89
  if auth_dict["tenant_id"] != LANGSMITH_TENANT_ID:
67
90
  raise AuthenticationError("Invalid tenant ID")
68
91
 
69
- return AuthCredentials(["authenticated"]), StudioUser(
70
- auth_dict.get("user_id"), is_authenticated=True
71
- )
92
+ credentials = AuthCredentials(["authenticated"])
93
+ user = StudioUser(auth_dict.get("user_id"), is_authenticated=True)
94
+
95
+ # Cache the result
96
+ self._cache.set(cache_key, AuthCacheEntry(credentials=credentials, user=user))
97
+
98
+ return credentials, user
@@ -6,6 +6,8 @@ import orjson
6
6
  from starlette.config import Config, undefined
7
7
  from starlette.datastructures import CommaSeparatedStrings
8
8
 
9
+ from langgraph_api import traceblock
10
+
9
11
  # types
10
12
 
11
13
 
@@ -173,7 +175,7 @@ LANGGRAPH_AES_KEY = env("LANGGRAPH_AES_KEY", default=None, cast=_get_encryption_
173
175
  # redis
174
176
  REDIS_URI = env("REDIS_URI", cast=str)
175
177
  REDIS_CLUSTER = env("REDIS_CLUSTER", cast=bool, default=False)
176
- REDIS_MAX_CONNECTIONS = env("REDIS_MAX_CONNECTIONS", cast=int, default=500)
178
+ REDIS_MAX_CONNECTIONS = env("REDIS_MAX_CONNECTIONS", cast=int, default=2000)
177
179
  REDIS_CONNECT_TIMEOUT = env("REDIS_CONNECT_TIMEOUT", cast=float, default=10.0)
178
180
  REDIS_MAX_IDLE_TIME = env("REDIS_MAX_IDLE_TIME", cast=float, default=120.0)
179
181
  REDIS_KEY_PREFIX = env("REDIS_KEY_PREFIX", cast=str, default="")
@@ -372,3 +374,5 @@ if not os.getenv("LANGCHAIN_REVISION_ID") and (
372
374
  # This is respected by the langsmith SDK env inference
373
375
  # https://github.com/langchain-ai/langsmith-sdk/blob/1b93e4c13b8369d92db891ae3babc3e2254f0e56/python/langsmith/env/_runtime_env.py#L190
374
376
  os.environ["LANGCHAIN_REVISION_ID"] = ref_sha
377
+
378
+ traceblock.patch_requests()
@@ -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
- getattr(exception, "response", None) is not None
125
- and exception.response.status_code >= 500
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
 
@@ -56,6 +56,7 @@ import {
56
56
  getStaticGraphSchema,
57
57
  } from "@langchain/langgraph-api/schema";
58
58
  import { filterValidExportPath } from "./src/utils/files.mts";
59
+ import { patchFetch } from "./traceblock.mts";
59
60
 
60
61
  const logger = createLogger({
61
62
  level: "debug",
@@ -1114,6 +1115,6 @@ async function getNodesExecutedRequest(
1114
1115
  nodesExecuted = 0;
1115
1116
  return { nodesExecuted: value };
1116
1117
  }
1117
-
1118
+ patchFetch();
1118
1119
  asyncExitHook(() => awaitAllCallbacks(), { wait: 3_000 });
1119
1120
  main();
@@ -0,0 +1,25 @@
1
+ import { overrideFetchImplementation } from "langsmith";
2
+
3
+ const RUNS_RE = /^https:\/\/api\.smith\.langchain\.com\/.*runs(\/|$)/i;
4
+
5
+ export function patchFetch() {
6
+ const shouldBlock =
7
+ typeof process !== "undefined" &&
8
+ !!(process.env && process.env.LANGSMITH_DISABLE_SAAS_RUNS === "true");
9
+
10
+ if (shouldBlock) {
11
+ overrideFetchImplementation(
12
+ async (input: RequestInfo, init?: RequestInit) => {
13
+ const req = input instanceof Request ? input : new Request(input, init);
14
+
15
+ if (req.method.toUpperCase() === "POST" && RUNS_RE.test(req.url)) {
16
+ throw new Error(
17
+ `Policy-blocked POST to ${new URL(req.url).pathname} — run tracking disabled`,
18
+ );
19
+ }
20
+
21
+ return fetch(req);
22
+ },
23
+ );
24
+ }
25
+ }
@@ -312,6 +312,7 @@ async def create_valid_run(
312
312
  after_seconds = payload.get("after_seconds", 0)
313
313
  configurable["__after_seconds__"] = after_seconds
314
314
  put_time_start = time.time()
315
+ if_not_exists = payload.get("if_not_exists", "reject")
315
316
  run_coro = Runs.put(
316
317
  conn,
317
318
  assistant_id,
@@ -337,7 +338,7 @@ async def create_valid_run(
337
338
  multitask_strategy=multitask_strategy,
338
339
  prevent_insert_if_inflight=prevent_insert_if_inflight,
339
340
  after_seconds=after_seconds,
340
- if_not_exists=payload.get("if_not_exists", "reject"),
341
+ if_not_exists=if_not_exists,
341
342
  )
342
343
  run_ = await run_coro
343
344
 
@@ -364,7 +365,7 @@ async def create_valid_run(
364
365
  stream_mode=stream_mode,
365
366
  temporary=temporary,
366
367
  after_seconds=after_seconds,
367
- if_not_exists=payload.get("if_not_exists", "reject"),
368
+ if_not_exists=if_not_exists,
368
369
  run_create_ms=(
369
370
  int(time.time() * 1_000) - request_start_time
370
371
  if request_start_time
@@ -0,0 +1,22 @@
1
+ import os
2
+ from urllib.parse import urlparse
3
+
4
+ from requests.sessions import Session
5
+
6
+ _HOST = "api.smith.langchain.com"
7
+ _PATH_PREFIX = "/runs"
8
+
9
+
10
+ def patch_requests():
11
+ if os.getenv("LANGSMITH_DISABLE_SAAS_RUNS") != "true":
12
+ return
13
+ _orig = Session.request
14
+
15
+ def _guard(self, method, url, *a, **kw):
16
+ if method.upper() == "POST":
17
+ u = urlparse(url)
18
+ if u.hostname == _HOST and _PATH_PREFIX in u.path:
19
+ raise RuntimeError(f"POST to {url} blocked by policy")
20
+ return _orig(self, method, url, *a, **kw)
21
+
22
+ Session.request = _guard
@@ -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()
@@ -113,6 +113,8 @@ async def worker(
113
113
  run_creation_ms=run_creation_ms,
114
114
  run_queue_ms=ms(run_started_at_dt, run["created_at"]),
115
115
  run_stream_start_ms=ms(run_stream_started_at_dt, run_started_at_dt),
116
+ temporary=temporary,
117
+ resumable=resumable,
116
118
  )
117
119
 
118
120
  def on_checkpoint(checkpoint_arg: CheckpointPayload):
@@ -242,47 +244,55 @@ async def worker(
242
244
  run_id=str(run_id),
243
245
  run_attempt=attempt,
244
246
  )
245
- await Threads.set_joint_status(
246
- conn, run["thread_id"], run_id, status, checkpoint=checkpoint
247
- )
247
+ if not temporary:
248
+ await Threads.set_joint_status(
249
+ conn, run["thread_id"], run_id, status, checkpoint=checkpoint
250
+ )
248
251
  elif isinstance(exception, TimeoutError):
249
252
  status = "timeout"
250
253
  await logger.awarning(
251
254
  "Background run timed out",
252
255
  **log_info,
253
256
  )
254
- await Threads.set_joint_status(
255
- conn, run["thread_id"], run_id, status, checkpoint=checkpoint
256
- )
257
- elif isinstance(exception, UserRollback):
258
- status = "rollback"
259
- try:
257
+ if not temporary:
260
258
  await Threads.set_joint_status(
261
259
  conn, run["thread_id"], run_id, status, checkpoint=checkpoint
262
260
  )
263
- await logger.ainfo(
264
- "Background run rolled back",
265
- **log_info,
266
- )
267
- except HTTPException as e:
268
- if e.status_code == 404:
261
+ elif isinstance(exception, UserRollback):
262
+ status = "rollback"
263
+ if not temporary:
264
+ try:
265
+ await Threads.set_joint_status(
266
+ conn,
267
+ run["thread_id"],
268
+ run_id,
269
+ status,
270
+ checkpoint=checkpoint,
271
+ )
269
272
  await logger.ainfo(
270
- "Ignoring rollback error for missing run",
273
+ "Background run rolled back",
271
274
  **log_info,
272
275
  )
273
- else:
274
- raise
275
-
276
- checkpoint = None # reset the checkpoint
276
+ except HTTPException as e:
277
+ if e.status_code == 404:
278
+ await logger.ainfo(
279
+ "Ignoring rollback error for missing run",
280
+ **log_info,
281
+ )
282
+ else:
283
+ raise
284
+
285
+ checkpoint = None # reset the checkpoint
277
286
  elif isinstance(exception, UserInterrupt):
278
287
  status = "interrupted"
279
288
  await logger.ainfo(
280
289
  "Background run interrupted",
281
290
  **log_info,
282
291
  )
283
- await Threads.set_joint_status(
284
- conn, run["thread_id"], run_id, status, checkpoint, exception
285
- )
292
+ if not temporary:
293
+ await Threads.set_joint_status(
294
+ conn, run["thread_id"], run_id, status, checkpoint, exception
295
+ )
286
296
  elif isinstance(exception, ALL_RETRIABLE_EXCEPTIONS):
287
297
  status = "retry"
288
298
  await logger.awarning(
@@ -290,6 +300,7 @@ async def worker(
290
300
  **log_info,
291
301
  )
292
302
  # Don't update thread status yet.
303
+ # Apply this even for temporary runs, so we retry
293
304
  await Runs.set_status(conn, run_id, "pending")
294
305
  else:
295
306
  status = "error"
@@ -303,13 +314,14 @@ async def worker(
303
314
  exc_info=not isinstance(exception, RemoteException),
304
315
  **log_info,
305
316
  )
306
- await Threads.set_joint_status(
307
- conn, run["thread_id"], run_id, status, checkpoint, exception
308
- )
317
+ if not temporary:
318
+ await Threads.set_joint_status(
319
+ conn, run["thread_id"], run_id, status, checkpoint, exception
320
+ )
309
321
 
310
322
  # delete thread if it's temporary and we don't want to retry
311
323
  if temporary and not isinstance(exception, ALL_RETRIABLE_EXCEPTIONS):
312
- await Threads.delete(conn, run["thread_id"])
324
+ await Threads._delete_with_run(conn, run["thread_id"], run_id)
313
325
 
314
326
  if isinstance(exception, ALL_RETRIABLE_EXCEPTIONS):
315
327
  await logger.awarning("RETRYING", exc_info=exception)
@@ -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
- raise NotImplementedError(
24
+ await logger.ainfo(
21
25
  "This is a noop license middleware. No license check is performed."
22
26
  )
27
+ return None
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "cryptography>=42.0.0,<45.0",
32
32
  "langgraph-sdk>=0.1.71",
33
33
  "cloudpickle>=3.0.0",
34
- "langgraph-runtime-inmem>=0.3.0,<0.4",
34
+ "langgraph-runtime-inmem>=0.4.0,<0.5",
35
35
  "truststore>=0.1",
36
36
  ]
37
37
 
@@ -580,7 +580,7 @@ wheels = [
580
580
 
581
581
  [[package]]
582
582
  name = "langsmith"
583
- version = "0.4.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/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
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/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
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