@crvouga/dev-fleet 0.0.0-stage → 0.2.0
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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Turn a health report into findings an agent (or human) can act on without re-deriving context.
|
|
2
|
+
|
|
3
|
+
Each finding names what failed, who can fix it, the evidence (structural trace, never prompt text),
|
|
4
|
+
the exact command that reproduces it cheaply, and where in the repository the fix would go.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
# category -> (owner, who can resolve it)
|
|
10
|
+
OWNERS = {
|
|
11
|
+
"credentials": ("credentials", "operator"),
|
|
12
|
+
"auth": ("credentials", "operator"),
|
|
13
|
+
"model_missing": ("proxy-config", "operator"),
|
|
14
|
+
"not_found": ("proxy-config", "operator"),
|
|
15
|
+
"rate_limit": ("provider-quota", "operator"),
|
|
16
|
+
"timeout": ("proxy-host", "operator"),
|
|
17
|
+
"transport": ("proxy-host", "operator"),
|
|
18
|
+
"upstream": ("provider-or-proxy", "operator"),
|
|
19
|
+
"protocol": ("endpoint-behavior", "agent"),
|
|
20
|
+
"assertion": ("endpoint-behavior", "agent"),
|
|
21
|
+
"request_rejected": ("endpoint-behavior", "agent"),
|
|
22
|
+
"harness": ("harness", "agent"),
|
|
23
|
+
"budget": ("run-budget", "agent"),
|
|
24
|
+
}
|
|
25
|
+
SEVERITY_RANK = {"critical": 0, "major": 1, "minor": 2, "info": 3}
|
|
26
|
+
TIER_PROFILE = {"light": "light", "full": "nightly", "manual": "manual"}
|
|
27
|
+
PACKAGE = "cliproxy"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _command(run_profile, model, check, seed, config):
|
|
31
|
+
if check is None:
|
|
32
|
+
return "bun run cliproxy model-health --profile readiness"
|
|
33
|
+
tier = next((c.tier for c in config.cases if c.capability == check), "full")
|
|
34
|
+
profile = TIER_PROFILE[tier]
|
|
35
|
+
return "bun run cliproxy %s --profile %s --model %s --check %s --seed %s" % (
|
|
36
|
+
"model-health" if profile == "light" else "model-suite", profile, model, check, seed)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _hints(category, model, check, config):
|
|
40
|
+
base = PACKAGE + "/models.json"
|
|
41
|
+
if category in ("protocol", "assertion", "request_rejected"):
|
|
42
|
+
capability = "capabilities['%s']" % check if check else "capabilities"
|
|
43
|
+
return [
|
|
44
|
+
"Run the reproduce command and read the evidence trace; decide whether the endpoint or the expectation is wrong.",
|
|
45
|
+
"Endpoint violates the public protocol: fix the proxy or provider configuration (an operator change); this repo cannot.",
|
|
46
|
+
"The provider route genuinely lacks the capability: in %s set models[id=%s].%s to optional or "
|
|
47
|
+
"{\"state\": \"unsupported\", \"reason\": \"...\"}. Never delete the check." % (base, model, capability),
|
|
48
|
+
"The expectation is wrong: edit checks[id=%s] in %s, or the validators in %s/scripts/modelhealth/protocol.py and expect.py, "
|
|
49
|
+
"and add a regression test in %s/tests/test_model_protocol.py." % ((check or "").split(".", 1)[-1], base, PACKAGE, PACKAGE),
|
|
50
|
+
]
|
|
51
|
+
if category == "harness":
|
|
52
|
+
return ["A defect in the checks, not the endpoint: run `bun run --filter @pkgs/cliproxy-fleet test:models`, "
|
|
53
|
+
"read the traceback class in the details, and fix %s/scripts/modelhealth/checks.py or the check fixture." % PACKAGE]
|
|
54
|
+
if category == "budget":
|
|
55
|
+
return ["The run ran out of its request or token budget. Raise runs.<profile>.budget in %s only if the cost is justified, "
|
|
56
|
+
"or run fewer checks with --check." % base]
|
|
57
|
+
advice = config.raw["diagnostics"].get(category)
|
|
58
|
+
return [advice] if advice else []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _evidence_line(trace):
|
|
62
|
+
parts = ["%s %s" % (trace.get("method", ""), trace.get("path", ""))]
|
|
63
|
+
if trace.get("http_status") is not None:
|
|
64
|
+
parts.append("HTTP %s" % trace["http_status"])
|
|
65
|
+
response = trace.get("response") or {}
|
|
66
|
+
for key in ("finish_reason", "events"):
|
|
67
|
+
if response.get(key) is not None:
|
|
68
|
+
parts.append("%s=%s" % (key, response[key]))
|
|
69
|
+
if response.get("tool_calls"):
|
|
70
|
+
parts.append("tool_calls=" + ",".join(response["tool_calls"]))
|
|
71
|
+
if trace.get("retry_after"):
|
|
72
|
+
parts.append("retry-after=%s" % trace["retry_after"])
|
|
73
|
+
for key in ("cf_ray", "request_id"):
|
|
74
|
+
if trace.get(key):
|
|
75
|
+
parts.append("%s=%s" % (key, trace[key]))
|
|
76
|
+
return " ".join(parts)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build(config, report, profile_name):
|
|
80
|
+
seed = report["seed"]
|
|
81
|
+
findings = []
|
|
82
|
+
|
|
83
|
+
def add(finding_id, category, severity, status, summary, model=None, check=None, endpoint=None, details=None, trace=None,
|
|
84
|
+
reproduce=None):
|
|
85
|
+
owner, resolver = OWNERS.get(category, ("unknown", "operator"))
|
|
86
|
+
findings.append({
|
|
87
|
+
"id": finding_id, "status": status, "severity": severity, "category": category,
|
|
88
|
+
"model": model, "check": check, "endpoint": endpoint, "summary": summary,
|
|
89
|
+
"owner": owner, "resolver": resolver, "agent_fixable": resolver == "agent",
|
|
90
|
+
"evidence": {"details": details or [], "trace": trace or []},
|
|
91
|
+
"reproduce": reproduce or _command(profile_name, model, check, seed, config),
|
|
92
|
+
"fix_hints": _hints(category, model, check, config),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
for name, level in report["levels"].items():
|
|
96
|
+
if level["status"] in ("unhealthy", "unknown") and level["category"] and level["category"] != "model_missing":
|
|
97
|
+
add("level:%s/%s" % (name, level["category"]), level["category"], "critical", "fail",
|
|
98
|
+
"The %s level is %s: %s" % (name, level["status"], "; ".join(level["details"]) or level["category"]),
|
|
99
|
+
details=level["details"])
|
|
100
|
+
for model in report["models"]:
|
|
101
|
+
mid = model["id"]
|
|
102
|
+
paused = next((r for r in model["reasons"] if r.startswith("paused:")), None)
|
|
103
|
+
if paused:
|
|
104
|
+
last = model.get("rate_limit_detail")
|
|
105
|
+
add("%s/rate_limit" % mid, "rate_limit", "critical", "paused",
|
|
106
|
+
"%s is rate limited; inference is paused (no quota is spent while it is)." % mid, model=mid,
|
|
107
|
+
details=[paused] + (["last error seen: " + last] if last else []),
|
|
108
|
+
reproduce="bun run cliproxy model-health --profile light --model %s --force-probe" % mid)
|
|
109
|
+
if any(r.startswith("model not served") for r in model["reasons"]):
|
|
110
|
+
add("%s/model_missing" % mid, "model_missing", "critical", "fail",
|
|
111
|
+
"%s is not listed by /v1/models" % mid, model=mid, details=model["reasons"])
|
|
112
|
+
for result in model["checks"]:
|
|
113
|
+
if result["status"] == "fail":
|
|
114
|
+
add("%s/%s/%s" % (mid, result["capability"], result["category"]), result["category"], result["severity"], "fail",
|
|
115
|
+
"%s failed %s (%s)" % (mid, result["capability"], result["title"]), model=mid, check=result["capability"],
|
|
116
|
+
endpoint=result["endpoint"], details=result.get("details"), trace=result.get("trace"))
|
|
117
|
+
elif result["status"] == "untested" and result["category"] == "budget":
|
|
118
|
+
add("%s/%s/budget" % (mid, result["capability"]), "budget", "info", "untested",
|
|
119
|
+
"%s was not run for %s: budget exhausted" % (result["capability"], mid), model=mid, check=result["capability"],
|
|
120
|
+
details=result["details"])
|
|
121
|
+
for sim in report["simulations"]:
|
|
122
|
+
if sim["status"] != "pass":
|
|
123
|
+
add("simulation:%s" % sim["id"], "harness", "major", "fail", "Simulated scenario %s failed: %s" % (sim["id"], sim["title"]),
|
|
124
|
+
details=sim["details"])
|
|
125
|
+
findings.sort(key=lambda f: (f["resolver"] != "operator", SEVERITY_RANK[f["severity"]], f["id"]))
|
|
126
|
+
return findings
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def fingerprint(findings):
|
|
130
|
+
text = "\n".join(sorted("%s|%s" % (f["id"], f["status"]) for f in findings))
|
|
131
|
+
return hashlib.sha256(text.encode()).hexdigest()[:16]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_brief(report):
|
|
135
|
+
findings = report["findings"]
|
|
136
|
+
summary = report["summary"]
|
|
137
|
+
out = ["# CLI proxy model health: %s (agent brief)" % summary["status"].upper(), ""]
|
|
138
|
+
out.append("Profile `%s`, suite `%s`, target `%s`, seed `%s`, %s. Full detail: `model-health.json` (`findings[]`, `models[]`), human report: `model-health.md`." % (
|
|
139
|
+
report["profile"], report["suite"], report["target"]["kind"], report["seed"], report["generated_at"]))
|
|
140
|
+
out.append("")
|
|
141
|
+
out.append("| Model | Status | Passing | Failing | Untested | Declared unsupported |")
|
|
142
|
+
out.append("| --- | --- | --- | --- | --- | --- |")
|
|
143
|
+
for m in report["models"]:
|
|
144
|
+
by = m["coverage"]["by_result"]
|
|
145
|
+
out.append("| %s | %s | %d | %d | %d | %d |" % (m["id"], m["status"], by.get("pass", 0), by.get("fail", 0),
|
|
146
|
+
by.get("untested", 0), by.get("unsupported", 0) + by.get("observed_unsupported", 0)))
|
|
147
|
+
out.append("")
|
|
148
|
+
if not findings:
|
|
149
|
+
out.append("No findings. Status `%s` describes only what was exercised (`untested` capabilities are not claims of health)." % summary["status"])
|
|
150
|
+
out.append("")
|
|
151
|
+
return "\n".join(out)
|
|
152
|
+
operator = [f for f in findings if not f["agent_fixable"]]
|
|
153
|
+
agent = [f for f in findings if f["agent_fixable"]]
|
|
154
|
+
out.append("%d finding(s): %d need an operator, %d an agent can attempt. Findings are ordered by who can fix them, then severity." % (
|
|
155
|
+
len(findings), len(operator), len(agent)))
|
|
156
|
+
out.append("")
|
|
157
|
+
for title, group in (("Needs an operator (cannot be fixed by editing this repository)", operator),
|
|
158
|
+
("An agent can attempt these", agent)):
|
|
159
|
+
if not group:
|
|
160
|
+
continue
|
|
161
|
+
out.append("## " + title)
|
|
162
|
+
out.append("")
|
|
163
|
+
for f in group:
|
|
164
|
+
out.append("### `%s` (%s, %s, %s)" % (f["id"], f["severity"], f["category"], f["status"]))
|
|
165
|
+
out.append("- **What:** " + f["summary"])
|
|
166
|
+
out.append("- **Owner:** %s" % f["owner"] + ("; endpoint `%s`" % f["endpoint"] if f["endpoint"] else ""))
|
|
167
|
+
for detail in f["evidence"]["details"][:4]:
|
|
168
|
+
out.append("- **Detail:** " + detail)
|
|
169
|
+
for trace in f["evidence"]["trace"][:3]:
|
|
170
|
+
out.append("- **Trace:** " + _evidence_line(trace) + (" error: %s" % trace["error"] if trace.get("error") else ""))
|
|
171
|
+
for violation in (trace.get("response") or {}).get("violations", [])[:4]:
|
|
172
|
+
out.append(" - violation: " + violation)
|
|
173
|
+
out.append("- **Reproduce (cheap, one check):** `%s`" % f["reproduce"])
|
|
174
|
+
for hint in f["fix_hints"]:
|
|
175
|
+
out.append("- **Fix:** " + hint)
|
|
176
|
+
out.append("")
|
|
177
|
+
out.append("## Verify a fix")
|
|
178
|
+
out.append("")
|
|
179
|
+
out.append("1. Offline contracts, no spend: `bun run --filter @pkgs/cliproxy-fleet test:models` and `bun run cliproxy model-suite --target mock --profile manual`.")
|
|
180
|
+
out.append("2. Live, cheapest first: the finding's reproduce command, then `bun run cliproxy model-health --profile readiness`, then `model-health` (light).")
|
|
181
|
+
out.append("3. Do not claim health from an offline result; a fix is verified only by a `live` target run whose finding is gone.")
|
|
182
|
+
out.append("")
|
|
183
|
+
return "\n".join(out)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Contract validators and normalizers for the public OpenAI wire protocols.
|
|
2
|
+
|
|
3
|
+
Chat Completions: https://platform.openai.com/docs/api-reference/chat
|
|
4
|
+
Responses: https://platform.openai.com/docs/api-reference/responses
|
|
5
|
+
Server-sent events: https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
6
|
+
|
|
7
|
+
Each parser converts a response into a protocol-independent ``Normalized`` view
|
|
8
|
+
and collects wire-level ``violations``; semantic expectations are evaluated
|
|
9
|
+
separately from declarative fixtures.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
CHAT_FINISH = {"stop", "length", "tool_calls", "content_filter", "function_call"}
|
|
15
|
+
RESPONSES_STATUS = {"completed", "incomplete", "failed", "in_progress", "cancelled", "queued"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Normalized:
|
|
19
|
+
def __init__(self, protocol, stream):
|
|
20
|
+
self.protocol = protocol
|
|
21
|
+
self.stream = stream
|
|
22
|
+
self.id = None
|
|
23
|
+
self.model = None
|
|
24
|
+
self.content = ""
|
|
25
|
+
self.tool_calls = [] # {id, name, arguments, args, index}
|
|
26
|
+
self.finish_reason = None
|
|
27
|
+
self.usage = None # {input, output, total}
|
|
28
|
+
self.status = None
|
|
29
|
+
self.violations = []
|
|
30
|
+
self.events = 0
|
|
31
|
+
self.ttft = None
|
|
32
|
+
self.terminated = False
|
|
33
|
+
self.upstream_error = None # error event/object reported inside a 200 stream
|
|
34
|
+
|
|
35
|
+
def flag(self, message):
|
|
36
|
+
self.violations.append(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_int(value):
|
|
40
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _arguments(call, norm, where):
|
|
44
|
+
raw = call["arguments"]
|
|
45
|
+
if not isinstance(raw, str):
|
|
46
|
+
norm.flag(where + ": arguments must be a JSON string")
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
call["args"] = json.loads(raw) if raw.strip() else {}
|
|
50
|
+
except ValueError:
|
|
51
|
+
norm.flag(where + ": arguments are not valid JSON")
|
|
52
|
+
return
|
|
53
|
+
if not isinstance(call["args"], dict):
|
|
54
|
+
norm.flag(where + ": arguments must encode an object")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _usage(raw, keys, norm):
|
|
58
|
+
if raw is None:
|
|
59
|
+
return None
|
|
60
|
+
if not isinstance(raw, dict):
|
|
61
|
+
norm.flag("usage must be an object")
|
|
62
|
+
return None
|
|
63
|
+
prompt, completion, total = (raw.get(k) for k in keys)
|
|
64
|
+
if not (_is_int(prompt) and _is_int(completion) and _is_int(total)):
|
|
65
|
+
norm.flag("usage token counts must be non-negative integers")
|
|
66
|
+
return None
|
|
67
|
+
if total != prompt + completion:
|
|
68
|
+
norm.flag("usage total is not input plus output tokens")
|
|
69
|
+
return {"input": prompt, "output": completion, "total": total}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def models_list_violations(body):
|
|
73
|
+
problems = []
|
|
74
|
+
if not isinstance(body, dict) or body.get("object") not in (None, "list"):
|
|
75
|
+
return ["models response is not an OpenAI list object"]
|
|
76
|
+
data = body.get("data")
|
|
77
|
+
if not isinstance(data, list):
|
|
78
|
+
return ["models response has no data array"]
|
|
79
|
+
for item in data:
|
|
80
|
+
if not isinstance(item, dict) or not isinstance(item.get("id"), str) or not item["id"]:
|
|
81
|
+
problems.append("model entry without a string id")
|
|
82
|
+
return problems
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def error_violations(body):
|
|
86
|
+
"""OpenAI-compatible error envelope: {"error": {"message", "type"|"code"}}."""
|
|
87
|
+
if not isinstance(body, dict) or not isinstance(body.get("error"), dict):
|
|
88
|
+
return ["error response is not an OpenAI error envelope"]
|
|
89
|
+
error = body["error"]
|
|
90
|
+
problems = []
|
|
91
|
+
if not isinstance(error.get("message"), str) or not error["message"].strip():
|
|
92
|
+
problems.append("error.message missing")
|
|
93
|
+
if not (isinstance(error.get("type"), str) or error.get("code") is not None):
|
|
94
|
+
problems.append("error.type/code missing")
|
|
95
|
+
return problems
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------ Chat Completions
|
|
99
|
+
|
|
100
|
+
def chat_response(body):
|
|
101
|
+
norm = Normalized("chat", False)
|
|
102
|
+
if not isinstance(body, dict):
|
|
103
|
+
norm.flag("body is not a JSON object")
|
|
104
|
+
return norm
|
|
105
|
+
if body.get("object") != "chat.completion":
|
|
106
|
+
norm.flag("object is not chat.completion")
|
|
107
|
+
if not isinstance(body.get("id"), str) or not body["id"]:
|
|
108
|
+
norm.flag("id missing")
|
|
109
|
+
if not _is_int(body.get("created")):
|
|
110
|
+
norm.flag("created is not an integer")
|
|
111
|
+
if not isinstance(body.get("model"), str) or not body["model"]:
|
|
112
|
+
norm.flag("model missing")
|
|
113
|
+
norm.id, norm.model = body.get("id"), body.get("model")
|
|
114
|
+
choices = body.get("choices")
|
|
115
|
+
if not isinstance(choices, list) or not choices:
|
|
116
|
+
norm.flag("choices missing")
|
|
117
|
+
return norm
|
|
118
|
+
choice = choices[0]
|
|
119
|
+
message = choice.get("message") if isinstance(choice, dict) else None
|
|
120
|
+
if not isinstance(message, dict) or message.get("role") != "assistant":
|
|
121
|
+
norm.flag("choices[0].message.role is not assistant")
|
|
122
|
+
return norm
|
|
123
|
+
content = message.get("content")
|
|
124
|
+
if content is not None and not isinstance(content, str):
|
|
125
|
+
norm.flag("message.content must be a string or null")
|
|
126
|
+
norm.content = content or ""
|
|
127
|
+
for index, call in enumerate(message.get("tool_calls") or []):
|
|
128
|
+
where = "tool_calls[%d]" % index
|
|
129
|
+
function = call.get("function") if isinstance(call, dict) else None
|
|
130
|
+
if not isinstance(function, dict) or call.get("type") != "function":
|
|
131
|
+
norm.flag(where + ": not a function call")
|
|
132
|
+
continue
|
|
133
|
+
if not isinstance(call.get("id"), str) or not call["id"]:
|
|
134
|
+
norm.flag(where + ": id missing")
|
|
135
|
+
if not isinstance(function.get("name"), str) or not function["name"]:
|
|
136
|
+
norm.flag(where + ": name missing")
|
|
137
|
+
entry = {"id": call.get("id"), "name": function.get("name"),
|
|
138
|
+
"arguments": function.get("arguments"), "args": None, "index": index}
|
|
139
|
+
_arguments(entry, norm, where)
|
|
140
|
+
norm.tool_calls.append(entry)
|
|
141
|
+
norm.finish_reason = choice.get("finish_reason")
|
|
142
|
+
if norm.finish_reason not in CHAT_FINISH:
|
|
143
|
+
norm.flag("finish_reason is not a valid value")
|
|
144
|
+
norm.usage = _usage(body.get("usage"), ("prompt_tokens", "completion_tokens", "total_tokens"), norm)
|
|
145
|
+
norm.terminated = True
|
|
146
|
+
return norm
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def chat_stream(result):
|
|
150
|
+
norm = Normalized("chat", True)
|
|
151
|
+
norm.violations.extend(result.violations)
|
|
152
|
+
norm.ttft = result.ttft
|
|
153
|
+
norm.terminated = result.done
|
|
154
|
+
if not result.done:
|
|
155
|
+
norm.flag("stream did not end with data: [DONE]")
|
|
156
|
+
parts, calls, finished = [], {}, False
|
|
157
|
+
for _event, chunk in result.json_frames():
|
|
158
|
+
norm.events += 1
|
|
159
|
+
if not isinstance(chunk, dict):
|
|
160
|
+
norm.flag("chunk is not an object")
|
|
161
|
+
continue
|
|
162
|
+
if "error" in chunk:
|
|
163
|
+
norm.upstream_error = chunk["error"]
|
|
164
|
+
continue
|
|
165
|
+
if chunk.get("object") != "chat.completion.chunk":
|
|
166
|
+
norm.flag("chunk object is not chat.completion.chunk")
|
|
167
|
+
if norm.id is None:
|
|
168
|
+
norm.id = chunk.get("id")
|
|
169
|
+
elif chunk.get("id") != norm.id:
|
|
170
|
+
norm.flag("chunk id changed mid-stream")
|
|
171
|
+
norm.model = norm.model or chunk.get("model")
|
|
172
|
+
if chunk.get("usage") is not None:
|
|
173
|
+
norm.usage = _usage(chunk["usage"], ("prompt_tokens", "completion_tokens", "total_tokens"), norm)
|
|
174
|
+
for choice in chunk.get("choices") or []:
|
|
175
|
+
delta = choice.get("delta") if isinstance(choice, dict) else None
|
|
176
|
+
if not isinstance(delta, dict):
|
|
177
|
+
norm.flag("choice without delta")
|
|
178
|
+
continue
|
|
179
|
+
if finished and (delta.get("content") or delta.get("tool_calls")):
|
|
180
|
+
norm.flag("content after finish_reason")
|
|
181
|
+
if delta.get("content"):
|
|
182
|
+
parts.append(delta["content"])
|
|
183
|
+
for fragment in delta.get("tool_calls") or []:
|
|
184
|
+
index = fragment.get("index")
|
|
185
|
+
if not _is_int(index):
|
|
186
|
+
norm.flag("tool call fragment without index")
|
|
187
|
+
continue
|
|
188
|
+
call = calls.setdefault(index, {"id": None, "name": None, "arguments": "", "args": None, "index": index})
|
|
189
|
+
function = fragment.get("function") or {}
|
|
190
|
+
if fragment.get("id"):
|
|
191
|
+
call["id"] = call["id"] or fragment["id"]
|
|
192
|
+
if function.get("name"):
|
|
193
|
+
call["name"] = call["name"] or function["name"]
|
|
194
|
+
if isinstance(function.get("arguments"), str):
|
|
195
|
+
call["arguments"] += function["arguments"]
|
|
196
|
+
if choice.get("finish_reason"):
|
|
197
|
+
if finished:
|
|
198
|
+
norm.flag("more than one finish_reason")
|
|
199
|
+
finished = True
|
|
200
|
+
norm.finish_reason = choice["finish_reason"]
|
|
201
|
+
if norm.finish_reason not in CHAT_FINISH:
|
|
202
|
+
norm.flag("finish_reason is not a valid value")
|
|
203
|
+
if not finished and norm.upstream_error is None:
|
|
204
|
+
norm.flag("stream carried no finish_reason")
|
|
205
|
+
norm.content = "".join(parts)
|
|
206
|
+
for index in sorted(calls):
|
|
207
|
+
call = calls[index]
|
|
208
|
+
if not call["id"] or not call["name"]:
|
|
209
|
+
norm.flag("tool call %d lacks id or name" % index)
|
|
210
|
+
_arguments(call, norm, "tool_calls[%d]" % index)
|
|
211
|
+
norm.tool_calls.append(call)
|
|
212
|
+
return norm
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------- Responses
|
|
216
|
+
|
|
217
|
+
def _responses_final(response, norm):
|
|
218
|
+
"""Fold a final Response object into the normalized view."""
|
|
219
|
+
if not isinstance(response, dict):
|
|
220
|
+
norm.flag("response is not an object")
|
|
221
|
+
return
|
|
222
|
+
if response.get("object") not in (None, "response"):
|
|
223
|
+
norm.flag("object is not response")
|
|
224
|
+
if not isinstance(response.get("id"), str) or not response["id"]:
|
|
225
|
+
norm.flag("response id missing")
|
|
226
|
+
norm.id, norm.model = response.get("id"), response.get("model")
|
|
227
|
+
if not isinstance(norm.model, str) or not norm.model:
|
|
228
|
+
norm.flag("model missing")
|
|
229
|
+
norm.status = response.get("status")
|
|
230
|
+
if norm.status not in RESPONSES_STATUS:
|
|
231
|
+
norm.flag("status is not a valid value")
|
|
232
|
+
output = response.get("output")
|
|
233
|
+
if not isinstance(output, list):
|
|
234
|
+
norm.flag("output is not an array")
|
|
235
|
+
return
|
|
236
|
+
texts, calls = [], []
|
|
237
|
+
for item in output:
|
|
238
|
+
kind = item.get("type") if isinstance(item, dict) else None
|
|
239
|
+
if kind == "message":
|
|
240
|
+
if item.get("role") != "assistant" or not isinstance(item.get("content"), list):
|
|
241
|
+
norm.flag("message item malformed")
|
|
242
|
+
continue
|
|
243
|
+
for part in item["content"]:
|
|
244
|
+
if part.get("type") == "output_text" and isinstance(part.get("text"), str):
|
|
245
|
+
texts.append(part["text"])
|
|
246
|
+
elif part.get("type") == "refusal":
|
|
247
|
+
texts.append(str(part.get("refusal", "")))
|
|
248
|
+
elif kind == "function_call":
|
|
249
|
+
if not isinstance(item.get("call_id"), str) or not item["call_id"]:
|
|
250
|
+
norm.flag("function_call without call_id")
|
|
251
|
+
entry = {"id": item.get("call_id"), "name": item.get("name"),
|
|
252
|
+
"arguments": item.get("arguments"), "args": None, "index": len(calls)}
|
|
253
|
+
if not isinstance(entry["name"], str) or not entry["name"]:
|
|
254
|
+
norm.flag("function_call without name")
|
|
255
|
+
_arguments(entry, norm, "function_call[%d]" % len(calls))
|
|
256
|
+
calls.append(entry)
|
|
257
|
+
norm.content = "".join(texts)
|
|
258
|
+
norm.tool_calls = calls
|
|
259
|
+
details = response.get("incomplete_details") or {}
|
|
260
|
+
if norm.status == "completed":
|
|
261
|
+
norm.finish_reason = "tool_calls" if calls else "stop"
|
|
262
|
+
elif norm.status == "incomplete":
|
|
263
|
+
norm.finish_reason = "length" if details.get("reason") == "max_output_tokens" else "content_filter"
|
|
264
|
+
else:
|
|
265
|
+
norm.finish_reason = norm.status
|
|
266
|
+
norm.usage = _usage(response.get("usage"), ("input_tokens", "output_tokens", "total_tokens"), norm)
|
|
267
|
+
if response.get("error"):
|
|
268
|
+
norm.upstream_error = response["error"]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def responses_response(body):
|
|
272
|
+
norm = Normalized("responses", False)
|
|
273
|
+
_responses_final(body, norm)
|
|
274
|
+
norm.terminated = True
|
|
275
|
+
return norm
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
_TERMINAL = ("response.completed", "response.incomplete", "response.failed")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def responses_stream(result):
|
|
282
|
+
norm = Normalized("responses", True)
|
|
283
|
+
norm.violations.extend(result.violations)
|
|
284
|
+
norm.ttft = result.ttft
|
|
285
|
+
last_sequence = -1
|
|
286
|
+
deltas, arguments, final, order = [], {}, None, []
|
|
287
|
+
for event, data in result.json_frames():
|
|
288
|
+
norm.events += 1
|
|
289
|
+
if not isinstance(data, dict) or not isinstance(data.get("type"), str):
|
|
290
|
+
norm.flag("event data lacks a type")
|
|
291
|
+
continue
|
|
292
|
+
kind = data["type"]
|
|
293
|
+
if event is not None and event != kind:
|
|
294
|
+
norm.flag("SSE event name differs from data.type")
|
|
295
|
+
order.append(kind)
|
|
296
|
+
if "sequence_number" in data:
|
|
297
|
+
if not _is_int(data["sequence_number"]) or data["sequence_number"] <= last_sequence:
|
|
298
|
+
norm.flag("sequence_number is not strictly increasing")
|
|
299
|
+
else:
|
|
300
|
+
last_sequence = data["sequence_number"]
|
|
301
|
+
if kind == "response.output_text.delta":
|
|
302
|
+
if not isinstance(data.get("delta"), str):
|
|
303
|
+
norm.flag("output_text.delta without delta string")
|
|
304
|
+
deltas.append(data.get("delta") or "")
|
|
305
|
+
elif kind == "response.function_call_arguments.delta":
|
|
306
|
+
arguments.setdefault(data.get("output_index"), []).append(data.get("delta") or "")
|
|
307
|
+
elif kind == "error":
|
|
308
|
+
norm.upstream_error = data.get("error") or data
|
|
309
|
+
elif kind in _TERMINAL:
|
|
310
|
+
if final is not None:
|
|
311
|
+
norm.flag("more than one terminal event")
|
|
312
|
+
final = data.get("response")
|
|
313
|
+
if not order or order[0] != "response.created":
|
|
314
|
+
norm.flag("first event is not response.created")
|
|
315
|
+
if final is None:
|
|
316
|
+
if norm.upstream_error is None:
|
|
317
|
+
norm.flag("stream ended without a terminal response event")
|
|
318
|
+
norm.content = "".join(deltas)
|
|
319
|
+
return norm
|
|
320
|
+
norm.terminated = True
|
|
321
|
+
_responses_final(final, norm)
|
|
322
|
+
streamed = "".join(deltas)
|
|
323
|
+
if deltas and norm.content != streamed:
|
|
324
|
+
norm.flag("streamed text differs from the final response")
|
|
325
|
+
# output_index counts every output item, so match calls by ascending index.
|
|
326
|
+
streamed_calls = ["".join(arguments[k]) for k in sorted(arguments, key=lambda k: (k is None, k))]
|
|
327
|
+
for call, pieces in zip(norm.tool_calls, streamed_calls):
|
|
328
|
+
if pieces != call["arguments"]:
|
|
329
|
+
norm.flag("streamed tool arguments differ from the final response")
|
|
330
|
+
return norm
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Credential and prompt redaction for every diagnostic that leaves the process."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_PATTERNS = [
|
|
6
|
+
(re.compile(r"(?i)(bearer\s+)[^\s\"']+"), r"\1[REDACTED]"),
|
|
7
|
+
(re.compile(r"(?:sk-|eyJ|hvs\.|hvb\.)[A-Za-z0-9_.-]{12,}"), "[REDACTED]"),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Redactor:
|
|
12
|
+
"""Replaces every registered secret value and known token shape."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, secrets=()):
|
|
15
|
+
self.secrets = sorted({s for s in secrets if s and len(s) >= 6}, key=len, reverse=True)
|
|
16
|
+
|
|
17
|
+
def add(self, secret):
|
|
18
|
+
if secret and len(secret) >= 6 and secret not in self.secrets:
|
|
19
|
+
self.secrets = sorted(self.secrets + [secret], key=len, reverse=True)
|
|
20
|
+
|
|
21
|
+
def __call__(self, text, limit=300):
|
|
22
|
+
text = str(text)
|
|
23
|
+
for secret in self.secrets:
|
|
24
|
+
text = text.replace(secret, "[REDACTED]")
|
|
25
|
+
for pattern, replacement in _PATTERNS:
|
|
26
|
+
text = pattern.sub(replacement, text)
|
|
27
|
+
text = " ".join(text.split())
|
|
28
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|