agent-hitch 0.2.16 → 0.2.17
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.
- package/README.md +7 -28
- package/README.zh-CN.md +6 -26
- package/dist/scripts/benchmark-dataset-verification.js +118 -0
- package/dist/scripts/benchmark-dataset-verification.js.map +1 -0
- package/dist/scripts/canary-external-endpoint.js +56 -0
- package/dist/scripts/canary-external-endpoint.js.map +1 -0
- package/dist/scripts/canary-native-training.js +8 -6
- package/dist/scripts/canary-native-training.js.map +1 -1
- package/dist/scripts/canary-remote-training.js +8 -6
- package/dist/scripts/canary-remote-training.js.map +1 -1
- package/dist/scripts/check-architecture.js +1 -1
- package/dist/scripts/check-architecture.js.map +1 -1
- package/dist/src/adapters/providers/codex.js +5 -2
- package/dist/src/adapters/providers/codex.js.map +1 -1
- package/dist/src/adapters/providers/model-call.js +1 -2
- package/dist/src/adapters/providers/model-call.js.map +1 -1
- package/dist/src/adapters/providers/training-tool.js +2 -0
- package/dist/src/adapters/providers/training-tool.js.map +1 -1
- package/dist/src/backends/harbor/artifact-pins.js +42 -0
- package/dist/src/backends/harbor/artifact-pins.js.map +1 -0
- package/dist/src/backends/harbor/backend.js +16 -8
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/backends/harbor/bound-credential-helper.js +24 -0
- package/dist/src/backends/harbor/bound-credential-helper.js.map +1 -0
- package/dist/src/backends/harbor/credential-snapshot.js +71 -0
- package/dist/src/backends/harbor/credential-snapshot.js.map +1 -0
- package/dist/src/backends/harbor/environment-config.js +25 -2
- package/dist/src/backends/harbor/environment-config.js.map +1 -1
- package/dist/src/backends/harbor/environment-files.js +143 -0
- package/dist/src/backends/harbor/environment-files.js.map +1 -0
- package/dist/src/backends/harbor/evaluation-binding.js +160 -0
- package/dist/src/backends/harbor/evaluation-binding.js.map +1 -0
- package/dist/src/backends/harbor/host-credential-helper.js +5 -0
- package/dist/src/backends/harbor/host-credential-helper.js.map +1 -1
- package/dist/src/backends/harbor/index.js +2 -0
- package/dist/src/backends/harbor/index.js.map +1 -1
- package/dist/src/backends/harbor/model-proxy-config.js +3 -18
- package/dist/src/backends/harbor/model-proxy-config.js.map +1 -1
- package/dist/src/backends/harbor/portable-regrade.js +15 -3
- package/dist/src/backends/harbor/portable-regrade.js.map +1 -1
- package/dist/src/backends/harbor/python-runtime.js +71 -0
- package/dist/src/backends/harbor/python-runtime.js.map +1 -0
- package/dist/src/backends/harbor/tools.js +6 -4
- package/dist/src/backends/harbor/tools.js.map +1 -1
- package/dist/src/backends/index.js +3 -0
- package/dist/src/backends/index.js.map +1 -1
- package/dist/src/cli/arguments.js +30 -30
- package/dist/src/cli/arguments.js.map +1 -1
- package/dist/src/cli/commands/capabilities.js +7 -3
- package/dist/src/cli/commands/capabilities.js.map +1 -1
- package/dist/src/cli/commands/daemon.js +0 -17
- package/dist/src/cli/commands/daemon.js.map +1 -1
- package/dist/src/cli/commands/eval-binding.js +32 -0
- package/dist/src/cli/commands/eval-binding.js.map +1 -0
- package/dist/src/cli/commands/eval.js +21 -15
- package/dist/src/cli/commands/eval.js.map +1 -1
- package/dist/src/cli/commands/models.js +32 -45
- package/dist/src/cli/commands/models.js.map +1 -1
- package/dist/src/cli/commands/run.js +0 -35
- package/dist/src/cli/commands/run.js.map +1 -1
- package/dist/src/cli/main.js +0 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/output.js +5 -16
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/control-plane/eval-records.js +3 -1
- package/dist/src/control-plane/eval-records.js.map +1 -1
- package/dist/src/control-plane/eval-recovery.js +4 -1
- package/dist/src/control-plane/eval-recovery.js.map +1 -1
- package/dist/src/control-plane/eval-scheduler.js +16 -8
- package/dist/src/control-plane/eval-scheduler.js.map +1 -1
- package/dist/src/control-plane/index.js +0 -2
- package/dist/src/control-plane/index.js.map +1 -1
- package/dist/src/control-plane/model-capture-planning.js +3 -4
- package/dist/src/control-plane/model-capture-planning.js.map +1 -1
- package/dist/src/control-plane/remote-model-routes.js +4 -24
- package/dist/src/control-plane/remote-model-routes.js.map +1 -1
- package/dist/src/control-plane/remote-offer-contract.js +2 -2
- package/dist/src/control-plane/remote-offer-contract.js.map +1 -1
- package/dist/src/control-plane/remote-result-transport.js +7 -8
- package/dist/src/control-plane/remote-result-transport.js.map +1 -1
- package/dist/src/control-plane/remote-work-coordinator.js +2 -2
- package/dist/src/control-plane/remote-work-coordinator.js.map +1 -1
- package/dist/src/control-plane/remote-work-recovery.js +2 -2
- package/dist/src/control-plane/remote-work-recovery.js.map +1 -1
- package/dist/src/control-plane/remote-worker-protocol.js +1 -1
- package/dist/src/control-plane/remote-worker-protocol.js.map +1 -1
- package/dist/src/control-plane/remote-worker-runner.js +21 -6
- package/dist/src/control-plane/remote-worker-runner.js.map +1 -1
- package/dist/src/control-plane/remote-workers.js +2 -2
- package/dist/src/control-plane/remote-workers.js.map +1 -1
- package/dist/src/control-plane/rerun-recovery.js +1 -1
- package/dist/src/control-plane/rerun-recovery.js.map +1 -1
- package/dist/src/control-plane/rerun-scheduler.js +21 -14
- package/dist/src/control-plane/rerun-scheduler.js.map +1 -1
- package/dist/src/control-plane/rerun-source.js +5 -2
- package/dist/src/control-plane/rerun-source.js.map +1 -1
- package/dist/src/control-plane/rerun-submission.js +7 -5
- package/dist/src/control-plane/rerun-submission.js.map +1 -1
- package/dist/src/control-plane/synthetic-result.js +3 -2
- package/dist/src/control-plane/synthetic-result.js.map +1 -1
- package/dist/src/control-plane/worker-model-relay.js +1 -1
- package/dist/src/control-plane/worker-model-relay.js.map +1 -1
- package/dist/src/controller-runtime/hash.js +6 -0
- package/dist/src/controller-runtime/hash.js.map +1 -1
- package/dist/src/daemon/client.js +12 -35
- package/dist/src/daemon/client.js.map +1 -1
- package/dist/src/daemon/evaluation-binding.js +33 -0
- package/dist/src/daemon/evaluation-binding.js.map +1 -0
- package/dist/src/daemon/evaluation-submission.js +27 -0
- package/dist/src/daemon/evaluation-submission.js.map +1 -0
- package/dist/src/daemon/index.js +1 -0
- package/dist/src/daemon/index.js.map +1 -1
- package/dist/src/daemon/json-stream.js +98 -0
- package/dist/src/daemon/json-stream.js.map +1 -0
- package/dist/src/daemon/scheduler.js +1 -4
- package/dist/src/daemon/scheduler.js.map +1 -1
- package/dist/src/daemon/server.js +14 -54
- package/dist/src/daemon/server.js.map +1 -1
- package/dist/src/domain/endpoints.js +2 -0
- package/dist/src/domain/endpoints.js.map +1 -0
- package/dist/src/domain/evaluation-binding.js +5 -0
- package/dist/src/domain/evaluation-binding.js.map +1 -0
- package/dist/src/domain/index.js +2 -0
- package/dist/src/domain/index.js.map +1 -1
- package/dist/src/domain/runs.js.map +1 -1
- package/dist/src/evals/benchmark-adapter-manifest.js +95 -32
- package/dist/src/evals/benchmark-adapter-manifest.js.map +1 -1
- package/dist/src/evals/benchmark-verification.js +106 -0
- package/dist/src/evals/benchmark-verification.js.map +1 -0
- package/dist/src/evals/collect-only-rerun.js +3 -2
- package/dist/src/evals/collect-only-rerun.js.map +1 -1
- package/dist/src/evals/directory.js +9 -1
- package/dist/src/evals/directory.js.map +1 -1
- package/dist/src/evals/endpoint-attempts.js +112 -0
- package/dist/src/evals/endpoint-attempts.js.map +1 -0
- package/dist/src/evals/endpoint-usage.js +8 -0
- package/dist/src/evals/endpoint-usage.js.map +1 -0
- package/dist/src/evals/environment-binding.js +9 -0
- package/dist/src/evals/environment-binding.js.map +1 -0
- package/dist/src/evals/eval-artifact-planning.js +10 -1
- package/dist/src/evals/eval-artifact-planning.js.map +1 -1
- package/dist/src/evals/eval-finalization.js +2 -3
- package/dist/src/evals/eval-finalization.js.map +1 -1
- package/dist/src/evals/eval-logical-plan.js +2 -0
- package/dist/src/evals/eval-logical-plan.js.map +1 -1
- package/dist/src/evals/eval-result-builder.js +4 -0
- package/dist/src/evals/eval-result-builder.js.map +1 -1
- package/dist/src/evals/eval-state.js +22 -0
- package/dist/src/evals/eval-state.js.map +1 -0
- package/dist/src/evals/execution-plan.js +6 -2
- package/dist/src/evals/execution-plan.js.map +1 -1
- package/dist/src/evals/index.js +5 -1
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/infrastructure-retry.js +2 -0
- package/dist/src/evals/infrastructure-retry.js.map +1 -1
- package/dist/src/evals/model-capture-runtime.js +9 -13
- package/dist/src/evals/model-capture-runtime.js.map +1 -1
- package/dist/src/evals/native-phase-evidence.js +8 -2
- package/dist/src/evals/native-phase-evidence.js.map +1 -1
- package/dist/src/evals/planned-execution.js +7 -2
- package/dist/src/evals/planned-execution.js.map +1 -1
- package/dist/src/evals/planned-progress-publisher.js +18 -5
- package/dist/src/evals/planned-progress-publisher.js.map +1 -1
- package/dist/src/evals/planned-retry-execution.js +1 -0
- package/dist/src/evals/planned-retry-execution.js.map +1 -1
- package/dist/src/evals/preparation-rerun.js +0 -1
- package/dist/src/evals/preparation-rerun.js.map +1 -1
- package/dist/src/evals/progress.js +92 -7
- package/dist/src/evals/progress.js.map +1 -1
- package/dist/src/evals/records.js +12 -2
- package/dist/src/evals/records.js.map +1 -1
- package/dist/src/evals/recovery.js +2 -0
- package/dist/src/evals/recovery.js.map +1 -1
- package/dist/src/evals/remote-rerun-execution.js +1 -3
- package/dist/src/evals/remote-rerun-execution.js.map +1 -1
- package/dist/src/evals/remote-verifier-rerun.js +1 -1
- package/dist/src/evals/remote-verifier-rerun.js.map +1 -1
- package/dist/src/evals/request.js +14 -45
- package/dist/src/evals/request.js.map +1 -1
- package/dist/src/evals/rerun-observation.js +31 -0
- package/dist/src/evals/rerun-observation.js.map +1 -0
- package/dist/src/evals/rerun.js +23 -28
- package/dist/src/evals/rerun.js.map +1 -1
- package/dist/src/evals/service.js +45 -42
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/shared-runtime.js +125 -0
- package/dist/src/evals/shared-runtime.js.map +1 -0
- package/dist/src/evals/trial-import.js +10 -2
- package/dist/src/evals/trial-import.js.map +1 -1
- package/dist/src/evals/trial-publication-recovery.js +1 -1
- package/dist/src/evals/trial-publication-recovery.js.map +1 -1
- package/dist/src/evals/verifier-artifacts.js +1 -0
- package/dist/src/evals/verifier-artifacts.js.map +1 -1
- package/dist/src/evals/verifier-only-rerun.js +1 -1
- package/dist/src/evals/verifier-only-rerun.js.map +1 -1
- package/dist/src/evals/verifier-rerun-result.js +3 -2
- package/dist/src/evals/verifier-rerun-result.js.map +1 -1
- package/dist/src/evals/verifier-score-artifacts.js +9 -1
- package/dist/src/evals/verifier-score-artifacts.js.map +1 -1
- package/dist/src/foundation/config.js +0 -5
- package/dist/src/foundation/config.js.map +1 -1
- package/dist/src/inference/index.js +3 -22
- package/dist/src/inference/index.js.map +1 -1
- package/dist/src/inference/lock.js +44 -130
- package/dist/src/inference/lock.js.map +1 -1
- package/dist/src/model-access/endpoint-contract.js +127 -0
- package/dist/src/model-access/endpoint-contract.js.map +1 -0
- package/dist/src/model-access/endpoint-credentials.js +54 -0
- package/dist/src/model-access/endpoint-credentials.js.map +1 -0
- package/dist/src/model-access/endpoint-probe-stream.js +66 -0
- package/dist/src/model-access/endpoint-probe-stream.js.map +1 -0
- package/dist/src/model-access/endpoint-verification.js +76 -0
- package/dist/src/model-access/endpoint-verification.js.map +1 -0
- package/dist/src/model-access/endpoints.js +105 -0
- package/dist/src/model-access/endpoints.js.map +1 -0
- package/dist/src/model-access/index.js +5 -1
- package/dist/src/model-access/index.js.map +1 -1
- package/dist/src/model-access/proxy.js +57 -14
- package/dist/src/model-access/proxy.js.map +1 -1
- package/dist/src/model-access/remote-model.js +21 -29
- package/dist/src/model-access/remote-model.js.map +1 -1
- package/dist/src/model-access/usage.js +30 -0
- package/dist/src/model-access/usage.js.map +1 -0
- package/dist/src/runs/endpoint-runtime.js +40 -0
- package/dist/src/runs/endpoint-runtime.js.map +1 -0
- package/dist/src/runs/executor.js +28 -42
- package/dist/src/runs/executor.js.map +1 -1
- package/dist/src/runs/local-inference-environment.js +5 -53
- package/dist/src/runs/local-inference-environment.js.map +1 -1
- package/dist/src/runs/manifest.js +1 -0
- package/dist/src/runs/manifest.js.map +1 -1
- package/dist/src/runs/outcome.js +3 -10
- package/dist/src/runs/outcome.js.map +1 -1
- package/dist/src/runs/queued.js +3 -2
- package/dist/src/runs/queued.js.map +1 -1
- package/dist/src/runs/records.js +3 -0
- package/dist/src/runs/records.js.map +1 -1
- package/dist/src/runs/request.js +10 -38
- package/dist/src/runs/request.js.map +1 -1
- package/dist/src/workers/remote-harbor-work-spec.js +8 -10
- package/dist/src/workers/remote-harbor-work-spec.js.map +1 -1
- package/dist/src/workers/remote-harbor-worker.js +3 -0
- package/dist/src/workers/remote-harbor-worker.js.map +1 -1
- package/docs/evidence/eval-preparation-2026-10-09.json +393 -0
- package/docs/evidence/eval-preparation-2026-10-09.md +178 -0
- package/docs/local-model-inference-spec.zh-CN.md +4 -663
- package/docs/resource-storage.zh-CN.md +116 -0
- package/docs/schemas/endpoint-evaluation-attempt.schema.json +29 -0
- package/docs/schemas/eval-artifact-pin.schema.json +14 -0
- package/docs/schemas/eval-execution-plan.schema.json +486 -105
- package/docs/schemas/eval-progress.schema.json +1 -1
- package/docs/schemas/eval-request.schema.json +149 -18
- package/docs/schemas/eval-rerun-submission.schema.json +8 -1
- package/docs/schemas/eval-result.schema.json +310 -56
- package/docs/schemas/evaluation-binding.schema.json +15 -0
- package/docs/schemas/external-endpoint-binding.schema.json +153 -0
- package/docs/schemas/external-endpoint-config.schema.json +93 -0
- package/docs/schemas/inference-execution-evidence.schema.json +253 -3
- package/docs/schemas/inference-lock-v3.schema.json +6 -0
- package/docs/schemas/inference-lock.schema.json +842 -341
- package/docs/schemas/ordered-eval-control.schema.json +7 -1
- package/docs/schemas/ready-endpoint-binding.schema.json +201 -0
- package/docs/schemas/run-manifest.schema.json +6 -0
- package/docs/schemas/run-request.schema.json +122 -20
- package/examples/external-inference/.env.example +16 -0
- package/examples/external-inference/README.md +203 -0
- package/examples/external-inference/accept.mjs +297 -0
- package/examples/external-inference/compose.gateway.yaml +119 -0
- package/examples/external-inference/compose.single.yaml +64 -0
- package/examples/external-inference/compose.tp2.yaml +15 -0
- package/examples/external-inference/deployment.example.json +58 -0
- package/examples/external-inference/endpoint.chat-completions.example.json +13 -0
- package/examples/external-inference/endpoint.responses.example.json +13 -0
- package/integrations/harbor/hitch_build_inputs.py +213 -0
- package/integrations/harbor/hitch_build_process.py +126 -0
- package/integrations/harbor/hitch_compose_preparation.py +274 -0
- package/integrations/harbor/hitch_harbor_agent.py +25 -39
- package/integrations/harbor/hitch_harbor_environment.py +96 -3
- package/integrations/harbor/hitch_image_cache.py +357 -0
- package/integrations/harbor/hitch_python_runtime.py +303 -0
- package/integrations/harbor/hitch_shared_runtime.py +123 -0
- package/integrations/model-call/cli.js +11 -10
- package/integrations/training-tool/cli.js +5 -3
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +15 -4
- package/dist/src/cli/commands/local.js +0 -130
- package/dist/src/cli/commands/local.js.map +0 -1
- package/dist/src/cli/commands/model-node.js +0 -26
- package/dist/src/cli/commands/model-node.js.map +0 -1
- package/dist/src/control-plane/inference-manager.js +0 -357
- package/dist/src/control-plane/inference-manager.js.map +0 -1
- package/dist/src/control-plane/managed-gateway-receipt.js +0 -26
- package/dist/src/control-plane/managed-gateway-receipt.js.map +0 -1
- package/dist/src/control-plane/managed-service-recovery.js +0 -158
- package/dist/src/control-plane/managed-service-recovery.js.map +0 -1
- package/dist/src/daemon/inference-prepare.js +0 -47
- package/dist/src/daemon/inference-prepare.js.map +0 -1
- package/dist/src/inference/daemon-capacity.js +0 -41
- package/dist/src/inference/daemon-capacity.js.map +0 -1
- package/dist/src/inference/device-reservation.js +0 -58
- package/dist/src/inference/device-reservation.js.map +0 -1
- package/dist/src/inference/doctor.js +0 -95
- package/dist/src/inference/doctor.js.map +0 -1
- package/dist/src/inference/generation-recovery.js +0 -83
- package/dist/src/inference/generation-recovery.js.map +0 -1
- package/dist/src/inference/managed-launcher.js +0 -35
- package/dist/src/inference/managed-launcher.js.map +0 -1
- package/dist/src/inference/materialize.js +0 -66
- package/dist/src/inference/materialize.js.map +0 -1
- package/dist/src/inference/model-location.js +0 -34
- package/dist/src/inference/model-location.js.map +0 -1
- package/dist/src/inference/node-client.js +0 -167
- package/dist/src/inference/node-client.js.map +0 -1
- package/dist/src/inference/node-registry.js +0 -79
- package/dist/src/inference/node-registry.js.map +0 -1
- package/dist/src/inference/observation.js +0 -79
- package/dist/src/inference/observation.js.map +0 -1
- package/dist/src/inference/preflight.js +0 -132
- package/dist/src/inference/preflight.js.map +0 -1
- package/dist/src/inference/process-sglang.js +0 -169
- package/dist/src/inference/process-sglang.js.map +0 -1
- package/dist/src/inference/protocol-probe.js +0 -29
- package/dist/src/inference/protocol-probe.js.map +0 -1
- package/dist/src/inference/remote-model-store.js +0 -24
- package/dist/src/inference/remote-model-store.js.map +0 -1
- package/dist/src/inference/runtime-catalog.js +0 -41
- package/dist/src/inference/runtime-catalog.js.map +0 -1
- package/dist/src/inference/runtime-store.js +0 -93
- package/dist/src/inference/runtime-store.js.map +0 -1
- package/dist/src/inference/service-attachment.js +0 -46
- package/dist/src/inference/service-attachment.js.map +0 -1
- package/dist/src/inference/service-inspection.js +0 -38
- package/dist/src/inference/service-inspection.js.map +0 -1
- package/dist/src/inference/sglang.js +0 -279
- package/dist/src/inference/sglang.js.map +0 -1
- package/dist/src/inference/supervisor.js +0 -460
- package/dist/src/inference/supervisor.js.map +0 -1
- package/dist/src/model-access/local-gateway-request.js +0 -139
- package/dist/src/model-access/local-gateway-request.js.map +0 -1
- package/dist/src/model-access/local-gateway.js +0 -323
- package/dist/src/model-access/local-gateway.js.map +0 -1
- package/dist/src/runs/local-inference-run.js +0 -53
- package/dist/src/runs/local-inference-run.js.map +0 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One-shot standard HTTP acceptance tool. No SDK, retry, engine management, or GPU accounting.
|
|
3
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const help = `Usage: node accept.mjs --endpoint endpoint.json --deployment deployment.json --out artifacts/acceptance.json
|
|
9
|
+
--budget 1024 Output budget, passed unchanged to every generation
|
|
10
|
+
--concurrent 4 Number of simultaneous independent requests (at least 2)
|
|
11
|
+
--timeout-ms 180000 Per-request deadline, including stream consumption
|
|
12
|
+
--worker-metrics URL Repeat for each worker's /metrics endpoint
|
|
13
|
+
--gateway-metrics URL Optional official Gateway Prometheus endpoint
|
|
14
|
+
--expect-workers 2 Require positive measured request-count deltas on this many workers
|
|
15
|
+
|
|
16
|
+
Requires Node.js 22+. Endpoint api is explicitly responses or chat-completions.
|
|
17
|
+
The report contains prompts, generated output, tool arguments/results, and usage.
|
|
18
|
+
An HTTP report does not attest GPU topology, loaded weight identity, or real Harness behavior.
|
|
19
|
+
`;
|
|
20
|
+
const args = { budget: 1024, concurrent: 4, timeoutMs: 180000, workerMetrics: [] };
|
|
21
|
+
const names = { '--endpoint': 'endpoint', '--deployment': 'deployment', '--out': 'out', '--budget': 'budget', '--concurrent': 'concurrent', '--timeout-ms': 'timeoutMs', '--expect-workers': 'expectWorkers', '--gateway-metrics': 'gatewayMetrics' };
|
|
22
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
23
|
+
const flag = process.argv[i];
|
|
24
|
+
if (flag === '--help') { console.log(help); process.exit(0); }
|
|
25
|
+
const value = process.argv[++i];
|
|
26
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`);
|
|
27
|
+
if (flag === '--worker-metrics') args.workerMetrics.push(value);
|
|
28
|
+
else if (names[flag]) args[names[flag]] = value;
|
|
29
|
+
else throw new Error(`Unknown option ${flag}`);
|
|
30
|
+
}
|
|
31
|
+
for (const field of ['endpoint', 'deployment', 'out']) if (!args[field]) throw new Error(`--${field} is required.\n${help}`);
|
|
32
|
+
for (const field of ['budget', 'concurrent', 'timeoutMs', 'expectWorkers']) {
|
|
33
|
+
if (args[field] === undefined) continue;
|
|
34
|
+
args[field] = Number(args[field]);
|
|
35
|
+
if (!Number.isSafeInteger(args[field]) || args[field] < (field === 'concurrent' ? 2 : 1)) throw new Error(`Invalid ${field}`);
|
|
36
|
+
}
|
|
37
|
+
if (args.expectWorkers && args.workerMetrics.length !== args.expectWorkers) throw new Error('--expect-workers must equal the number of --worker-metrics URLs');
|
|
38
|
+
|
|
39
|
+
const endpoint = JSON.parse(await readFile(args.endpoint, 'utf8'));
|
|
40
|
+
const deployment = JSON.parse(await readFile(args.deployment, 'utf8'));
|
|
41
|
+
function check(condition, message) { if (!condition) throw new Error(message); }
|
|
42
|
+
check(['responses', 'chat-completions'].includes(endpoint.api), 'Endpoint must explicitly select responses or chat-completions');
|
|
43
|
+
check(typeof endpoint.wire_model === 'string' && endpoint.wire_model.length > 0, 'Missing wire_model');
|
|
44
|
+
const responses = endpoint.api === 'responses';
|
|
45
|
+
const base = endpoint.base_url.replace(/\/+$/, '');
|
|
46
|
+
for (const url of [base, ...args.workerMetrics, args.gatewayMetrics].filter(Boolean)) {
|
|
47
|
+
const parsed = new URL(url);
|
|
48
|
+
check(['http:', 'https:'].includes(parsed.protocol) && !parsed.username && !parsed.password, 'Use HTTP(S) URLs without embedded credentials');
|
|
49
|
+
}
|
|
50
|
+
let apiKey;
|
|
51
|
+
if (endpoint.auth?.kind === 'env') {
|
|
52
|
+
apiKey = process.env[endpoint.auth.name];
|
|
53
|
+
check(typeof apiKey === 'string' && apiKey.length > 0, `Missing credential environment variable ${endpoint.auth.name}`);
|
|
54
|
+
} else check(endpoint.auth?.kind === 'none', 'This probe supports auth kind env or none');
|
|
55
|
+
function redact(value, key = '') {
|
|
56
|
+
if (/^(api_?key|authorization|password|secret|credential|access_token)$/i.test(key)) return '[REDACTED]';
|
|
57
|
+
if (typeof value === 'string') return apiKey ? value.split(apiKey).join('[REDACTED]') : value;
|
|
58
|
+
if (Array.isArray(value)) return value.map(item => redact(item));
|
|
59
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, redact(item, name)]));
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
const report = {
|
|
63
|
+
schema_version: '1', observation_kind: 'standard-http-endpoint-acceptance',
|
|
64
|
+
started_at: new Date().toISOString(), passed: false,
|
|
65
|
+
hardware_validation: 'NOT PERFORMED BY THIS HTTP PROBE',
|
|
66
|
+
identity_validation: 'operator metadata only; not proof of loaded weights',
|
|
67
|
+
harness_validation: 'NOT PERFORMED; run the selected real Harness separately',
|
|
68
|
+
configuration: { endpoint, deployment, budget: args.budget, temperature: 0, concurrent_requests: args.concurrent, request_timeout_ms: args.timeoutMs, transparent_retries: 0 },
|
|
69
|
+
probe: { node_version: process.version, sha256: createHash('sha256').update(await readFile(fileURLToPath(import.meta.url))).digest('hex') },
|
|
70
|
+
checks: [], exchanges: [],
|
|
71
|
+
};
|
|
72
|
+
const generationUrl = `${base}/${responses ? 'responses' : 'chat/completions'}`;
|
|
73
|
+
const user = text => ({ role: 'user', content: text });
|
|
74
|
+
function body(history, { stream = false, tools, toolChoice } = {}) {
|
|
75
|
+
return {
|
|
76
|
+
model: endpoint.wire_model, temperature: 0,
|
|
77
|
+
...(responses ? { input: history, max_output_tokens: args.budget, store: false } : { messages: history, max_tokens: args.budget }),
|
|
78
|
+
stream,
|
|
79
|
+
...(!responses && stream ? { stream_options: { include_usage: true } } : {}),
|
|
80
|
+
...(tools ? { tools, parallel_tool_calls: false, tool_choice: toolChoice ?? 'auto' } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function view(json) {
|
|
84
|
+
if (responses) {
|
|
85
|
+
check(['completed', 'incomplete'].includes(json.status), `Unexpected Responses status ${json.status}`);
|
|
86
|
+
if (json.status === 'incomplete') check(json.incomplete_details?.reason === 'max_output_tokens', 'Unexpected incomplete response reason');
|
|
87
|
+
return { text: (json.output ?? []).flatMap(item => item.type === 'message' ? (item.content ?? []).filter(part => part.type === 'output_text').map(part => part.text) : []).join(''), tools: (json.output ?? []).filter(item => item.type === 'function_call').map(item => ({ id: item.call_id, name: item.name, arguments: item.arguments })), history: json.output ?? [], usage: json.usage, terminal: json.status };
|
|
88
|
+
}
|
|
89
|
+
const choice = json.choices?.[0];
|
|
90
|
+
check(choice && choice.finish_reason !== null && choice.finish_reason !== undefined, 'Missing chat finish_reason');
|
|
91
|
+
const message = choice.message;
|
|
92
|
+
check(message?.role === 'assistant', 'Missing assistant chat message');
|
|
93
|
+
return { text: message.content ?? '', tools: (message.tool_calls ?? []).map(item => ({ id: item.id, name: item.function?.name, arguments: item.function?.arguments })), history: [{ role: 'assistant', content: message.content ?? null, ...(message.tool_calls?.length ? { tool_calls: message.tool_calls } : {}) }], usage: json.usage, terminal: choice.finish_reason };
|
|
94
|
+
}
|
|
95
|
+
function verifyUsage(result) {
|
|
96
|
+
const usage = result.usage;
|
|
97
|
+
check(usage && typeof usage === 'object' && !Array.isArray(usage), 'Missing or invalid usage; missing values are not replaced with zero');
|
|
98
|
+
function count(name, aliases) {
|
|
99
|
+
const fields = aliases.filter(field => Object.hasOwn(usage, field));
|
|
100
|
+
check(fields.length > 0 && fields.every(field => Number.isSafeInteger(usage[field]) && usage[field] >= 0), `Missing or invalid ${name} usage; missing values are not replaced with zero`);
|
|
101
|
+
const value = usage[fields[0]];
|
|
102
|
+
check(fields.every(field => usage[field] === value), `Inconsistent ${name} usage aliases: ${fields.join(', ')}`);
|
|
103
|
+
return { value, fields };
|
|
104
|
+
}
|
|
105
|
+
// Verify recognized wire variants without rewriting the captured upstream usage.
|
|
106
|
+
const input = count('input', ['input_tokens', 'prompt_tokens']);
|
|
107
|
+
const output = count('output', ['output_tokens', 'completion_tokens']);
|
|
108
|
+
check(output.value <= args.budget, `Reported output usage ${output.value} exceeds the unchanged budget ${args.budget}`);
|
|
109
|
+
return { input_tokens: input.value, output_tokens: output.value, source_fields: { input_tokens: input.fields, output_tokens: output.fields } };
|
|
110
|
+
}
|
|
111
|
+
async function exchange(name, url, { data, mode = 'json', signal, onContent, expectedAbort = false, acceptedStatus, key = apiKey } = {}) {
|
|
112
|
+
const started = performance.now();
|
|
113
|
+
const record = { name, started_at: new Date().toISOString(), request: { method: data ? 'POST' : 'GET', url, ...(data ? { body: data } : {}), authentication: key === apiKey ? endpoint.auth.kind : 'intentionally-invalid-key' } };
|
|
114
|
+
report.exchanges.push(record);
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(url, { method: data ? 'POST' : 'GET', headers: { ...(data ? { 'Content-Type': 'application/json' } : {}), ...(key ? { Authorization: `Bearer ${key}` } : {}), 'X-Request-ID': `hitch-accept-${randomUUID()}` }, ...(data ? { body: JSON.stringify(data) } : {}), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(args.timeoutMs)]) : AbortSignal.timeout(args.timeoutMs) });
|
|
117
|
+
record.response = { status: response.status, headers: Object.fromEntries(['content-type', 'x-request-id', 'retry-after'].flatMap(key => response.headers.has(key) ? [[key, response.headers.get(key)]] : [])) };
|
|
118
|
+
if (!response.ok || acceptedStatus || mode === 'text') {
|
|
119
|
+
record.response.body = await response.text();
|
|
120
|
+
check(acceptedStatus ? acceptedStatus.includes(response.status) : response.ok, `HTTP ${response.status} for ${name}`);
|
|
121
|
+
return record.response.body;
|
|
122
|
+
}
|
|
123
|
+
if (mode === 'json') {
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
record.response.body = text;
|
|
126
|
+
const json = JSON.parse(text);
|
|
127
|
+
record.response.body = json;
|
|
128
|
+
const result = view(json);
|
|
129
|
+
record.response.usage_verification = verifyUsage(result);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
check(response.headers.get('content-type')?.includes('text/event-stream'), 'Streaming response is not SSE');
|
|
133
|
+
const events = [];
|
|
134
|
+
record.response.events = events;
|
|
135
|
+
let buffer = '', totalBytes = 0, streamedText = '', finalResponse, usage, finishReason, done = false;
|
|
136
|
+
const calls = new Map();
|
|
137
|
+
function event(block) {
|
|
138
|
+
const lines = block.split(/\r?\n/);
|
|
139
|
+
const payload = lines.filter(line => line.startsWith('data:')).map(line => line.slice(5).replace(/^ /, '')).join('\n');
|
|
140
|
+
if (!payload) return;
|
|
141
|
+
if (payload === '[DONE]') { done = true; events.push({ data: '[DONE]', elapsed_ms: performance.now() - started }); return; }
|
|
142
|
+
const json = JSON.parse(payload);
|
|
143
|
+
events.push({ event: lines.find(line => line.startsWith('event:'))?.slice(6).trim(), data: json, elapsed_ms: performance.now() - started });
|
|
144
|
+
check(json.type !== 'error' && json.type !== 'response.failed' && !json.error, 'Upstream reported a streaming error');
|
|
145
|
+
if (responses) {
|
|
146
|
+
if (json.type === 'response.output_text.delta' && json.delta) { streamedText += json.delta; onContent?.(); }
|
|
147
|
+
if (['response.completed', 'response.incomplete'].includes(json.type)) finalResponse = json.response;
|
|
148
|
+
} else {
|
|
149
|
+
if (json.usage) usage = json.usage;
|
|
150
|
+
for (const choice of json.choices ?? []) {
|
|
151
|
+
if (choice.delta?.content) { streamedText += choice.delta.content; onContent?.(); }
|
|
152
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
153
|
+
for (const delta of choice.delta?.tool_calls ?? []) {
|
|
154
|
+
const call = calls.get(delta.index) ?? { id: '', type: 'function', function: { name: '', arguments: '' } };
|
|
155
|
+
call.id += delta.id ?? '';
|
|
156
|
+
call.function.name += delta.function?.name ?? '';
|
|
157
|
+
call.function.arguments += delta.function?.arguments ?? '';
|
|
158
|
+
calls.set(delta.index, call);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const decoder = new TextDecoder();
|
|
164
|
+
for await (const chunk of response.body) {
|
|
165
|
+
totalBytes += chunk.byteLength;
|
|
166
|
+
check(totalBytes <= 8 * 1024 * 1024, 'SSE response exceeds probe capture limit');
|
|
167
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
168
|
+
let boundary;
|
|
169
|
+
while ((boundary = /\r?\n\r?\n/.exec(buffer))) {
|
|
170
|
+
event(buffer.slice(0, boundary.index));
|
|
171
|
+
buffer = buffer.slice(boundary.index + boundary[0].length);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
buffer += decoder.decode();
|
|
175
|
+
if (buffer.trim()) event(buffer);
|
|
176
|
+
if (responses) check(finalResponse, 'SSE ended without a terminal Responses snapshot');
|
|
177
|
+
else check(done && finishReason, 'SSE ended without chat finish_reason and [DONE]');
|
|
178
|
+
const result = view(responses ? finalResponse : { choices: [{ finish_reason: finishReason, message: { role: 'assistant', content: streamedText || null, ...(calls.size ? { tool_calls: [...calls.values()] } : {}) } }], usage });
|
|
179
|
+
record.response.usage_verification = verifyUsage(result);
|
|
180
|
+
result.streamed_text = streamedText;
|
|
181
|
+
result.streamed_tool_arguments = responses ? events.filter(item => item.data?.type === 'response.function_call_arguments.delta').map(item => item.data.delta ?? '').join('') : [...calls.values()].map(item => item.function.arguments).join('');
|
|
182
|
+
record.response.terminal = result.terminal;
|
|
183
|
+
record.response.usage = result.usage;
|
|
184
|
+
return result;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
record.error = { name: error.name, message: error.message };
|
|
187
|
+
if (expectedAbort && signal?.aborted) { record.expected_client_abort = true; return { aborted: true }; }
|
|
188
|
+
throw error;
|
|
189
|
+
} finally { record.duration_ms = performance.now() - started; }
|
|
190
|
+
}
|
|
191
|
+
async function checkStep(name, run) {
|
|
192
|
+
const item = { name, passed: false };
|
|
193
|
+
report.checks.push(item);
|
|
194
|
+
try { await run(); item.passed = true; }
|
|
195
|
+
catch (error) { item.error = error.message; throw error; }
|
|
196
|
+
}
|
|
197
|
+
async function generate(name, history, options = {}) { return exchange(name, generationUrl, { data: body(history, options), mode: options.stream ? 'sse' : 'json', ...options }); }
|
|
198
|
+
const marker = prefix => `${prefix}_${randomUUID().replaceAll('-', '')}`;
|
|
199
|
+
const assertText = (result, expected) => check(typeof result.text === 'string' && result.text.includes(expected), `Generated output did not contain expected marker ${expected}`);
|
|
200
|
+
async function toolRoundtrip(stream) {
|
|
201
|
+
const parcel = marker('PARCEL'), delivery = marker('DELIVERED');
|
|
202
|
+
const definition = { name: 'lookup_delivery_code', description: 'Look up the delivery confirmation code for a parcel.', parameters: { type: 'object', properties: { parcel_id: { type: 'string' } }, required: ['parcel_id'], additionalProperties: false }, strict: true };
|
|
203
|
+
const tools = [responses ? { type: 'function', ...definition } : { type: 'function', function: definition }];
|
|
204
|
+
const toolChoice = responses ? { type: 'function', name: definition.name } : { type: 'function', function: { name: definition.name } };
|
|
205
|
+
const history = [user(`Call lookup_delivery_code with parcel_id ${parcel}. Then tell me the delivery_code returned by the tool. Do not invent the result.`)];
|
|
206
|
+
const first = await generate(`tools-${stream ? 'stream' : 'json'}-call`, history, { tools, toolChoice, stream });
|
|
207
|
+
check(first.tools.length === 1, 'Expected exactly one function call');
|
|
208
|
+
if (stream) check(first.streamed_tool_arguments.length > 0, 'Streaming tool response did not emit function arguments');
|
|
209
|
+
const call = first.tools[0];
|
|
210
|
+
check(call.id && call.name === definition.name, 'Model returned an undeclared function or missing call id');
|
|
211
|
+
const input = JSON.parse(call.arguments);
|
|
212
|
+
check(input.parcel_id === parcel && Object.keys(input).length === 1, 'Tool arguments do not match the requested parcel');
|
|
213
|
+
// Execute this one allowlisted, pure local tool; then send its actual output back.
|
|
214
|
+
const output = { parcel_id: input.parcel_id, delivery_code: delivery };
|
|
215
|
+
report.exchanges.push({ name: 'local-tool-execution', tool: call.name, call_id: call.id, input, output });
|
|
216
|
+
const resultItem = responses ? { type: 'function_call_output', call_id: call.id, output: JSON.stringify(output) } : { role: 'tool', tool_call_id: call.id, content: JSON.stringify(output) };
|
|
217
|
+
const final = await generate(`tools-${stream ? 'stream' : 'json'}-continue`, [...history, ...first.history, resultItem], { tools, toolChoice: 'none', stream });
|
|
218
|
+
assertText(final, delivery);
|
|
219
|
+
if (stream) check(final.streamed_text.includes(delivery), 'Tool continuation text was not delivered in SSE deltas');
|
|
220
|
+
check(final.tools.length === 0, 'Model did not finish after the tool result');
|
|
221
|
+
}
|
|
222
|
+
async function metricsSnapshot(label) {
|
|
223
|
+
return Promise.all(args.workerMetrics.map(async (url, index) => {
|
|
224
|
+
const text = await exchange(`${label}-worker-${index + 1}-metrics`, url, { mode: 'text' });
|
|
225
|
+
const matches = [...text.matchAll(/^sglang:num_requests_total(?:\{[^}]*\})?\s+([0-9eE.+-]+)/gm)];
|
|
226
|
+
return { url, request_count: matches.length ? matches.reduce((sum, match) => sum + Number(match[1]), 0) : null };
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
async function cancellationIsolation() {
|
|
230
|
+
const cancelled = new AbortController();
|
|
231
|
+
const cancelledFirst = Promise.withResolvers(), survivorFirst = Promise.withResolvers();
|
|
232
|
+
let cancelledEnded = false, survivorEnded = false;
|
|
233
|
+
const cancelMarker = marker('CANCEL'), survivorMarker = marker('SURVIVE');
|
|
234
|
+
const prompt = value => [user(`Begin with ${value}. Then write a long tutorial with at least 150 numbered sentences about arithmetic, explaining each sentence in detail.`)];
|
|
235
|
+
const cancelRequest = generate('cancel-one-active-stream', prompt(cancelMarker), { stream: true, signal: cancelled.signal, expectedAbort: true, onContent: () => cancelledFirst.resolve() }).finally(() => { cancelledEnded = true; cancelledFirst.reject(new Error('Cancelled request ended before content')); });
|
|
236
|
+
const survivorRequest = generate('independent-surviving-stream', prompt(survivorMarker), { stream: true, onContent: () => survivorFirst.resolve() }).finally(() => { survivorEnded = true; survivorFirst.reject(new Error('Survivor ended before content')); });
|
|
237
|
+
// Attach handlers immediately so a failure cannot leave an unhandled rejection.
|
|
238
|
+
const settled = Promise.allSettled([cancelRequest, survivorRequest]);
|
|
239
|
+
try {
|
|
240
|
+
await Promise.all([cancelledFirst.promise, survivorFirst.promise]);
|
|
241
|
+
check(!cancelledEnded && !survivorEnded, 'Streams did not overlap long enough to observe cancellation isolation');
|
|
242
|
+
report.cancellation = { aborted_at: new Date().toISOString(), both_streams_had_content: true, both_streams_incomplete_at_abort: true, scope: 'only the cancelled request AbortController; no stop, drain, flush, or release API' };
|
|
243
|
+
cancelled.abort(new DOMException('Acceptance cancels this request only', 'AbortError'));
|
|
244
|
+
} catch (error) { cancelled.abort(); await settled; throw error; }
|
|
245
|
+
const [a, b] = await settled;
|
|
246
|
+
check(a.status === 'fulfilled' && a.value.aborted, 'Cancelled stream did not observe client abort');
|
|
247
|
+
if (b.status === 'rejected') throw b.reason;
|
|
248
|
+
assertText(b.value, survivorMarker);
|
|
249
|
+
const after = marker('STILL_SERVING');
|
|
250
|
+
assertText(await generate('service-remains-available-after-cancel', [user(`Reply with exactly ${after}.`)]), after);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
if (apiKey) await checkStep('authentication rejects an invalid key on the generation endpoint', () => exchange('invalid-auth', generationUrl, { data: body([user('Say hello.')]), acceptedStatus: [401, 403], key: `invalid-${randomUUID()}` }));
|
|
254
|
+
await checkStep('normal-budget generation and usage', async () => {
|
|
255
|
+
const expected = marker('GENERATED');
|
|
256
|
+
assertText(await generate('plain-generation', [user(`Reply with exactly ${expected}.`)]), expected);
|
|
257
|
+
});
|
|
258
|
+
await checkStep('complete multi-turn history without previous_response_id', async () => {
|
|
259
|
+
const expected = marker('REMEMBER');
|
|
260
|
+
const history = [user(`Remember this marker: ${expected}. Reply with Remembered.`)];
|
|
261
|
+
const first = await generate('history-first-turn', history);
|
|
262
|
+
assertText(await generate('history-second-turn', [...history, ...first.history, user('Repeat the marker from my first message exactly.')]), expected);
|
|
263
|
+
});
|
|
264
|
+
await checkStep('SSE terminal event and usage', async () => {
|
|
265
|
+
const expected = marker('STREAMED');
|
|
266
|
+
const result = await generate('stream-generation', [user(`Reply with exactly ${expected}.`)], { stream: true });
|
|
267
|
+
assertText(result, expected);
|
|
268
|
+
check(result.streamed_text.includes(expected), 'Text was not delivered in SSE deltas');
|
|
269
|
+
});
|
|
270
|
+
await checkStep('JSON tool call, local execution, result, and continuation', () => toolRoundtrip(false));
|
|
271
|
+
await checkStep('streaming tool call, local execution, result, and continuation', () => toolRoundtrip(true));
|
|
272
|
+
const before = await metricsSnapshot('before-concurrency');
|
|
273
|
+
if (args.gatewayMetrics) await exchange('before-gateway-metrics', args.gatewayMetrics, { mode: 'text' });
|
|
274
|
+
await checkStep('independent concurrent requests', async () => {
|
|
275
|
+
await Promise.all(Array.from({ length: args.concurrent }, async (_, index) => {
|
|
276
|
+
const expected = marker(`CONCURRENT_${index}`);
|
|
277
|
+
assertText(await generate(`concurrent-${index + 1}`, [user(`Reply with exactly ${expected}.`)]), expected);
|
|
278
|
+
}));
|
|
279
|
+
});
|
|
280
|
+
await checkStep('abort an active stream while another completes and service remains usable', cancellationIsolation);
|
|
281
|
+
const after = await metricsSnapshot('after-concurrency');
|
|
282
|
+
if (args.gatewayMetrics) await exchange('after-gateway-metrics', args.gatewayMetrics, { mode: 'text' });
|
|
283
|
+
report.worker_distribution = before.map((entry, index) => ({ url: entry.url, before: entry.request_count, after: after[index].request_count, delta: entry.request_count !== null && after[index].request_count !== null ? after[index].request_count - entry.request_count : null }));
|
|
284
|
+
if (args.expectWorkers) await checkStep('server metrics show requests on every expected worker', async () => {
|
|
285
|
+
check(report.worker_distribution.every(entry => Number.isFinite(entry.delta) && entry.delta > 0), 'Missing worker request counters, reset counters, or no positive request delta on a worker');
|
|
286
|
+
});
|
|
287
|
+
report.passed = true;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
report.error = { name: error.name, message: error.message };
|
|
290
|
+
} finally {
|
|
291
|
+
report.finished_at = new Date().toISOString();
|
|
292
|
+
const output = resolve(args.out);
|
|
293
|
+
await mkdir(dirname(output), { recursive: true });
|
|
294
|
+
await writeFile(output, `${JSON.stringify(redact(report), null, 2)}\n`, { mode: 0o600 });
|
|
295
|
+
console.log(`${report.passed ? 'PASS' : 'FAIL'}: HTTP acceptance report written to ${output}`);
|
|
296
|
+
if (!report.passed) { console.error(redact(report.error?.message ?? 'Acceptance failed')); process.exitCode = 1; }
|
|
297
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: hitch-external-inference
|
|
2
|
+
|
|
3
|
+
x-worker: &worker
|
|
4
|
+
image: ${SGLANG_IMAGE:-lmsysorg/sglang:v0.5.21}
|
|
5
|
+
init: true
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
shm_size: 8gb
|
|
8
|
+
entrypoint: ["/bin/sh", "-ec"]
|
|
9
|
+
command:
|
|
10
|
+
- |
|
|
11
|
+
key="$$(cat /run/secrets/api_key)"
|
|
12
|
+
test -n "$$key"
|
|
13
|
+
exec python3 -m sglang.launch_server \
|
|
14
|
+
--model-path /model --tokenizer-path /model \
|
|
15
|
+
--served-model-name "$$WIRE_MODEL" \
|
|
16
|
+
--host 0.0.0.0 --port 30000 --api-key "$$key" \
|
|
17
|
+
--tp-size 1 --dtype bfloat16 --disable-cuda-graph \
|
|
18
|
+
--context-length "$$CONTEXT_LENGTH" \
|
|
19
|
+
--mem-fraction-static "$$MEM_FRACTION_STATIC" \
|
|
20
|
+
--max-running-requests "$$ENGINE_MAX_RUNNING" \
|
|
21
|
+
--max-queued-requests "$$ENGINE_MAX_QUEUED" \
|
|
22
|
+
--tool-call-parser qwen25 --enable-metrics
|
|
23
|
+
environment:
|
|
24
|
+
WIRE_MODEL: ${WIRE_MODEL:?Set a versioned wire model name}
|
|
25
|
+
CONTEXT_LENGTH: ${CONTEXT_LENGTH:-16384}
|
|
26
|
+
MEM_FRACTION_STATIC: ${MEM_FRACTION_STATIC:-0.82}
|
|
27
|
+
ENGINE_MAX_RUNNING: ${ENGINE_MAX_RUNNING:-4}
|
|
28
|
+
ENGINE_MAX_QUEUED: ${ENGINE_MAX_QUEUED:-8}
|
|
29
|
+
HF_HUB_OFFLINE: "1"
|
|
30
|
+
TRANSFORMERS_OFFLINE: "1"
|
|
31
|
+
secrets: [api_key]
|
|
32
|
+
volumes:
|
|
33
|
+
- type: bind
|
|
34
|
+
source: ${HF_SNAPSHOT_DIR:?Set a self-contained immutable HF snapshot directory}
|
|
35
|
+
target: /model
|
|
36
|
+
read_only: true
|
|
37
|
+
bind:
|
|
38
|
+
create_host_path: false
|
|
39
|
+
healthcheck:
|
|
40
|
+
test: ["CMD-SHELL", 'curl -fsS -H "Authorization: Bearer $$(cat /run/secrets/api_key)" http://127.0.0.1:30000/health >/dev/null']
|
|
41
|
+
interval: 10s
|
|
42
|
+
timeout: 5s
|
|
43
|
+
start_period: 10m
|
|
44
|
+
retries: 60
|
|
45
|
+
networks: [model]
|
|
46
|
+
|
|
47
|
+
services:
|
|
48
|
+
worker-1:
|
|
49
|
+
<<: *worker
|
|
50
|
+
ports: ["127.0.0.1:30001:30000"]
|
|
51
|
+
deploy:
|
|
52
|
+
resources:
|
|
53
|
+
reservations:
|
|
54
|
+
devices:
|
|
55
|
+
- driver: nvidia
|
|
56
|
+
device_ids: ["${INFERENCE_GPU_0:?Set the first GPU UUID reserved for inference}"]
|
|
57
|
+
capabilities: [gpu]
|
|
58
|
+
|
|
59
|
+
worker-2:
|
|
60
|
+
<<: *worker
|
|
61
|
+
ports: ["127.0.0.1:30002:30000"]
|
|
62
|
+
deploy:
|
|
63
|
+
resources:
|
|
64
|
+
reservations:
|
|
65
|
+
devices:
|
|
66
|
+
- driver: nvidia
|
|
67
|
+
device_ids: ["${INFERENCE_GPU_1:?Set a different GPU UUID reserved for inference}"]
|
|
68
|
+
capabilities: [gpu]
|
|
69
|
+
|
|
70
|
+
gateway:
|
|
71
|
+
# Upstream gateway-only release; workers retain their separate SGLang image.
|
|
72
|
+
image: ${GATEWAY_IMAGE:-lightseekorg/smg:1.12.0@sha256:d08b509491f222c2c9786323a3df90136127364c25b9b9b48e91b4b3e9e695d6}
|
|
73
|
+
# Read the host-owned 0600 Compose secret without relaxing its permissions.
|
|
74
|
+
user: "0:0"
|
|
75
|
+
init: true
|
|
76
|
+
restart: unless-stopped
|
|
77
|
+
entrypoint: ["/bin/sh", "-ec"]
|
|
78
|
+
command:
|
|
79
|
+
- |
|
|
80
|
+
key="$$(cat /run/secrets/api_key)"
|
|
81
|
+
test -n "$$key"
|
|
82
|
+
exec smg launch \
|
|
83
|
+
--host 0.0.0.0 --port 30000 \
|
|
84
|
+
--worker-urls http://worker-1:30000 http://worker-2:30000 \
|
|
85
|
+
--api-key "$$key" --policy round_robin --disable-retries \
|
|
86
|
+
--disable-tokenizer-autoload \
|
|
87
|
+
--max-concurrent-requests "$$GATEWAY_MAX_CONCURRENT" \
|
|
88
|
+
--queue-size "$$GATEWAY_QUEUE_SIZE" --queue-timeout-secs 30 \
|
|
89
|
+
--request-timeout-secs 180 --worker-startup-timeout-secs 1800 \
|
|
90
|
+
--history-backend none --prometheus-host 0.0.0.0 --prometheus-port 10001
|
|
91
|
+
environment:
|
|
92
|
+
NVIDIA_VISIBLE_DEVICES: "void"
|
|
93
|
+
CUDA_VISIBLE_DEVICES: ""
|
|
94
|
+
GATEWAY_MAX_CONCURRENT: ${GATEWAY_MAX_CONCURRENT:-8}
|
|
95
|
+
GATEWAY_QUEUE_SIZE: ${GATEWAY_QUEUE_SIZE:-16}
|
|
96
|
+
secrets: [api_key]
|
|
97
|
+
ports:
|
|
98
|
+
- "127.0.0.1:${INFERENCE_PORT:-30000}:30000"
|
|
99
|
+
- "127.0.0.1:10001:10001"
|
|
100
|
+
depends_on:
|
|
101
|
+
worker-1:
|
|
102
|
+
condition: service_healthy
|
|
103
|
+
worker-2:
|
|
104
|
+
condition: service_healthy
|
|
105
|
+
healthcheck:
|
|
106
|
+
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:30000/readiness', timeout=4).close()"]
|
|
107
|
+
interval: 10s
|
|
108
|
+
timeout: 5s
|
|
109
|
+
start_period: 1m
|
|
110
|
+
retries: 60
|
|
111
|
+
networks: [model]
|
|
112
|
+
|
|
113
|
+
secrets:
|
|
114
|
+
api_key:
|
|
115
|
+
file: ${INFERENCE_API_KEY_FILE:?Set an absolute path to a private API key file}
|
|
116
|
+
|
|
117
|
+
networks:
|
|
118
|
+
model:
|
|
119
|
+
internal: true
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: hitch-external-inference
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
engine:
|
|
5
|
+
image: ${SGLANG_IMAGE:-lmsysorg/sglang:v0.5.21}
|
|
6
|
+
init: true
|
|
7
|
+
restart: unless-stopped
|
|
8
|
+
shm_size: 8gb
|
|
9
|
+
entrypoint: ["/bin/sh", "-ec"]
|
|
10
|
+
command:
|
|
11
|
+
- |
|
|
12
|
+
key="$$(cat /run/secrets/api_key)"
|
|
13
|
+
test -n "$$key"
|
|
14
|
+
exec python3 -m sglang.launch_server \
|
|
15
|
+
--model-path /model --tokenizer-path /model \
|
|
16
|
+
--served-model-name "$$WIRE_MODEL" \
|
|
17
|
+
--host 0.0.0.0 --port 30000 --api-key "$$key" \
|
|
18
|
+
--tp-size "$$TP_SIZE" --dtype bfloat16 --disable-cuda-graph \
|
|
19
|
+
--context-length "$$CONTEXT_LENGTH" \
|
|
20
|
+
--mem-fraction-static "$$MEM_FRACTION_STATIC" \
|
|
21
|
+
--max-running-requests "$$ENGINE_MAX_RUNNING" \
|
|
22
|
+
--max-queued-requests "$$ENGINE_MAX_QUEUED" \
|
|
23
|
+
--tool-call-parser qwen25 --enable-metrics
|
|
24
|
+
environment:
|
|
25
|
+
WIRE_MODEL: ${WIRE_MODEL:?Set a versioned wire model name}
|
|
26
|
+
TP_SIZE: "1"
|
|
27
|
+
CONTEXT_LENGTH: ${CONTEXT_LENGTH:-16384}
|
|
28
|
+
MEM_FRACTION_STATIC: ${MEM_FRACTION_STATIC:-0.82}
|
|
29
|
+
ENGINE_MAX_RUNNING: ${ENGINE_MAX_RUNNING:-4}
|
|
30
|
+
ENGINE_MAX_QUEUED: ${ENGINE_MAX_QUEUED:-8}
|
|
31
|
+
HF_HUB_OFFLINE: "1"
|
|
32
|
+
TRANSFORMERS_OFFLINE: "1"
|
|
33
|
+
secrets: [api_key]
|
|
34
|
+
volumes:
|
|
35
|
+
- type: bind
|
|
36
|
+
source: ${HF_SNAPSHOT_DIR:?Set a self-contained immutable HF snapshot directory}
|
|
37
|
+
target: /model
|
|
38
|
+
read_only: true
|
|
39
|
+
bind:
|
|
40
|
+
create_host_path: false
|
|
41
|
+
ports:
|
|
42
|
+
- "127.0.0.1:${INFERENCE_PORT:-30000}:30000"
|
|
43
|
+
healthcheck:
|
|
44
|
+
test: ["CMD-SHELL", 'curl -fsS -H "Authorization: Bearer $$(cat /run/secrets/api_key)" http://127.0.0.1:30000/health >/dev/null']
|
|
45
|
+
interval: 10s
|
|
46
|
+
timeout: 5s
|
|
47
|
+
start_period: 10m
|
|
48
|
+
retries: 60
|
|
49
|
+
deploy:
|
|
50
|
+
resources:
|
|
51
|
+
reservations:
|
|
52
|
+
devices:
|
|
53
|
+
- driver: nvidia
|
|
54
|
+
device_ids: ["${INFERENCE_GPU_0:?Set the GPU UUID reserved for inference}"]
|
|
55
|
+
capabilities: [gpu]
|
|
56
|
+
networks: [model]
|
|
57
|
+
|
|
58
|
+
secrets:
|
|
59
|
+
api_key:
|
|
60
|
+
file: ${INFERENCE_API_KEY_FILE:?Set an absolute path to a private API key file}
|
|
61
|
+
|
|
62
|
+
networks:
|
|
63
|
+
model:
|
|
64
|
+
internal: true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Apply only with compose.single.yaml. Requires Docker Compose >= 2.24.4.
|
|
2
|
+
services:
|
|
3
|
+
engine:
|
|
4
|
+
shm_size: 16gb
|
|
5
|
+
environment:
|
|
6
|
+
TP_SIZE: "2"
|
|
7
|
+
deploy:
|
|
8
|
+
resources:
|
|
9
|
+
reservations:
|
|
10
|
+
devices: !override
|
|
11
|
+
- driver: nvidia
|
|
12
|
+
device_ids:
|
|
13
|
+
- "${INFERENCE_GPU_0:?Set the first inference GPU UUID}"
|
|
14
|
+
- "${INFERENCE_GPU_1:?Set the second inference GPU UUID}"
|
|
15
|
+
capabilities: [gpu]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"record_kind": "operator-declared-example-not-a-trusted-deployment-receipt",
|
|
3
|
+
"deployment_ref": "REPLACE_WITH_DEPLOYMENT_PIPELINE_RUN_OR_CHANGE_REF",
|
|
4
|
+
"subject": {
|
|
5
|
+
"artifact_digest": "COPY_FROM_EXISTING_CAS_MODEL_MANIFEST",
|
|
6
|
+
"hf_repository": "Qwen/Qwen2.5-7B-Instruct",
|
|
7
|
+
"hf_revision": "a09a35458c702b33eeacc393d103063234e8bc28"
|
|
8
|
+
},
|
|
9
|
+
"engine": {
|
|
10
|
+
"image": "lmsysorg/sglang:v0.5.21",
|
|
11
|
+
"resolved_image_digest": null,
|
|
12
|
+
"source_commit": "e00930c5489053f26d86b179cee0d087f846acbb",
|
|
13
|
+
"configured_sglang_version": "0.5.21",
|
|
14
|
+
"observed_package_versions": null
|
|
15
|
+
},
|
|
16
|
+
"gateway": {
|
|
17
|
+
"enabled": false,
|
|
18
|
+
"image": "lightseekorg/smg:1.12.0@sha256:d08b509491f222c2c9786323a3df90136127364c25b9b9b48e91b4b3e9e695d6",
|
|
19
|
+
"resolved_image_digest": null,
|
|
20
|
+
"source_commit": "dcc5cb0b8f2a64694938c10f02c6caaa0c40d5ef",
|
|
21
|
+
"configured_smg_version": "1.12.0",
|
|
22
|
+
"observed_package_versions": null
|
|
23
|
+
},
|
|
24
|
+
"configuration": {
|
|
25
|
+
"compose_files": [
|
|
26
|
+
"compose.single.yaml"
|
|
27
|
+
],
|
|
28
|
+
"wire_model": "qwen25-7b-a09a35458c70",
|
|
29
|
+
"tensor_parallel_size": 1,
|
|
30
|
+
"replicas": 1,
|
|
31
|
+
"dtype": "bfloat16",
|
|
32
|
+
"quantization": "none",
|
|
33
|
+
"context_length": 16384,
|
|
34
|
+
"tool_parser": "qwen25",
|
|
35
|
+
"tokenizer_and_template": "from-the-same-read-only-HF-snapshot",
|
|
36
|
+
"radix_cache": "upstream-default-enabled",
|
|
37
|
+
"max_running_requests_per_worker": 4,
|
|
38
|
+
"max_queued_requests_per_worker": 8,
|
|
39
|
+
"gateway": null,
|
|
40
|
+
"request_sampling": {
|
|
41
|
+
"temperature": 0,
|
|
42
|
+
"output_budget": 1024
|
|
43
|
+
},
|
|
44
|
+
"mem_fraction_static": 0.82,
|
|
45
|
+
"disable_cuda_graph": true
|
|
46
|
+
},
|
|
47
|
+
"isolation": {
|
|
48
|
+
"policy": "static-exclusive-device-allocation-by-deployment-owner",
|
|
49
|
+
"inference_gpu_uuids": [],
|
|
50
|
+
"training_gpu_uuids": [],
|
|
51
|
+
"allocation_record_ref": null
|
|
52
|
+
},
|
|
53
|
+
"hardware_validation": {
|
|
54
|
+
"status": "NOT RUN",
|
|
55
|
+
"evidence_ref": null
|
|
56
|
+
},
|
|
57
|
+
"receipt_source": null
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"base_url": "http://127.0.0.1:30000/v1",
|
|
4
|
+
"wire_model": "qwen25-7b-a09a35458c70",
|
|
5
|
+
"api": "chat-completions",
|
|
6
|
+
"auth": {"kind": "env", "name": "INFERENCE_API_KEY"},
|
|
7
|
+
"capabilities": {
|
|
8
|
+
"streaming": true,
|
|
9
|
+
"tool_calls": true,
|
|
10
|
+
"parallel_tool_calls": false,
|
|
11
|
+
"input_modalities": ["text"]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"base_url": "http://127.0.0.1:30000/v1",
|
|
4
|
+
"wire_model": "qwen25-7b-a09a35458c70",
|
|
5
|
+
"api": "responses",
|
|
6
|
+
"auth": {"kind": "env", "name": "INFERENCE_API_KEY"},
|
|
7
|
+
"capabilities": {
|
|
8
|
+
"streaming": true,
|
|
9
|
+
"tool_calls": true,
|
|
10
|
+
"parallel_tool_calls": false,
|
|
11
|
+
"input_modalities": ["text"]
|
|
12
|
+
}
|
|
13
|
+
}
|