langgraph-api 0.4.36__tar.gz → 0.4.38__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 (137) hide show
  1. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/PKG-INFO +1 -1
  2. langgraph_api-0.4.38/benchmark/Makefile +54 -0
  3. langgraph_api-0.4.38/benchmark/capacity_k6.js +221 -0
  4. langgraph_api-0.4.38/benchmark/capacity_runner.mjs +307 -0
  5. langgraph_api-0.4.38/benchmark/reporting/dd_reporting.py +85 -0
  6. langgraph_api-0.4.38/langgraph_api/__init__.py +1 -0
  7. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/meta.py +2 -1
  8. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/cli.py +2 -2
  9. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/config.py +25 -11
  10. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/graph.py +1 -1
  11. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http_metrics.py +1 -1
  12. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/logging.py +24 -0
  13. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/metadata.py +5 -5
  14. langgraph_api-0.4.38/langgraph_api/self_hosted_logs.py +124 -0
  15. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/self_hosted_metrics.py +9 -9
  16. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/pyproject.toml +1 -0
  17. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/uv.lock +50 -12
  18. langgraph_api-0.4.36/benchmark/Makefile +0 -24
  19. langgraph_api-0.4.36/langgraph_api/__init__.py +0 -1
  20. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/.gitignore +0 -0
  21. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/LICENSE +0 -0
  22. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/Makefile +0 -0
  23. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/README.md +0 -0
  24. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/.gitignore +0 -0
  25. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/README.md +0 -0
  26. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/burst.js +0 -0
  27. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/clean.js +0 -0
  28. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/graphs.js +0 -0
  29. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/package.json +0 -0
  30. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/ramp.js +0 -0
  31. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/update-revision.js +0 -0
  32. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/benchmark/weather.js +0 -0
  33. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/constraints.txt +0 -0
  34. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/forbidden.txt +0 -0
  35. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/healthcheck.py +0 -0
  36. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/__init__.py +0 -0
  37. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/a2a.py +0 -0
  38. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/assistants.py +0 -0
  39. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/mcp.py +0 -0
  40. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/openapi.py +0 -0
  41. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/runs.py +0 -0
  42. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/store.py +0 -0
  43. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/threads.py +0 -0
  44. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/api/ui.py +0 -0
  45. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/asgi_transport.py +0 -0
  46. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/asyncio.py +0 -0
  47. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/__init__.py +0 -0
  48. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/custom.py +0 -0
  49. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/__init__.py +0 -0
  50. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/backend.py +0 -0
  51. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/langsmith/client.py +0 -0
  52. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/middleware.py +0 -0
  53. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/noop.py +0 -0
  54. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/auth/studio_user.py +0 -0
  55. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/command.py +0 -0
  56. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/cron_scheduler.py +0 -0
  57. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/errors.py +0 -0
  58. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/executor_entrypoint.py +0 -0
  59. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/feature_flags.py +0 -0
  60. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/__init__.py +0 -0
  61. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/client.py +0 -0
  62. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/__init__.py +0 -0
  63. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -0
  64. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2.pyi +0 -0
  65. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +0 -0
  66. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/grpc_ops/ops.py +0 -0
  67. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http.py +0 -0
  68. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/http_metrics_utils.py +0 -0
  69. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/.gitignore +0 -0
  70. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/.prettierrc +0 -0
  71. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/__init__.py +0 -0
  72. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/base.py +0 -0
  73. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/build.mts +0 -0
  74. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/client.http.mts +0 -0
  75. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/client.mts +0 -0
  76. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/errors.py +0 -0
  77. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/global.d.ts +0 -0
  78. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/package.json +0 -0
  79. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/remote.py +0 -0
  80. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/schema.py +0 -0
  81. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/graph.mts +0 -0
  82. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/load.hooks.mjs +0 -0
  83. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/preload.mjs +0 -0
  84. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/files.mts +0 -0
  85. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/importMap.mts +0 -0
  86. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  87. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/src/utils/serde.mts +0 -0
  88. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/sse.py +0 -0
  89. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/traceblock.mts +0 -0
  90. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/tsconfig.json +0 -0
  91. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/ui.py +0 -0
  92. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/js/yarn.lock +0 -0
  93. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/__init__.py +0 -0
  94. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/http_logger.py +0 -0
  95. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/private_network.py +0 -0
  96. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/middleware/request_id.py +0 -0
  97. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/models/__init__.py +0 -0
  98. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/models/run.py +0 -0
  99. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/patch.py +0 -0
  100. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/queue_entrypoint.py +0 -0
  101. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/route.py +0 -0
  102. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/schema.py +0 -0
  103. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/serde.py +0 -0
  104. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/server.py +0 -0
  105. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/sse.py +0 -0
  106. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/state.py +0 -0
  107. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/store.py +0 -0
  108. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/stream.py +0 -0
  109. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/thread_ttl.py +0 -0
  110. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/traceblock.py +0 -0
  111. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/tunneling/cloudflare.py +0 -0
  112. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/__init__.py +0 -0
  113. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/cache.py +0 -0
  114. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/config.py +0 -0
  115. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/errors.py +0 -0
  116. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/future.py +0 -0
  117. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/headers.py +0 -0
  118. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/retriable_client.py +0 -0
  119. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/stream_codec.py +0 -0
  120. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/utils/uuids.py +0 -0
  121. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/validation.py +0 -0
  122. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/webhook.py +0 -0
  123. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_api/worker.py +0 -0
  124. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_license/__init__.py +0 -0
  125. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_license/validation.py +0 -0
  126. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/__init__.py +0 -0
  127. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/checkpoint.py +0 -0
  128. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/database.py +0 -0
  129. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/lifespan.py +0 -0
  130. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/metrics.py +0 -0
  131. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/ops.py +0 -0
  132. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/queue.py +0 -0
  133. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/retry.py +0 -0
  134. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/langgraph_runtime/store.py +0 -0
  135. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/logging.json +0 -0
  136. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/openapi.json +0 -0
  137. {langgraph_api-0.4.36 → langgraph_api-0.4.38}/scripts/create_license.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.4.36
3
+ Version: 0.4.38
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
@@ -0,0 +1,54 @@
1
+ # Benchmark commands
2
+ BASE_URL ?= https://ifh-core-api-dr-benchmark-048d364b548f5d9790082d8ba4fb44d8.us.langgraph.app
3
+ RAMP_START ?= 10
4
+ RAMP_END ?= 1000
5
+ RAMP_MULTIPLIER ?= 2
6
+ WAIT_SECONDS ?= 60
7
+ SUCCESS_THRESHOLD ?= 0.99
8
+ CLEAR_BETWEEN_STEPS ?= true
9
+ CLEAR_DELAY_SECONDS ?= 5
10
+ DATA_SIZE ?= 1000
11
+ DELAY ?= 0
12
+ EXPAND ?= 10
13
+ STEPS ?= 10
14
+
15
+ benchmark-burst:
16
+ make benchmark-reset
17
+ k6 run burst.js
18
+
19
+ benchmark-ramp:
20
+ make benchmark-reset
21
+ k6 run --out json=raw_data_$(shell date +%Y-%m-%dT%H-%M-%S).json ramp.js
22
+
23
+ benchmark-capacity:
24
+ rm -f capacity_summary_t*.json capacity_report_*.json capacity_raw_t*.json capacity_hist_*.png capacity_pie_*.png
25
+ npm install
26
+ BASE_URL=$(BASE_URL) \
27
+ RAMP_START=$(RAMP_START) \
28
+ RAMP_END=$(RAMP_END) \
29
+ RAMP_MULTIPLIER=$(RAMP_MULTIPLIER) \
30
+ WAIT_SECONDS=$(WAIT_SECONDS) \
31
+ SUCCESS_THRESHOLD=$(SUCCESS_THRESHOLD) \
32
+ CLEAR_BETWEEN_STEPS=$(CLEAR_BETWEEN_STEPS) \
33
+ CLEAR_DELAY_SECONDS=$(CLEAR_DELAY_SECONDS) \
34
+ DATA_SIZE=$(DATA_SIZE) \
35
+ DELAY=$(DELAY) \
36
+ EXPAND=$(EXPAND) \
37
+ STEPS=$(STEPS) \
38
+ node capacity_runner.mjs
39
+
40
+ benchmark-charts:
41
+ npm install
42
+ node graphs.js $(shell ls -t raw_data_*.json | head -1) true
43
+
44
+ benchmark-reset:
45
+ node clean.js
46
+
47
+ benchmark-new-revision:
48
+ node update-revision.js
49
+
50
+ benchmark-clean:
51
+ rm -f results_*.json summary_*.json raw_data_*.json *_chart_*.png
52
+
53
+ benchmark-clean-charts:
54
+ rm -f *_chart_*.png
@@ -0,0 +1,221 @@
1
+ import http from 'k6/http';
2
+ import { sleep } from 'k6';
3
+ import { Trend, Counter, Rate } from 'k6/metrics';
4
+
5
+ // Mao
6
+
7
+ const baseUrlToBaseUrlName = {
8
+ 'https://ifh-core-api-dr-benchmark-048d364b548f5d9790082d8ba4fb44d8.us.langgraph.app': 'DRC,API S,OPS S,1 Job per',
9
+ 'https://ifh-benchmarks-ba3f7d811fb6564ebe5248bcd7a0a662.us.langgraph.app': 'API S, OPS S, 1 Job per',
10
+ 'https://wfh-benchmark-distributed-r-1603f73b1a175234b9b0fb1f9beea4f1.us.langgraph.app': 'DR,API S,OPS S,1 Job per',
11
+ }
12
+
13
+ // Metrics
14
+ const runDuration = new Trend('run_duration'); // ms for successful runs
15
+ const runPickUpDuration = new Trend('run_pickup_duration');
16
+ const runReturnDuration = new Trend('run_return_duration');
17
+ const runInsertionDuration = new Trend('run_insertion_duration');
18
+ const runOSSDuration = new Trend('run_oss_duration');
19
+ const successfulRuns = new Counter('successful_runs');
20
+ const failedRuns = new Counter('failed_runs');
21
+ const capacitySuccessRate = new Rate('capacity_success_rate');
22
+
23
+ // Env
24
+ const BASE_URL = __ENV.BASE_URL;
25
+ const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
26
+
27
+ const TARGET = parseInt(__ENV.TARGET || '10');
28
+ const WAIT_SECONDS = parseInt(__ENV.WAIT_SECONDS || '60');
29
+ const SUCCESS_THRESHOLD = parseFloat(__ENV.SUCCESS_THRESHOLD || '0.99');
30
+
31
+ // Agent params
32
+ const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
33
+ const DELAY = parseInt(__ENV.DELAY || '0');
34
+ const EXPAND = parseInt(__ENV.EXPAND || '10');
35
+ const STEPS = parseInt(__ENV.STEPS || '10');
36
+
37
+ // Options
38
+ export const options = {
39
+ scenarios: {
40
+ capacity_single_shot: {
41
+ executor: 'shared-iterations',
42
+ vus: TARGET,
43
+ iterations: TARGET,
44
+ maxDuration: `${Math.max(WAIT_SECONDS + 120, 150)}s`,
45
+ },
46
+ },
47
+ thresholds: {
48
+ 'capacity_success_rate': [`rate>=${SUCCESS_THRESHOLD}`],
49
+ },
50
+ };
51
+
52
+ function headers() {
53
+ const h = { 'Content-Type': 'application/json' };
54
+ if (LANGSMITH_API_KEY) h['x-api-key'] = LANGSMITH_API_KEY;
55
+ return h;
56
+ }
57
+
58
+ function buildPayload() {
59
+ return JSON.stringify({
60
+ assistant_id: 'benchmark',
61
+ input: {
62
+ data_size: DATA_SIZE,
63
+ delay: DELAY,
64
+ expand: EXPAND,
65
+ steps: STEPS,
66
+ },
67
+ config: {
68
+ recursion_limit: STEPS + 2,
69
+ },
70
+ });
71
+ }
72
+
73
+ export default function () {
74
+ // one-shot per iteration: create → wait → poll status once
75
+ const payload = buildPayload();
76
+ const reqHeaders = headers();
77
+
78
+ let threadId;
79
+ let runId;
80
+ let createdAt;
81
+
82
+ try {
83
+ const tRes = http.post(`${BASE_URL}/threads`, payload, {
84
+ headers: reqHeaders,
85
+ timeout: '60s',
86
+ });
87
+ if (tRes.status !== 200) {
88
+ failedRuns.add(1);
89
+ capacitySuccessRate.add(0);
90
+ return;
91
+ }
92
+ const t = tRes.json();
93
+ threadId = t.thread_id;
94
+
95
+ createdAt = new Date().getTime();
96
+ const rRes = http.post(`${BASE_URL}/threads/${threadId}/runs`, payload, {
97
+ headers: reqHeaders,
98
+ timeout: '60s',
99
+ });
100
+ if (rRes.status !== 200) {
101
+ failedRuns.add(1);
102
+ capacitySuccessRate.add(0);
103
+ return;
104
+ }
105
+ const r = rRes.json();
106
+ runId = r.run_id;
107
+ } catch (e) {
108
+ // Any network error counts as a failure — no retries
109
+ failedRuns.add(1);
110
+ capacitySuccessRate.add(0);
111
+ return;
112
+ }
113
+
114
+ // Sleep the configured wait time, exactly once
115
+ sleep(WAIT_SECONDS);
116
+
117
+ // Poll exactly once
118
+ try {
119
+ const gRes = http.get(`${BASE_URL}/threads/${threadId}/runs/${runId}`, {
120
+ headers: reqHeaders,
121
+ timeout: '30s',
122
+ });
123
+ const tRes = http.get(`${BASE_URL}/threads/${threadId}/state`, {
124
+ headers: reqHeaders,
125
+ timeout: '30s',
126
+ });
127
+ if (gRes.status !== 200) {
128
+ failedRuns.add(1);
129
+ capacitySuccessRate.add(0);
130
+ return;
131
+ }
132
+ const run = gRes.json();
133
+ const t = tRes.json();
134
+ if (run.status === 'success') {
135
+ successfulRuns.add(1);
136
+ capacitySuccessRate.add(1);
137
+ try {
138
+ const insertionMs = new Date(run.created_at).getTime() - createdAt;
139
+ if (!Number.isNaN(insertionMs) && insertionMs >= 0) {
140
+ runInsertionDuration.add(insertionMs);
141
+ }
142
+ const durMs = new Date(run.updated_at).getTime() - createdAt;
143
+ if (!Number.isNaN(durMs) && durMs >= 0) {
144
+ runDuration.add(durMs);
145
+ }
146
+ const pickupDurMs = new Date(t.values.start_time).getTime() - new Date(run.created_at).getTime();
147
+ if (!Number.isNaN(pickupDurMs) && pickupDurMs >= 0) {
148
+ runPickUpDuration.add(pickupDurMs);
149
+ }
150
+ // Note: we are missing the time from `set_joint_status` to actually return to the client (ideally this is negligible)
151
+ const returnDurMs = new Date(run.updated_at).getTime() - new Date(t.values.end_time).getTime();
152
+ if (!Number.isNaN(returnDurMs) && returnDurMs >= 0) {
153
+ runReturnDuration.add(returnDurMs);
154
+ }
155
+ const ossDurMs = new Date(t.values.end_time).getTime() - new Date(t.values.start_time).getTime();
156
+ if (!Number.isNaN(ossDurMs) && ossDurMs >= 0) {
157
+ runOSSDuration.add(ossDurMs);
158
+ }
159
+ } catch (_) {
160
+ // ignore duration parsing errors
161
+ }
162
+ } else {
163
+ failedRuns.add(1);
164
+ capacitySuccessRate.add(0);
165
+ }
166
+ } catch (e) {
167
+ failedRuns.add(1);
168
+ capacitySuccessRate.add(0);
169
+ }
170
+ }
171
+
172
+ export function handleSummary(data) {
173
+ const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
174
+
175
+ const total = (data.metrics.successful_runs?.values?.count || 0) + (data.metrics.failed_runs?.values?.count || 0);
176
+ const succ = data.metrics.successful_runs?.values?.count || 0;
177
+ const fail = data.metrics.failed_runs?.values?.count || 0;
178
+ const successRate = total > 0 ? (succ / total) * 100 : 0;
179
+
180
+ function withStats(metric) {
181
+ if (!data.metrics[metric]?.values) return {};
182
+ const vals = data.metrics[metric].values;
183
+ return {
184
+ avg: vals.avg ? vals.avg / 1000 : null,
185
+ p50: vals.med ? vals.med / 1000 : null,
186
+ p95: vals['p(95)'] ? vals['p(95)'] / 1000 : null,
187
+ max: vals.max ? vals.max / 1000 : null,
188
+ };
189
+ }
190
+
191
+ const summary = {
192
+ timestamp: ts,
193
+ settings: {
194
+ baseUrl: BASE_URL,
195
+ baseUrlName: baseUrlToBaseUrlName[BASE_URL],
196
+ target: TARGET,
197
+ waitSeconds: WAIT_SECONDS,
198
+ dataSize: DATA_SIZE,
199
+ delay: DELAY,
200
+ expand: EXPAND,
201
+ steps: STEPS,
202
+ },
203
+ metrics: {
204
+ totalRuns: total,
205
+ successfulRuns: succ,
206
+ failedRuns: fail,
207
+ successRate,
208
+ runDuration: withStats('run_duration'),
209
+ runPickupDuration: withStats('run_pickup_duration'),
210
+ runReturnDuration: withStats('run_return_duration'),
211
+ runInsertionDuration: withStats('run_insertion_duration'),
212
+ runOSSDuration: withStats('run_oss_duration'),
213
+ },
214
+ };
215
+
216
+ const fname = `capacity_summary_t${TARGET}_${ts}.json`;
217
+ return {
218
+ [fname]: JSON.stringify(summary, null, 2),
219
+ stdout: JSON.stringify(summary),
220
+ };
221
+ }
@@ -0,0 +1,307 @@
1
+ /*
2
+ * Adaptive capacity benchmark orchestrator.
3
+ * Ramps TARGET from RAMP_START up to RAMP_END by RAMP_MULTIPLIER.
4
+ * For each level N: optional cleanup → run k6 (N users, 1 run each) → wait summary → decide next.
5
+ * No retries anywhere; errors reduce success rate.
6
+ */
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import { readdirSync, readFileSync, writeFileSync, createReadStream } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import readline from 'node:readline';
12
+ import QuickChart from 'quickchart-js';
13
+
14
+ function envBool(name, def = false) {
15
+ const v = process.env[name];
16
+ if (v === undefined || v === null) return def;
17
+ return String(v).toLowerCase() === 'true';
18
+ }
19
+
20
+ function envInt(name, def) {
21
+ const v = process.env[name];
22
+ if (!v) return def;
23
+ const n = parseInt(v, 10);
24
+ return Number.isFinite(n) ? n : def;
25
+ }
26
+
27
+ function envFloat(name, def) {
28
+ const v = process.env[name];
29
+ if (!v) return def;
30
+ const n = parseFloat(v);
31
+ return Number.isFinite(n) ? n : def;
32
+ }
33
+
34
+ const BASE_URL = process.env.BASE_URL;
35
+ const LANGSMITH_API_KEY = process.env.LANGSMITH_API_KEY;
36
+
37
+ const RAMP_START = envInt('RAMP_START', 10);
38
+ const RAMP_END = envInt('RAMP_END', 1000);
39
+ const RAMP_MULTIPLIER = envFloat('RAMP_MULTIPLIER', 10);
40
+ const WAIT_SECONDS = envInt('WAIT_SECONDS', 60);
41
+ const SUCCESS_THRESHOLD = envFloat('SUCCESS_THRESHOLD', 0.99);
42
+ const CLEAR_BETWEEN_STEPS = envBool('CLEAR_BETWEEN_STEPS', true);
43
+ const CLEAR_DELAY_SECONDS = envInt('CLEAR_DELAY_SECONDS', 5);
44
+
45
+ // Agent params
46
+ const DATA_SIZE = envInt('DATA_SIZE', 1000);
47
+ const DELAY = envInt('DELAY', 0);
48
+ const EXPAND = envInt('EXPAND', 50);
49
+ const STEPS = envInt('STEPS', 10);
50
+
51
+ if (!BASE_URL) {
52
+ console.error('BASE_URL is required');
53
+ process.exit(1);
54
+ }
55
+ if (!(RAMP_MULTIPLIER > 1)) {
56
+ console.error('RAMP_MULTIPLIER must be > 1');
57
+ process.exit(1);
58
+ }
59
+
60
+ function headers() {
61
+ const h = { 'Content-Type': 'application/json' };
62
+ if (LANGSMITH_API_KEY) h['x-api-key'] = LANGSMITH_API_KEY;
63
+ return h;
64
+ }
65
+
66
+ async function cleanThreads() {
67
+ if (!CLEAR_BETWEEN_STEPS) return;
68
+ const hdrs = headers();
69
+ const searchUrl = `${BASE_URL}/threads/search`;
70
+ let totalDeleted = 0;
71
+ // Loop until no more threads
72
+ while (true) {
73
+ const res = await fetch(searchUrl, {
74
+ method: 'POST',
75
+ headers: hdrs,
76
+ body: JSON.stringify({ limit: 1000 }),
77
+ });
78
+ if (!res.ok) {
79
+ console.error(`Cleanup search failed: ${res.status} ${res.statusText}`);
80
+ break;
81
+ }
82
+ const threads = await res.json();
83
+ if (!Array.isArray(threads) || threads.length === 0) break;
84
+ for (const t of threads) {
85
+ try {
86
+ const del = await fetch(`${BASE_URL}/threads/${t.thread_id}`, {
87
+ method: 'DELETE',
88
+ headers: hdrs,
89
+ });
90
+ if (del.ok) totalDeleted++;
91
+ } catch (e) {
92
+ // Ignore delete errors; do not retry
93
+ }
94
+ }
95
+ }
96
+ if (CLEAR_DELAY_SECONDS > 0) {
97
+ await new Promise((r) => setTimeout(r, CLEAR_DELAY_SECONDS * 1000));
98
+ }
99
+ console.log(`Cleanup completed. Deleted ~${totalDeleted} threads.`);
100
+ }
101
+
102
+ function runK6(target) {
103
+ const env = {
104
+ ...process.env,
105
+ BASE_URL,
106
+ LANGSMITH_API_KEY,
107
+ TARGET: String(target),
108
+ WAIT_SECONDS: String(WAIT_SECONDS),
109
+ SUCCESS_THRESHOLD: String(SUCCESS_THRESHOLD),
110
+ DATA_SIZE: String(DATA_SIZE),
111
+ DELAY: String(DELAY),
112
+ EXPAND: String(EXPAND),
113
+ STEPS: String(STEPS),
114
+ };
115
+ console.log(`Running k6 with TARGET=${target}`);
116
+ // Also write raw JSON stream for per-stage histograms
117
+ const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
118
+ const rawOut = `capacity_raw_t${target}_${ts}.json`;
119
+ // We rely on handleSummary to write capacity_summary_t${TARGET}_<ts>.json
120
+ execFileSync('k6', ['run', '--out', `json=${rawOut}`, 'capacity_k6.js'], {
121
+ cwd: process.cwd(),
122
+ env,
123
+ stdio: 'inherit',
124
+ });
125
+ return { rawOut, ts };
126
+ }
127
+
128
+ function loadSummaryForTarget(target) {
129
+ const files = readdirSync(process.cwd())
130
+ .filter((f) => f.startsWith(`capacity_summary_t${target}_`) && f.endsWith('.json'))
131
+ .sort();
132
+ if (files.length === 0) {
133
+ throw new Error(`No capacity summary file found for target ${target}`);
134
+ }
135
+ const latest = files[files.length - 1];
136
+ const content = readFileSync(join(process.cwd(), latest), 'utf-8');
137
+ return JSON.parse(content);
138
+ }
139
+
140
+ async function main() {
141
+ let n = RAMP_START;
142
+ let lastSuccess = null; // { target, avgDurationSeconds, successRate }
143
+ let failedStep = null; // { target, successRate }
144
+
145
+ while (n <= RAMP_END) {
146
+ console.log(`\n=== Capacity step: N=${n} ===`);
147
+ if (CLEAR_BETWEEN_STEPS) {
148
+ console.log('Clearing threads before step...');
149
+ await cleanThreads();
150
+ }
151
+
152
+ try {
153
+ const { rawOut, ts } = runK6(n);
154
+ try {
155
+ await generateHistogramsForStage(rawOut, n, ts);
156
+ } catch (e) {
157
+ console.error(`Failed to generate histograms for N=${n}:`, e?.message || e);
158
+ }
159
+ } catch (e) {
160
+ console.error(`k6 run failed at N=${n}:`, e?.message || e);
161
+ // Treat as failure for this step
162
+ }
163
+
164
+ let successRate = 0;
165
+ let avgDurationSeconds = null;
166
+ try {
167
+ const summary = loadSummaryForTarget(n);
168
+ const s = summary?.metrics?.successRate; // percent
169
+ successRate = Number.isFinite(s) ? s / 100 : 0;
170
+ avgDurationSeconds = summary?.metrics?.averageDurationSeconds ?? null;
171
+ console.log(`Step N=${n} successRate=${(successRate * 100).toFixed(2)}% avgDur=${avgDurationSeconds ?? 'n/a'}s`);
172
+ } catch (e) {
173
+ console.error(`Failed to read summary for N=${n}:`, e?.message || e);
174
+ }
175
+
176
+ if (successRate >= SUCCESS_THRESHOLD) {
177
+ lastSuccess = { target: n, avgDurationSeconds, successRate };
178
+ // next n
179
+ const next = Math.floor(n * RAMP_MULTIPLIER);
180
+ if (next <= n) {
181
+ console.log('Next ramp value would not increase; stopping.');
182
+ break;
183
+ }
184
+ n = next;
185
+ } else {
186
+ failedStep = { target: n, successRate };
187
+ break;
188
+ }
189
+ }
190
+
191
+ const ts = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
192
+ const report = {
193
+ timestamp: ts,
194
+ ramp: { start: RAMP_START, end: RAMP_END, multiplier: RAMP_MULTIPLIER },
195
+ waitSeconds: WAIT_SECONDS,
196
+ threshold: SUCCESS_THRESHOLD,
197
+ last_success_target: lastSuccess?.target ?? 0,
198
+ last_success_avg_duration_seconds: lastSuccess?.avgDurationSeconds ?? null,
199
+ last_success_rate: lastSuccess?.successRate ?? null,
200
+ failed_target: failedStep?.target ?? null,
201
+ failed_success_rate: failedStep?.successRate ?? null,
202
+ };
203
+ const fname = `capacity_report_${ts}.json`;
204
+ writeFileSync(join(process.cwd(), fname), JSON.stringify(report, null, 2));
205
+
206
+ // Export selected fields as GitHub Action outputs if available
207
+ if (process.env.GITHUB_OUTPUT) {
208
+ const out = [
209
+ `last_success_target=${report.last_success_target}`,
210
+ `last_success_avg_duration_seconds=${report.last_success_avg_duration_seconds}`,
211
+ `failed_target=${report.failed_target}`,
212
+ `failed_success_rate=${report.failed_success_rate}`,
213
+ ].join('\n');
214
+ writeFileSync(process.env.GITHUB_OUTPUT, `${out}\n`, { flag: 'a' });
215
+ }
216
+
217
+ console.log('=== Capacity Benchmark Report ===');
218
+ console.log(`Last successful step: ${report.last_success_target}`);
219
+ console.log(`Average duration (s) at success: ${report.last_success_avg_duration_seconds}`);
220
+ console.log(`Failed step: ${report.failed_target} with success rate: ${report.failed_success_rate}`);
221
+ }
222
+
223
+ main().catch((e) => {
224
+ console.error('Fatal error in capacity runner:', e?.stack || e?.message || e);
225
+ process.exit(1);
226
+ });
227
+
228
+ // Build and save histogram charts for one stage from raw K6 JSON
229
+ async function generateHistogramsForStage(rawFile, target, ts) {
230
+ // Parse streaming JSONL from k6 --out json
231
+ const metrics = {
232
+ run_duration: [],
233
+ run_pickup_duration: [],
234
+ run_return_duration: [],
235
+ run_insertion_duration: [],
236
+ run_oss_duration: [],
237
+ };
238
+
239
+ await new Promise((resolve, reject) => {
240
+ const rl = readline.createInterface({ input: createReadStream(join(process.cwd(), rawFile), { encoding: 'utf-8' }), crlfDelay: Infinity });
241
+ rl.on('line', (line) => {
242
+ try {
243
+ const entry = JSON.parse(line);
244
+ if (entry.type === 'Point') {
245
+ const name = entry.metric;
246
+ if (name in metrics) {
247
+ const v = entry?.data?.value;
248
+ if (Number.isFinite(v)) metrics[name].push(v);
249
+ }
250
+ }
251
+ } catch (_) {
252
+ // ignore parse errors
253
+ }
254
+ });
255
+ rl.on('close', resolve);
256
+ rl.on('error', reject);
257
+ });
258
+
259
+ // Build pie chart for component breakdown based on average seconds
260
+ const avg = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0);
261
+ const avgInsertionS = avg(metrics.run_insertion_duration) / 1000;
262
+ const avgPickupS = avg(metrics.run_pickup_duration) / 1000;
263
+ const avgOssS = avg(metrics.run_oss_duration) / 1000;
264
+ const avgReturnS = avg(metrics.run_return_duration) / 1000;
265
+ const parts = [avgInsertionS, avgPickupS, avgOssS, avgReturnS];
266
+ const totalParts = parts.reduce((a, b) => a + b, 0);
267
+ if (totalParts > 0) {
268
+ const chart = new QuickChart();
269
+ chart.setWidth(700);
270
+ chart.setHeight(500);
271
+ chart.setFormat('png');
272
+ chart.setConfig({
273
+ type: 'pie',
274
+ data: {
275
+ labels: ['Insertion', 'Pickup', 'OSS', 'Return'],
276
+ datasets: [{
277
+ label: `Breakdown of Run Duration (N=${target})`,
278
+ data: parts.map((v) => Number(v.toFixed(4))),
279
+ backgroundColor: [
280
+ 'rgba(255, 99, 132, 0.6)',
281
+ 'rgba(54, 162, 235, 0.6)',
282
+ 'rgba(255, 206, 86, 0.6)',
283
+ 'rgba(75, 192, 192, 0.6)',
284
+ ],
285
+ borderColor: [
286
+ 'rgba(255, 99, 132, 1)',
287
+ 'rgba(54, 162, 235, 1)',
288
+ 'rgba(255, 206, 86, 1)',
289
+ 'rgba(75, 192, 192, 1)',
290
+ ],
291
+ borderWidth: 1,
292
+ }],
293
+ },
294
+ options: {
295
+ plugins: {
296
+ title: { display: true, text: `Run Duration Breakdown (s) — N=${target}` },
297
+ legend: { position: 'right' },
298
+ tooltip: { callbacks: { label: (ctx) => `${ctx.label}: ${ctx.parsed.toFixed(3)}s` } },
299
+ },
300
+ },
301
+ });
302
+ const pieBuf = await chart.toBinary();
303
+ const piePath = join(process.cwd(), `capacity_pie_breakdown_t${target}_${ts}.png`);
304
+ writeFileSync(piePath, pieBuf);
305
+ console.log(`Saved pie chart: ${piePath}`);
306
+ }
307
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Parse benchmark JSON results and send to Datadog.
4
+ Install: pip install datadog-api-client
5
+ """
6
+
7
+ import argparse
8
+ import glob
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ from datadog_api_client import ApiClient, Configuration
14
+ from datadog_api_client.v2.api.logs_api import LogsApi
15
+ from datadog_api_client.v2.model.http_log import HTTPLog
16
+ from datadog_api_client.v2.model.http_log_item import HTTPLogItem
17
+
18
+
19
+ def send_benchmark_results(
20
+ benchmark_data, common_labels=None, dd_site="us5.datadoghq.com", api_key=None
21
+ ):
22
+ """
23
+ Send benchmark JSON to Datadog.
24
+
25
+ Args:
26
+ benchmark_data: Dict containing 'settings' and 'metrics' from benchmark
27
+ common_labels: Additional labels (e.g., base_url)
28
+ """
29
+ configuration = Configuration()
30
+ configuration.server_variables["site"] = dd_site
31
+ configuration.api_key["apiKeyAuth"] = api_key
32
+
33
+ if common_labels:
34
+ benchmark_data["labels"] = common_labels
35
+
36
+ log_item = HTTPLogItem(
37
+ ddsource="benchmark",
38
+ ddtags=f"env:benchmarking,base_url:{common_labels.get('base_url', 'unknown')}"
39
+ if common_labels
40
+ else "env:benchmarking",
41
+ hostname=os.getenv("HOSTNAME", "localhost"),
42
+ message=json.dumps(benchmark_data),
43
+ service="benchmark-results",
44
+ )
45
+
46
+ with ApiClient(configuration) as api_client:
47
+ api_instance = LogsApi(api_client)
48
+ body = HTTPLog([log_item])
49
+
50
+ api_instance.submit_log(body=body)
51
+
52
+
53
+ def process_benchmark_file(
54
+ json_file, common_labels=None, dd_site="us5.datadoghq.com", api_key=None
55
+ ):
56
+ """
57
+ Read benchmark JSON file and send to Datadog.
58
+
59
+ Args:
60
+ json_file: Path to benchmark results JSON file
61
+ common_labels: Additional labels for all metrics
62
+ """
63
+ with open(json_file) as f:
64
+ data = json.load(f)
65
+
66
+ send_benchmark_results(data, common_labels, dd_site, api_key)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ parser = argparse.ArgumentParser(description="Send benchmark results to Datadog")
71
+ parser.add_argument(
72
+ "benchmark_file", type=str, help="Path to benchmark results file"
73
+ )
74
+ args = parser.parse_args()
75
+
76
+ DD_API_KEY = os.getenv("DD_API_KEY")
77
+ if not DD_API_KEY:
78
+ sys.exit(1)
79
+
80
+ DD_SITE = os.getenv("DD_SITE", "us5.datadoghq.com")
81
+
82
+ common_labels = {"base_url": os.getenv("BASE_URL")}
83
+
84
+ for file in glob.glob(args.benchmark_file):
85
+ process_benchmark_file(file, common_labels, DD_SITE, DD_API_KEY)
@@ -0,0 +1 @@
1
+ __version__ = "0.4.38"
@@ -24,7 +24,8 @@ async def meta_info(request: ApiRequest):
24
24
  "flags": {
25
25
  "assistants": True,
26
26
  "crons": plus and config.FF_CRONS_ENABLED,
27
- "langsmith": bool(config.LANGSMITH_API_KEY) and bool(config.TRACING),
27
+ "langsmith": bool(config.LANGSMITH_CONTROL_PLANE_API_KEY)
28
+ and bool(config.TRACING),
28
29
  "langsmith_tracing_replicas": True,
29
30
  },
30
31
  "host": {