@wardby/cli 0.2.1 → 0.4.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/.env.example +53 -4
- package/README.md +52 -13
- package/dist/claude-coding-worker/driver.d.ts +6 -1
- package/dist/claude-coding-worker/driver.js +27 -1
- package/dist/claude-coding-worker/main.js +2 -0
- package/dist/claude-coding-worker/tool-socket.d.ts +9 -0
- package/dist/claude-coding-worker/tool-socket.js +26 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +14 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +284 -36
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/collect-exclude.d.ts +25 -0
- package/dist/coding/collect-exclude.js +76 -0
- package/dist/coding/profile.d.ts +67 -22
- package/dist/coding/profile.js +58 -25
- package/dist/coding/protected-path-wording.d.ts +24 -0
- package/dist/coding/protected-path-wording.js +55 -0
- package/dist/coding/protected-paths.d.ts +31 -0
- package/dist/coding/protected-paths.js +48 -0
- package/dist/coding/protocol.d.ts +91 -4
- package/dist/coding/protocol.js +123 -12
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/coding/registry/adapters.d.ts +2 -0
- package/dist/coding/registry/adapters.js +9 -0
- package/dist/coding/registry/allowlist.d.ts +9 -0
- package/dist/coding/registry/allowlist.js +28 -0
- package/dist/coding/registry/json-scan.d.ts +55 -0
- package/dist/coding/registry/json-scan.js +191 -0
- package/dist/coding/registry/lockfiles.d.ts +6 -0
- package/dist/coding/registry/lockfiles.js +89 -0
- package/dist/coding/registry/npm-lockfile.d.ts +9 -0
- package/dist/coding/registry/npm-lockfile.js +73 -0
- package/dist/coding/registry/npm-plan.d.ts +29 -0
- package/dist/coding/registry/npm-plan.js +289 -0
- package/dist/coding/registry/npm.d.ts +7 -0
- package/dist/coding/registry/npm.js +252 -0
- package/dist/coding/registry/pypi.d.ts +4 -0
- package/dist/coding/registry/pypi.js +210 -0
- package/dist/coding/registry/report.d.ts +37 -0
- package/dist/coding/registry/report.js +31 -0
- package/dist/coding/registry/token.d.ts +4 -0
- package/dist/coding/registry/token.js +7 -0
- package/dist/coding/registry/types.d.ts +286 -0
- package/dist/coding/registry/types.js +19 -0
- package/dist/coding/registry/worker-config.d.ts +14 -0
- package/dist/coding/registry/worker-config.js +31 -0
- package/dist/coding/services/builtins.d.ts +22 -0
- package/dist/coding/services/builtins.js +85 -0
- package/dist/coding/services/catalog.d.ts +443 -0
- package/dist/coding/services/catalog.js +159 -0
- package/dist/coding/services/declaration.d.ts +11 -0
- package/dist/coding/services/declaration.js +114 -0
- package/dist/coding/services/note.d.ts +8 -0
- package/dist/coding/services/note.js +15 -0
- package/dist/coding/services/resolve.d.ts +25 -0
- package/dist/coding/services/resolve.js +50 -0
- package/dist/coding/services/wording.d.ts +25 -0
- package/dist/coding/services/wording.js +70 -0
- package/dist/coding-proxy/main.js +24 -4
- package/dist/coding-worker/artifact.d.ts +6 -0
- package/dist/coding-worker/debug-trace.d.ts +37 -0
- package/dist/coding-worker/debug-trace.js +117 -0
- package/dist/coding-worker/driver.d.ts +13 -1
- package/dist/coding-worker/driver.js +102 -13
- package/dist/coding-worker/errors.js +8 -0
- package/dist/coding-worker/main.js +10 -2
- package/dist/coding-worker/sdk.d.ts +2 -2
- package/dist/coding-worker/sdk.js +7 -2
- package/dist/coding-worker/types.d.ts +3 -0
- package/dist/config/providers.d.ts +66 -0
- package/dist/config/providers.js +137 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +120 -10
- package/dist/core/budget-groups.js +133 -24
- package/dist/core/budget-wording.d.ts +22 -0
- package/dist/core/budget-wording.js +55 -0
- package/dist/core/coding-queue.d.ts +3 -0
- package/dist/core/coding-queue.js +6 -2
- package/dist/core/coding-service-status.d.ts +11 -0
- package/dist/core/coding-service-status.js +17 -0
- package/dist/core/cost-report.d.ts +88 -0
- package/dist/core/cost-report.js +248 -0
- package/dist/core/datastores.js +18 -2
- package/dist/core/db.d.ts +5 -1
- package/dist/core/db.js +8 -2
- package/dist/core/dispatch.d.ts +74 -7
- package/dist/core/dispatch.js +382 -47
- package/dist/core/engine-native.js +44 -14
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/grants.d.ts +86 -0
- package/dist/core/grants.js +126 -0
- package/dist/core/host-events.d.ts +72 -0
- package/dist/core/host-events.js +597 -0
- package/dist/core/host-identity-links.d.ts +54 -0
- package/dist/core/host-identity-links.js +189 -0
- package/dist/core/host-status.d.ts +78 -0
- package/dist/core/host-status.js +228 -0
- package/dist/core/in-flight-runs.d.ts +8 -0
- package/dist/core/in-flight-runs.js +56 -0
- package/dist/core/issue-bridge.d.ts +60 -0
- package/dist/core/issue-bridge.js +189 -0
- package/dist/core/issue-dedupe.d.ts +70 -0
- package/dist/core/issue-dedupe.js +255 -0
- package/dist/core/issue-events.d.ts +42 -0
- package/dist/core/issue-events.js +155 -0
- package/dist/core/issue-status.d.ts +29 -0
- package/dist/core/issue-status.js +241 -0
- package/dist/core/issue-tracker-tools.d.ts +64 -0
- package/dist/core/issue-tracker-tools.js +850 -0
- package/dist/core/model-usage.d.ts +10 -0
- package/dist/core/model-usage.js +24 -0
- package/dist/core/provider-wording.d.ts +12 -0
- package/dist/core/provider-wording.js +44 -0
- package/dist/core/reconciler.d.ts +42 -2
- package/dist/core/reconciler.js +97 -2
- package/dist/core/repo-access.d.ts +99 -0
- package/dist/core/repo-access.js +136 -0
- package/dist/core/review-host-checks.d.ts +11 -0
- package/dist/core/review-host-checks.js +41 -0
- package/dist/core/review-host-tools.d.ts +47 -0
- package/dist/core/review-host-tools.js +354 -0
- package/dist/core/run-heartbeat.d.ts +27 -0
- package/dist/core/run-heartbeat.js +54 -0
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +33 -8
- package/dist/core/runner.js +410 -62
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/secrets.d.ts +11 -2
- package/dist/core/secrets.js +25 -6
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/subagent-memory-tools.d.ts +1 -1
- package/dist/core/subagent-memory-tools.js +16 -3
- package/dist/core/tool-admin.d.ts +81 -0
- package/dist/core/tool-admin.js +129 -0
- package/dist/core/tool-names.d.ts +42 -0
- package/dist/core/tool-names.js +67 -0
- package/dist/core/untrusted-content.d.ts +32 -0
- package/dist/core/untrusted-content.js +72 -0
- package/dist/core/webhooks.d.ts +17 -2
- package/dist/core/webhooks.js +42 -3
- package/dist/generated/prisma/browser.d.ts +166 -0
- package/dist/generated/prisma/client.d.ts +166 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +152 -52
- package/dist/generated/prisma/enums.d.ts +13 -0
- package/dist/generated/prisma/enums.js +12 -1
- package/dist/generated/prisma/internal/class.d.ts +242 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +2571 -577
- package/dist/generated/prisma/internal/prismaNamespace.js +312 -6
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +328 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +312 -6
- package/dist/generated/prisma/models/Agent.d.ts +682 -1
- package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
- package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1425 -0
- package/dist/generated/prisma/models/AgentRepository.js +1 -0
- package/dist/generated/prisma/models/AgentTool.d.ts +95 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +56 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +258 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +195 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +1405 -95
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
- package/dist/generated/prisma/models/CodingService.d.ts +1348 -0
- package/dist/generated/prisma/models/CodingService.js +1 -0
- package/dist/generated/prisma/models/HostEventDelivery.d.ts +946 -0
- package/dist/generated/prisma/models/HostEventDelivery.js +1 -0
- package/dist/generated/prisma/models/HostIdentity.d.ts +1232 -0
- package/dist/generated/prisma/models/HostIdentity.js +1 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.d.ts +1473 -0
- package/dist/generated/prisma/models/HostIdentityLinkRequest.js +1 -0
- package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
- package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
- package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
- package/dist/generated/prisma/models/Principal.d.ts +455 -0
- package/dist/generated/prisma/models/RegistryAllowance.d.ts +1148 -0
- package/dist/generated/prisma/models/RegistryAllowance.js +1 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +1219 -0
- package/dist/generated/prisma/models/RegistryApprovedVersion.js +1 -0
- package/dist/generated/prisma/models/RegistryFetch.d.ts +1428 -0
- package/dist/generated/prisma/models/RegistryFetch.js +1 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.d.ts +1294 -0
- package/dist/generated/prisma/models/RegistryPlanRefusal.js +1 -0
- package/dist/generated/prisma/models/RegistryVersionFact.d.ts +1085 -0
- package/dist/generated/prisma/models/RegistryVersionFact.js +1 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1437 -0
- package/dist/generated/prisma/models/ResourceGrant.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +1402 -82
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.js +1 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +1239 -0
- package/dist/generated/prisma/models/RunHostCheck.js +1 -0
- package/dist/generated/prisma/models/RunHostStatus.d.ts +1315 -0
- package/dist/generated/prisma/models/RunHostStatus.js +1 -0
- package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
- package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
- package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
- package/dist/generated/prisma/models/RunModelUsage.js +1 -0
- package/dist/generated/prisma/models/Tool.d.ts +15 -3
- package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +22 -0
- package/dist/help/build.d.ts +1 -0
- package/dist/help/build.js +9 -0
- package/dist/help/catalog.d.ts +24 -0
- package/dist/help/catalog.js +160 -0
- package/dist/help/cli.d.ts +2 -0
- package/dist/help/cli.js +65 -0
- package/dist/help/runtime.d.ts +3 -0
- package/dist/help/runtime.js +44 -0
- package/dist/help/search.d.ts +9 -0
- package/dist/help/search.js +104 -0
- package/dist/help-index.json +999 -0
- package/dist/import/cli-args.js +3 -2
- package/dist/import/create.d.ts +5 -0
- package/dist/import/create.js +67 -13
- package/dist/import/index.js +19 -9
- package/dist/import/neutral-schema.d.ts +18 -18
- package/dist/knowledge/check.d.ts +13 -0
- package/dist/knowledge/check.js +69 -0
- package/dist/knowledge/cli.d.ts +14 -0
- package/dist/knowledge/cli.js +67 -0
- package/dist/knowledge/concept.d.ts +54 -0
- package/dist/knowledge/concept.js +78 -0
- package/dist/knowledge/note.d.ts +11 -0
- package/dist/knowledge/note.js +39 -0
- package/dist/knowledge/relevance.d.ts +11 -0
- package/dist/knowledge/relevance.js +14 -0
- package/dist/knowledge/span-hash.d.ts +3 -0
- package/dist/knowledge/span-hash.js +16 -0
- package/dist/mcp/auth/access.d.ts +72 -0
- package/dist/mcp/auth/access.js +58 -0
- package/dist/mcp/auth/grants-cli.d.ts +149 -0
- package/dist/mcp/auth/grants-cli.js +518 -0
- package/dist/mcp/auth/host-account-cli.d.ts +2 -0
- package/dist/mcp/auth/host-account-cli.js +47 -0
- package/dist/mcp/auth/ownership.d.ts +25 -51
- package/dist/mcp/auth/ownership.js +19 -14
- package/dist/mcp/auth/repo-authorization.d.ts +22 -0
- package/dist/mcp/auth/repo-authorization.js +51 -0
- package/dist/mcp/auth/resource-server.d.ts +33 -2
- package/dist/mcp/auth/resource-server.js +99 -3
- package/dist/mcp/auth/self-hosted/browser.js +2 -2
- package/dist/mcp/auth/self-hosted/cli.js +47 -13
- package/dist/mcp/auth/self-hosted/credentials.d.ts +23 -5
- package/dist/mcp/auth/self-hosted/credentials.js +62 -4
- package/dist/mcp/auth/self-hosted/session.d.ts +7 -6
- package/dist/mcp/context.d.ts +30 -1
- package/dist/mcp/errors.d.ts +25 -6
- package/dist/mcp/errors.js +98 -0
- package/dist/mcp/host-events/deliveries.d.ts +9 -0
- package/dist/mcp/host-events/deliveries.js +17 -0
- package/dist/mcp/host-events/github-ingress.d.ts +38 -0
- package/dist/mcp/host-events/github-ingress.js +83 -0
- package/dist/mcp/host-events/github-user-callback.d.ts +18 -0
- package/dist/mcp/host-events/github-user-callback.js +50 -0
- package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
- package/dist/mcp/host-events/jira-ingress.js +92 -0
- package/dist/mcp/index.d.ts +10 -1
- package/dist/mcp/index.js +187 -19
- package/dist/mcp/server.js +32 -11
- package/dist/mcp/tools/agents.js +519 -50
- package/dist/mcp/tools/budget-groups.js +3 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/datastore.js +22 -15
- package/dist/mcp/tools/grants.d.ts +2 -0
- package/dist/mcp/tools/grants.js +239 -0
- package/dist/mcp/tools/help.d.ts +5 -0
- package/dist/mcp/tools/help.js +67 -0
- package/dist/mcp/tools/host-accounts.d.ts +2 -0
- package/dist/mcp/tools/host-accounts.js +114 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/memory.d.ts +7 -1
- package/dist/mcp/tools/memory.js +5 -5
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.d.ts +2 -0
- package/dist/mcp/tools/repositories.js +182 -0
- package/dist/mcp/tools/runs.d.ts +6 -0
- package/dist/mcp/tools/runs.js +60 -6
- package/dist/mcp/tools/scheduling.js +3 -6
- package/dist/mcp/tools/secrets.js +18 -6
- package/dist/mcp/tools/services.d.ts +2 -0
- package/dist/mcp/tools/services.js +222 -0
- package/dist/mcp/tools/subagents.js +52 -16
- package/dist/mcp/tools/tools.d.ts +41 -0
- package/dist/mcp/tools/tools.js +201 -40
- package/dist/mcp/tools/trigger.js +65 -7
- package/dist/mcp/tools/webhooks.js +15 -4
- package/dist/mcp/transport/streamable-http.d.ts +15 -0
- package/dist/mcp/transport/streamable-http.js +55 -3
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/delegating.d.ts +19 -0
- package/dist/providers/auth/delegating.js +71 -0
- package/dist/providers/auth/index.d.ts +2 -0
- package/dist/providers/auth/index.js +11 -0
- package/dist/providers/auth/self-hosted.d.ts +10 -2
- package/dist/providers/auth/self-hosted.js +55 -6
- package/dist/providers/auth/types.d.ts +6 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +4 -1
- package/dist/providers/coding-proxy/memory-ledger.js +31 -3
- package/dist/providers/coding-proxy/metering.d.ts +6 -1
- package/dist/providers/coding-proxy/metering.js +41 -4
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +17 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +106 -8
- package/dist/providers/coding-proxy/proxy.d.ts +25 -2
- package/dist/providers/coding-proxy/proxy.js +561 -38
- package/dist/providers/coding-proxy/registry/audit.d.ts +131 -0
- package/dist/providers/coding-proxy/registry/audit.js +380 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/bounded-fetch.js +77 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +58 -0
- package/dist/providers/coding-proxy/registry/plan.js +304 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +34 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +151 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +213 -0
- package/dist/providers/coding-proxy/registry/service.js +1137 -0
- package/dist/providers/coding-proxy/registry/store.d.ts +127 -0
- package/dist/providers/coding-proxy/registry/store.js +70 -0
- package/dist/providers/coding-proxy/runtime.d.ts +17 -0
- package/dist/providers/coding-proxy/runtime.js +63 -0
- package/dist/providers/coding-proxy/secure-fetch.js +0 -1
- package/dist/providers/coding-proxy/server.d.ts +11 -0
- package/dist/providers/coding-proxy/server.js +163 -0
- package/dist/providers/coding-proxy/types.d.ts +33 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +29 -0
- package/dist/providers/executor/build.d.ts +2 -2
- package/dist/providers/executor/composition.d.ts +3 -0
- package/dist/providers/executor/composition.js +28 -1
- package/dist/providers/executor/container.d.ts +134 -4
- package/dist/providers/executor/container.js +394 -37
- package/dist/providers/executor/dbos.d.ts +4 -3
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/in-process.d.ts +2 -3
- package/dist/providers/executor/routing.d.ts +13 -0
- package/dist/providers/executor/routing.js +18 -0
- package/dist/providers/executor/types.d.ts +34 -0
- package/dist/providers/issue-tracker/adf.d.ts +31 -0
- package/dist/providers/issue-tracker/adf.js +181 -0
- package/dist/providers/issue-tracker/index.d.ts +5 -0
- package/dist/providers/issue-tracker/index.js +12 -0
- package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
- package/dist/providers/issue-tracker/jira-client.js +151 -0
- package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
- package/dist/providers/issue-tracker/jira-events.js +98 -0
- package/dist/providers/issue-tracker/jira.d.ts +116 -0
- package/dist/providers/issue-tracker/jira.js +502 -0
- package/dist/providers/issue-tracker/types.d.ts +269 -0
- package/dist/providers/issue-tracker/types.js +16 -0
- package/dist/providers/jobs/claude-tool-setup.d.ts +23 -0
- package/dist/providers/jobs/claude-tool-setup.js +50 -0
- package/dist/providers/jobs/collect-prune.d.ts +7 -0
- package/dist/providers/jobs/collect-prune.js +27 -0
- package/dist/providers/jobs/docker-isolation.d.ts +48 -3
- package/dist/providers/jobs/docker-isolation.js +213 -22
- package/dist/providers/jobs/docker-services.d.ts +35 -0
- package/dist/providers/jobs/docker-services.js +191 -0
- package/dist/providers/jobs/docker.d.ts +53 -2
- package/dist/providers/jobs/docker.js +307 -32
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +1 -0
- package/dist/providers/jobs/fake-kubernetes-api.js +12 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +17 -2
- package/dist/providers/jobs/kubernetes-isolation.js +238 -57
- package/dist/providers/jobs/kubernetes-platform.d.ts +10 -4
- package/dist/providers/jobs/kubernetes-platform.js +11 -5
- package/dist/providers/jobs/kubernetes-preflight.js +3 -0
- package/dist/providers/jobs/kubernetes.d.ts +24 -2
- package/dist/providers/jobs/kubernetes.js +153 -22
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/jobs/types.d.ts +18 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -5
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -8
- package/dist/providers/llm/catalog-lookup.d.ts +10 -0
- package/dist/providers/llm/catalog-lookup.js +15 -0
- package/dist/providers/llm/catalog-shipped.d.ts +18 -0
- package/dist/providers/llm/catalog-shipped.js +197 -0
- package/dist/providers/llm/catalog-store.d.ts +58 -0
- package/dist/providers/llm/catalog-store.js +138 -0
- package/dist/providers/llm/catalog-types.d.ts +66 -0
- package/dist/providers/llm/catalog-types.js +64 -0
- package/dist/providers/llm/catalog.d.ts +61 -0
- package/dist/providers/llm/catalog.js +147 -0
- package/dist/providers/llm/claude-messages.d.ts +5 -1
- package/dist/providers/llm/claude-messages.js +1 -0
- package/dist/providers/llm/claude-provider.d.ts +10 -12
- package/dist/providers/llm/claude-provider.js +15 -6
- package/dist/providers/llm/index.d.ts +9 -6
- package/dist/providers/llm/index.js +8 -5
- package/dist/providers/llm/openai.d.ts +14 -5
- package/dist/providers/llm/openai.js +24 -14
- package/dist/providers/llm/pricing-core.d.ts +5 -3
- package/dist/providers/llm/registration.js +8 -12
- package/dist/providers/llm/routing.d.ts +21 -11
- package/dist/providers/llm/routing.js +47 -13
- package/dist/providers/llm/types.d.ts +12 -0
- package/dist/providers/llm/types.js +8 -1
- package/dist/providers/review-host/diff-lines.d.ts +16 -0
- package/dist/providers/review-host/diff-lines.js +59 -0
- package/dist/providers/review-host/github-events.d.ts +6 -0
- package/dist/providers/review-host/github-events.js +226 -0
- package/dist/providers/review-host/github-user-auth.d.ts +38 -0
- package/dist/providers/review-host/github-user-auth.js +128 -0
- package/dist/providers/review-host/github.d.ts +57 -0
- package/dist/providers/review-host/github.js +568 -0
- package/dist/providers/review-host/index.d.ts +12 -0
- package/dist/providers/review-host/index.js +30 -0
- package/dist/providers/review-host/review-format.d.ts +19 -0
- package/dist/providers/review-host/review-format.js +59 -0
- package/dist/providers/review-host/types.d.ts +309 -0
- package/dist/providers/review-host/types.js +24 -0
- package/dist/providers/vcs/git.d.ts +16 -6
- package/dist/providers/vcs/git.js +78 -34
- package/dist/providers/vcs/github.d.ts +94 -3
- package/dist/providers/vcs/github.js +223 -17
- package/dist/providers/vcs/types.d.ts +48 -4
- package/dist/quickstart/index.d.ts +2 -0
- package/dist/quickstart/index.js +36 -8
- package/dist/sandbox/fetch-policy.d.ts +20 -2
- package/dist/sandbox/fetch-policy.js +64 -3
- package/dist/sandbox/host-functions.d.ts +9 -1
- package/dist/sandbox/host-functions.js +12 -5
- package/dist/serve.js +8 -2
- package/dist/viewer/api-schema.d.ts +2757 -0
- package/dist/viewer/api-schema.js +165 -0
- package/dist/viewer/build-schemas.d.ts +2 -0
- package/dist/viewer/build-schemas.js +18 -0
- package/dist/viewer/event-bus.d.ts +38 -0
- package/dist/viewer/event-bus.js +232 -0
- package/dist/viewer/graph.d.ts +40 -0
- package/dist/viewer/graph.js +243 -0
- package/dist/viewer/http.d.ts +30 -0
- package/dist/viewer/http.js +133 -0
- package/dist/viewer/run-detail.d.ts +4 -0
- package/dist/viewer/run-detail.js +61 -0
- package/dist/wardby-bin.js +11 -0
- package/docs/README.md +40 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/architecture-runtime.md +90 -0
- package/docs/assets/brand/wardby-icon-512.png +0 -0
- package/docs/assets/brand/wardby-icon.svg +16 -0
- package/docs/assets/brand/wardby-mascot-profile-512.png +0 -0
- package/docs/assets/brand/wardby-mascot.png +0 -0
- package/docs/assets/brand/wardby-mascot.svg +5 -0
- package/docs/assets/wardby-workflow.svg +106 -0
- package/docs/code-review-agents.md +508 -0
- package/docs/coding-agent-setup.md +175 -0
- package/docs/coding-packages.md +455 -0
- package/docs/coding-services.md +300 -0
- package/docs/coding-worker-byo-images.md +98 -0
- package/docs/coding-worker-isolation.md +1068 -0
- package/docs/getting-started-gke.md +632 -0
- package/docs/getting-started-identity-provider.md +319 -0
- package/docs/getting-started.md +147 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/observability.md +53 -0
- package/docs/release-verification.md +66 -0
- package/docs/security-deployment.md +667 -0
- package/docs/viewer-api.md +142 -0
- package/help/admin-viewer.md +39 -0
- package/help/agent-recipes.md +173 -0
- package/help/architecture-agent.md +189 -0
- package/help/builder-agent.md +80 -0
- package/help/code-review-agents.md +37 -0
- package/help/coding-packages.md +31 -0
- package/help/coding-services.md +71 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +90 -0
- package/help/deploy-gke.md +45 -0
- package/help/deployment-targets.md +39 -0
- package/help/errors/budget-group-exhausted.md +25 -0
- package/help/errors/docker-isolation-unsupported.md +26 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/errors/protected-path.md +45 -0
- package/help/errors/repo-access.md +27 -0
- package/help/errors/service-declaration-invalid.md +27 -0
- package/help/errors/service-declaration-unavailable.md +25 -0
- package/help/errors/service-launcher-unsupported.md +30 -0
- package/help/errors/service-not-allowed.md +27 -0
- package/help/errors/service-unknown.md +23 -0
- package/help/errors/service-unready.md +49 -0
- package/help/getting-started.md +32 -0
- package/help/github.md +48 -0
- package/help/identity-and-access.md +44 -0
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/mcp.md +30 -0
- package/help/models.md +90 -0
- package/help/native-capabilities.md +30 -0
- package/help/observability.md +41 -0
- package/help/operating-agents.md +36 -0
- package/help/security.md +27 -0
- package/help/troubleshooting/budgets.md +32 -0
- package/help/troubleshooting/coding-workers.md +47 -0
- package/help/troubleshooting/repository-access.md +27 -0
- package/package.json +14 -3
- package/prisma/migrations/20260925010000_coding_collect_exclude/migration.sql +8 -0
- package/prisma/migrations/20260925015000_allowed_egress_default/migration.sql +8 -0
- package/prisma/migrations/20260925020000_coding_package_registry/migration.sql +55 -0
- package/prisma/migrations/20260925030000_tool_name_per_owner/migration.sql +11 -0
- package/prisma/migrations/20260926010000_run_trigger_host_event/migration.sql +7 -0
- package/prisma/migrations/20260926020000_code_review_hosts/migration.sql +53 -0
- package/prisma/migrations/20260926030000_auth_user_roles/migration.sql +9 -0
- package/prisma/migrations/20260926040000_agent_effort/migration.sql +6 -0
- package/prisma/migrations/20260926050000_registry_lockfile_plan/migration.sql +32 -0
- package/prisma/migrations/20260926050000_repo_access_authorization/migration.sql +82 -0
- package/prisma/migrations/20260926060000_registry_plan_refusal/migration.sql +19 -0
- package/prisma/migrations/20260926100000_registry_plan_refusal_published_at/migration.sql +5 -0
- package/prisma/migrations/20260926190000_run_host_status/migration.sql +18 -0
- package/prisma/migrations/20260926210000_run_host_status_at_dispatch/migration.sql +5 -0
- package/prisma/migrations/20260927010000_resource_grants/migration.sql +72 -0
- package/prisma/migrations/20260927020000_proxy_session_budget_exhausted/migration.sql +3 -0
- package/prisma/migrations/20260927030000_coding_debug_trace/migration.sql +4 -0
- package/prisma/migrations/20260927040000_proxy_session_upstream_failure/migration.sql +3 -0
- package/prisma/migrations/20260927050000_coding_run_services/migration.sql +74 -0
- package/prisma/migrations/20260928000000_coding_run_tool_image/migration.sql +5 -0
- package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
- package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
- package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
- package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
- package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
- package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
- package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
- package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
- package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
- package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
- package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
- package/prisma/schema.prisma +663 -21
- package/dist/mcp/tools/models.d.ts +0 -8
- package/dist/mcp/tools/models.js +0 -15
- package/dist/providers/llm/pricing-anthropic.d.ts +0 -4
- package/dist/providers/llm/pricing-anthropic.js +0 -30
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -12
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -37
- package/dist/providers/llm/pricing.d.ts +0 -30
- package/dist/providers/llm/pricing.js +0 -74
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { shippedCatalog } from "../llm/catalog.js";
|
|
3
|
+
import { SHIPPED_CATALOG_VERSION } from "../llm/catalog-shipped.js";
|
|
4
|
+
import { deriveRegistryToken } from "../../coding/registry/token.js";
|
|
5
|
+
import { AnthropicSseUsageTracker, actualCostUsd, estimateReservationUsd, fingerprintRequest, parseAnthropicAuthoritativeUsage, parseAuthoritativeUsage, pricingSnapshot, safeUpstreamErrorCode, terminalUsageFromSseFrame, } from "./metering.js";
|
|
5
6
|
import { createPinnedProxyFetch } from "./secure-fetch.js";
|
|
6
7
|
export const PROXY_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
7
8
|
export const PROXY_MAX_OUTPUT_TOKENS = 1_000_000;
|
|
@@ -16,12 +17,26 @@ export const CLAUDE_CODE_ANTHROPIC_BETAS = [
|
|
|
16
17
|
"thinking-token-count-2026-05-13",
|
|
17
18
|
];
|
|
18
19
|
const MAX_UPSTREAM_JSON_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
/** A rejected upstream's error body is read only this far, for its error code. */
|
|
21
|
+
const MAX_UPSTREAM_ERROR_BYTES = 64 * 1024;
|
|
19
22
|
const REQUEST_KEY_PATTERN = /^[\x21-\x7e]{1,200}$/;
|
|
20
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Betas Claude Code sends only for some models. per-turn-control is sent for Claude Opus 5.5; it is
|
|
25
|
+
* required only by an effort-only system message (per-turn effort), and every body field is still
|
|
26
|
+
* checked against the reviewed shape, so the header alone unlocks nothing.
|
|
27
|
+
*/
|
|
28
|
+
export const OPTIONAL_ANTHROPIC_BETAS = ["per-turn-control-2026-07-01"];
|
|
29
|
+
const APPROVED_ANTHROPIC_BETAS = new Set([...CLAUDE_CODE_ANTHROPIC_BETAS, ...OPTIONAL_ANTHROPIC_BETAS]);
|
|
21
30
|
const WARDBY_COMMAND_TOOL = "mcp__wardby_tools__run_command";
|
|
22
31
|
const STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
|
|
23
32
|
const MAX_COMMAND_BYTES = 16 * 1024;
|
|
24
33
|
const MAX_TOOL_TEXT_BYTES = 256 * 1024;
|
|
34
|
+
/**
|
|
35
|
+
* The longest run_command timeout the Claude tool runner offers the model (its MAX_TIMEOUT_MS,
|
|
36
|
+
* src/claude-tool-runner/command.mjs; the proxy tests keep the two equal). A lower limit here let the
|
|
37
|
+
* command run, then refused the next request, which replays that tool call, as unsupported.
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_COMMAND_TIMEOUT_MS = 120_000;
|
|
25
40
|
export class CodingProxyError extends Error {
|
|
26
41
|
status;
|
|
27
42
|
code;
|
|
@@ -31,9 +46,361 @@ export class CodingProxyError extends Error {
|
|
|
31
46
|
this.code = code;
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
|
-
function capabilityHash(capability) {
|
|
49
|
+
export function capabilityHash(capability) {
|
|
35
50
|
return createHash("sha256").update(capability).digest("base64url");
|
|
36
51
|
}
|
|
52
|
+
// OpenAI Responses allowlist. Everything a request may carry is listed here;
|
|
53
|
+
// anything else is refused before a credential is resolved. It mirrors what the
|
|
54
|
+
// pinned Codex CLI (src/coding-worker/package.json) actually sends, recorded in
|
|
55
|
+
// fixtures/codex-<version>-responses-requests.json. Only client-side tools
|
|
56
|
+
// (function, custom, and namespaces of them) are allowed: a hosted tool (web
|
|
57
|
+
// search, remote MCP, code interpreter, image generation, file search, ...), a
|
|
58
|
+
// remote image or file, a stored prompt, or a previous response would make
|
|
59
|
+
// OpenAI fetch or run something outside the worker's egress policy and outside
|
|
60
|
+
// the token-metered budget. Tool names vary by model and Codex version, so they
|
|
61
|
+
// are checked for shape, not against a fixed list: a client-side tool the model
|
|
62
|
+
// calls is executed by the worker itself, which the container boundary governs.
|
|
63
|
+
const OPENAI_REQUEST_KEYS = new Set([
|
|
64
|
+
"model",
|
|
65
|
+
"instructions",
|
|
66
|
+
"input",
|
|
67
|
+
"tools",
|
|
68
|
+
"tool_choice",
|
|
69
|
+
"parallel_tool_calls",
|
|
70
|
+
"reasoning",
|
|
71
|
+
"store",
|
|
72
|
+
"stream",
|
|
73
|
+
"include",
|
|
74
|
+
"prompt_cache_key",
|
|
75
|
+
"text",
|
|
76
|
+
"client_metadata",
|
|
77
|
+
"max_output_tokens",
|
|
78
|
+
"background",
|
|
79
|
+
"service_tier",
|
|
80
|
+
]);
|
|
81
|
+
const OPENAI_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
82
|
+
const OPENAI_REASONING_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
|
|
83
|
+
const OPENAI_REASONING_CONTEXTS = new Set(["all_turns", "current_turn"]);
|
|
84
|
+
const OPENAI_INCLUDES = new Set(["reasoning.encrypted_content"]);
|
|
85
|
+
// Only unset (Codex never sends one) or an explicit "default". "auto" defers to
|
|
86
|
+
// the OpenAI project's own tier setting, which may be priority or scale; those
|
|
87
|
+
// and "flex" are billed at rates the pricing table does not track.
|
|
88
|
+
const OPENAI_SERVICE_TIERS = new Set(["default"]);
|
|
89
|
+
// The client_metadata keys the pinned Codex sends (recorded fixture). Several
|
|
90
|
+
// are x-codex-*/x-openai-* keys whose server-side meaning is undocumented, so a
|
|
91
|
+
// new key fails closed like every other unknown field.
|
|
92
|
+
const OPENAI_CLIENT_METADATA_KEYS = new Set([
|
|
93
|
+
"parent_turn_id",
|
|
94
|
+
"root_turn_id",
|
|
95
|
+
"session_id",
|
|
96
|
+
"thread_id",
|
|
97
|
+
"turn_id",
|
|
98
|
+
"x-codex-installation-id",
|
|
99
|
+
"x-codex-parent-thread-id",
|
|
100
|
+
"x-codex-turn-metadata",
|
|
101
|
+
"x-codex-window-id",
|
|
102
|
+
"x-openai-subagent",
|
|
103
|
+
]);
|
|
104
|
+
const OPENAI_TOOL_CHOICES = new Set(["auto", "none", "required"]);
|
|
105
|
+
const OPENAI_VERBOSITIES = new Set(["low", "medium", "high"]);
|
|
106
|
+
const OPENAI_IMAGE_DETAILS = new Set(["auto", "low", "high", "original"]);
|
|
107
|
+
const OPENAI_MESSAGE_ROLES = new Set(["user", "developer", "system", "assistant"]);
|
|
108
|
+
const OPENAI_MESSAGE_PHASES = new Set(["commentary", "final_answer"]);
|
|
109
|
+
const OPENAI_GRAMMAR_SYNTAXES = new Set(["lark", "regex"]);
|
|
110
|
+
const OPENAI_INPUT_PARTS = new Set(["input_text", "input_image"]);
|
|
111
|
+
const OPENAI_ASSISTANT_PARTS = new Set(["output_text"]);
|
|
112
|
+
const OPENAI_AGENT_MESSAGE_PARTS = new Set(["input_text", "encrypted_content"]);
|
|
113
|
+
const OPENAI_TOOL_NAME = /^[A-Za-z0-9_-]{1,128}$/;
|
|
114
|
+
const OPENAI_CODE_SUFFIX = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
115
|
+
// Only an inline base64 image: Codex's view_image and code-mode image() send
|
|
116
|
+
// local files this way. A URL (or a file_id) would make OpenAI fetch it.
|
|
117
|
+
const OPENAI_INLINE_IMAGE = /^data:image\/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
118
|
+
const MAX_OPENAI_ITEMS = 10_000;
|
|
119
|
+
const MAX_OPENAI_TOOLS = 512;
|
|
120
|
+
const MAX_OPENAI_ID_LENGTH = 512;
|
|
121
|
+
const MAX_OPENAI_METADATA_VALUE_LENGTH = 16 * 1024;
|
|
122
|
+
function openAiCode(prefix, value) {
|
|
123
|
+
return `${prefix}:${typeof value === "string" && OPENAI_CODE_SUFFIX.test(value) ? value : "other"}`;
|
|
124
|
+
}
|
|
125
|
+
function openAiRecord(value, code = "invalid_openai_request") {
|
|
126
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
127
|
+
throw new CodingProxyError(400, code);
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
function openAiOnlyKeys(value, keys, code = "invalid_openai_request") {
|
|
131
|
+
const allowed = new Set(keys);
|
|
132
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
133
|
+
throw new CodingProxyError(400, code);
|
|
134
|
+
}
|
|
135
|
+
function openAiString(value, maxLength, code = "invalid_openai_request") {
|
|
136
|
+
if (typeof value !== "string" || value.length > maxLength)
|
|
137
|
+
throw new CodingProxyError(400, code);
|
|
138
|
+
}
|
|
139
|
+
function openAiOptional(value, check) {
|
|
140
|
+
if (value !== undefined)
|
|
141
|
+
check(value);
|
|
142
|
+
}
|
|
143
|
+
function openAiId(value) {
|
|
144
|
+
if (typeof value !== "string" || value.length < 1 || value.length > MAX_OPENAI_ID_LENGTH) {
|
|
145
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function openAiToolName(value, code = "invalid_openai_request") {
|
|
149
|
+
if (typeof value !== "string" || !OPENAI_TOOL_NAME.test(value))
|
|
150
|
+
throw new CodingProxyError(400, code);
|
|
151
|
+
}
|
|
152
|
+
function validateOpenAiFunctionTool(tool) {
|
|
153
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "strict", "parameters"], "invalid_openai_tool");
|
|
154
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
155
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
156
|
+
if (tool.strict !== undefined && typeof tool.strict !== "boolean") {
|
|
157
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
158
|
+
}
|
|
159
|
+
openAiOptional(tool.parameters, (value) => openAiRecord(value, "invalid_openai_tool"));
|
|
160
|
+
}
|
|
161
|
+
function validateOpenAiCustomTool(tool) {
|
|
162
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "format"], "invalid_openai_tool");
|
|
163
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
164
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
165
|
+
if (tool.format === undefined)
|
|
166
|
+
return;
|
|
167
|
+
const format = openAiRecord(tool.format, "invalid_openai_tool");
|
|
168
|
+
if (format.type === "text")
|
|
169
|
+
return openAiOnlyKeys(format, ["type"], "invalid_openai_tool");
|
|
170
|
+
openAiOnlyKeys(format, ["type", "syntax", "definition"], "invalid_openai_tool");
|
|
171
|
+
if (format.type !== "grammar" || typeof format.syntax !== "string" || !OPENAI_GRAMMAR_SYNTAXES.has(format.syntax)) {
|
|
172
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
173
|
+
}
|
|
174
|
+
openAiString(format.definition, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool");
|
|
175
|
+
}
|
|
176
|
+
function validateOpenAiTools(value, count, nested = false) {
|
|
177
|
+
if (!Array.isArray(value))
|
|
178
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
179
|
+
for (const candidate of value) {
|
|
180
|
+
if (++count.value > MAX_OPENAI_TOOLS)
|
|
181
|
+
throw new CodingProxyError(400, "invalid_openai_tool");
|
|
182
|
+
const tool = openAiRecord(candidate, "invalid_openai_tool");
|
|
183
|
+
if (tool.type === "function") {
|
|
184
|
+
validateOpenAiFunctionTool(tool);
|
|
185
|
+
}
|
|
186
|
+
else if (tool.type === "custom") {
|
|
187
|
+
validateOpenAiCustomTool(tool);
|
|
188
|
+
}
|
|
189
|
+
else if (tool.type === "namespace" && !nested) {
|
|
190
|
+
openAiOnlyKeys(tool, ["type", "name", "description", "tools"], "invalid_openai_tool");
|
|
191
|
+
openAiToolName(tool.name, "invalid_openai_tool");
|
|
192
|
+
openAiOptional(tool.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "invalid_openai_tool"));
|
|
193
|
+
validateOpenAiTools(tool.tools, count, true);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
throw new CodingProxyError(400, openAiCode("openai_tool_not_allowed", tool.type));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function validateOpenAiContentPart(value, allowed) {
|
|
201
|
+
const part = openAiRecord(value);
|
|
202
|
+
if (typeof part.type !== "string" || !allowed.has(part.type)) {
|
|
203
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", part.type));
|
|
204
|
+
}
|
|
205
|
+
if (part.type === "input_image") {
|
|
206
|
+
if (part.file_id !== undefined || typeof part.image_url !== "string" || !OPENAI_INLINE_IMAGE.test(part.image_url)) {
|
|
207
|
+
throw new CodingProxyError(400, "openai_remote_input_not_allowed");
|
|
208
|
+
}
|
|
209
|
+
openAiOnlyKeys(part, ["type", "image_url", "detail"]);
|
|
210
|
+
if (part.detail !== undefined && (typeof part.detail !== "string" || !OPENAI_IMAGE_DETAILS.has(part.detail))) {
|
|
211
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (part.type === "encrypted_content") {
|
|
215
|
+
openAiOnlyKeys(part, ["type", "encrypted_content"]);
|
|
216
|
+
openAiString(part.encrypted_content, PROXY_MAX_BODY_BYTES);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
openAiOnlyKeys(part, ["type", "text"]);
|
|
220
|
+
openAiString(part.text, PROXY_MAX_BODY_BYTES);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function validateOpenAiContent(value, allowed, allowString = true) {
|
|
224
|
+
if (typeof value === "string" && allowString)
|
|
225
|
+
return;
|
|
226
|
+
if (!Array.isArray(value) || value.length > MAX_OPENAI_ITEMS)
|
|
227
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
228
|
+
for (const part of value)
|
|
229
|
+
validateOpenAiContentPart(part, allowed);
|
|
230
|
+
}
|
|
231
|
+
function validateOpenAiTextParts(value, type) {
|
|
232
|
+
if (!Array.isArray(value) || value.length > MAX_OPENAI_ITEMS)
|
|
233
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
234
|
+
for (const candidate of value) {
|
|
235
|
+
const part = openAiRecord(candidate);
|
|
236
|
+
openAiOnlyKeys(part, ["type", "text"]);
|
|
237
|
+
if (part.type !== type)
|
|
238
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", part.type));
|
|
239
|
+
openAiString(part.text, PROXY_MAX_BODY_BYTES);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function validateOpenAiInputItem(value, toolCount) {
|
|
243
|
+
const item = openAiRecord(value);
|
|
244
|
+
switch (item.type) {
|
|
245
|
+
case undefined:
|
|
246
|
+
case "message": {
|
|
247
|
+
openAiOnlyKeys(item, ["type", "id", "role", "content", "phase"]);
|
|
248
|
+
openAiOptional(item.id, openAiId);
|
|
249
|
+
if (typeof item.role !== "string" || !OPENAI_MESSAGE_ROLES.has(item.role)) {
|
|
250
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
251
|
+
}
|
|
252
|
+
if (item.phase !== undefined && (typeof item.phase !== "string" || !OPENAI_MESSAGE_PHASES.has(item.phase))) {
|
|
253
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
254
|
+
}
|
|
255
|
+
validateOpenAiContent(item.content, item.role === "assistant" ? OPENAI_ASSISTANT_PARTS : OPENAI_INPUT_PARTS);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
case "reasoning":
|
|
259
|
+
openAiOnlyKeys(item, ["type", "id", "summary", "content", "encrypted_content"]);
|
|
260
|
+
openAiOptional(item.id, openAiId);
|
|
261
|
+
validateOpenAiTextParts(item.summary, "summary_text");
|
|
262
|
+
if (item.content !== undefined && item.content !== null)
|
|
263
|
+
validateOpenAiTextParts(item.content, "reasoning_text");
|
|
264
|
+
if (item.encrypted_content !== undefined && item.encrypted_content !== null) {
|
|
265
|
+
openAiString(item.encrypted_content, PROXY_MAX_BODY_BYTES);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
case "function_call":
|
|
269
|
+
openAiOnlyKeys(item, ["type", "id", "call_id", "name", "namespace", "arguments"]);
|
|
270
|
+
openAiOptional(item.id, openAiId);
|
|
271
|
+
openAiId(item.call_id);
|
|
272
|
+
openAiToolName(item.name);
|
|
273
|
+
openAiOptional(item.namespace, (value) => openAiToolName(value));
|
|
274
|
+
openAiString(item.arguments, PROXY_MAX_BODY_BYTES);
|
|
275
|
+
return;
|
|
276
|
+
case "custom_tool_call":
|
|
277
|
+
openAiOnlyKeys(item, ["type", "id", "status", "call_id", "name", "input"]);
|
|
278
|
+
openAiOptional(item.id, openAiId);
|
|
279
|
+
openAiOptional(item.status, (value) => openAiString(value, 64));
|
|
280
|
+
openAiId(item.call_id);
|
|
281
|
+
openAiToolName(item.name);
|
|
282
|
+
openAiString(item.input, PROXY_MAX_BODY_BYTES);
|
|
283
|
+
return;
|
|
284
|
+
case "function_call_output":
|
|
285
|
+
case "custom_tool_call_output":
|
|
286
|
+
openAiOnlyKeys(item, item.type === "function_call_output"
|
|
287
|
+
? ["type", "id", "call_id", "output"]
|
|
288
|
+
: ["type", "id", "call_id", "name", "output"]);
|
|
289
|
+
openAiOptional(item.id, openAiId);
|
|
290
|
+
openAiId(item.call_id);
|
|
291
|
+
openAiOptional(item.name, (value) => openAiToolName(value));
|
|
292
|
+
validateOpenAiContent(item.output, OPENAI_INPUT_PARTS);
|
|
293
|
+
return;
|
|
294
|
+
case "agent_message":
|
|
295
|
+
openAiOnlyKeys(item, ["type", "id", "author", "recipient", "content"]);
|
|
296
|
+
openAiOptional(item.id, openAiId);
|
|
297
|
+
openAiOptional(item.author, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
298
|
+
openAiOptional(item.recipient, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
299
|
+
validateOpenAiContent(item.content, OPENAI_AGENT_MESSAGE_PARTS, false);
|
|
300
|
+
return;
|
|
301
|
+
case "additional_tools":
|
|
302
|
+
openAiOnlyKeys(item, ["type", "id", "role", "tools"]);
|
|
303
|
+
openAiOptional(item.id, openAiId);
|
|
304
|
+
if (item.role !== undefined && item.role !== "developer")
|
|
305
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
306
|
+
validateOpenAiTools(item.tools, toolCount);
|
|
307
|
+
return;
|
|
308
|
+
default:
|
|
309
|
+
throw new CodingProxyError(400, openAiCode("openai_input_not_allowed", item.type));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function validateOpenAiReasoning(value) {
|
|
313
|
+
const reasoning = openAiRecord(value, "openai_reasoning_not_allowed");
|
|
314
|
+
openAiOnlyKeys(reasoning, ["effort", "summary", "context"], "openai_reasoning_not_allowed");
|
|
315
|
+
for (const [key, allowed] of [
|
|
316
|
+
["effort", OPENAI_REASONING_EFFORTS],
|
|
317
|
+
["summary", OPENAI_REASONING_SUMMARIES],
|
|
318
|
+
["context", OPENAI_REASONING_CONTEXTS],
|
|
319
|
+
]) {
|
|
320
|
+
const setting = reasoning[key];
|
|
321
|
+
if (setting !== undefined && (typeof setting !== "string" || !allowed.has(setting))) {
|
|
322
|
+
throw new CodingProxyError(400, "openai_reasoning_not_allowed");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function validateOpenAiText(value) {
|
|
327
|
+
const text = openAiRecord(value, "openai_text_not_allowed");
|
|
328
|
+
openAiOnlyKeys(text, ["verbosity", "format"], "openai_text_not_allowed");
|
|
329
|
+
if (text.verbosity !== undefined && (typeof text.verbosity !== "string" || !OPENAI_VERBOSITIES.has(text.verbosity))) {
|
|
330
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
331
|
+
}
|
|
332
|
+
if (text.format === undefined)
|
|
333
|
+
return;
|
|
334
|
+
const format = openAiRecord(text.format, "openai_text_not_allowed");
|
|
335
|
+
if (format.type === "text")
|
|
336
|
+
return openAiOnlyKeys(format, ["type"], "openai_text_not_allowed");
|
|
337
|
+
openAiOnlyKeys(format, ["type", "name", "schema", "strict", "description"], "openai_text_not_allowed");
|
|
338
|
+
if (format.type !== "json_schema" || typeof format.name !== "string" || !OPENAI_TOOL_NAME.test(format.name)) {
|
|
339
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
340
|
+
}
|
|
341
|
+
openAiRecord(format.schema, "openai_text_not_allowed");
|
|
342
|
+
if (format.strict !== undefined && typeof format.strict !== "boolean") {
|
|
343
|
+
throw new CodingProxyError(400, "openai_text_not_allowed");
|
|
344
|
+
}
|
|
345
|
+
openAiOptional(format.description, (value) => openAiString(value, MAX_TOOL_TEXT_BYTES, "openai_text_not_allowed"));
|
|
346
|
+
}
|
|
347
|
+
function validateOpenAiClientMetadata(value) {
|
|
348
|
+
const metadata = openAiRecord(value);
|
|
349
|
+
for (const [key, entry] of Object.entries(metadata)) {
|
|
350
|
+
if (!OPENAI_CLIENT_METADATA_KEYS.has(key)) {
|
|
351
|
+
throw new CodingProxyError(400, openAiCode("openai_client_metadata_key_not_allowed", key));
|
|
352
|
+
}
|
|
353
|
+
openAiString(entry, MAX_OPENAI_METADATA_VALUE_LENGTH);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function validateOpenAiBody(body) {
|
|
357
|
+
for (const key of Object.keys(body)) {
|
|
358
|
+
if (!OPENAI_REQUEST_KEYS.has(key))
|
|
359
|
+
throw new CodingProxyError(400, openAiCode("openai_request_key_not_allowed", key));
|
|
360
|
+
}
|
|
361
|
+
if (body.background !== undefined && body.background !== false) {
|
|
362
|
+
throw new CodingProxyError(400, body.background === true ? "background_not_allowed" : "invalid_openai_request");
|
|
363
|
+
}
|
|
364
|
+
if (body.service_tier !== undefined) {
|
|
365
|
+
if (typeof body.service_tier !== "string" || !OPENAI_SERVICE_TIERS.has(body.service_tier)) {
|
|
366
|
+
throw new CodingProxyError(400, "service_tier_not_allowed");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (body.store !== undefined && typeof body.store !== "boolean") {
|
|
370
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
371
|
+
}
|
|
372
|
+
openAiOptional(body.instructions, (value) => openAiString(value, PROXY_MAX_BODY_BYTES));
|
|
373
|
+
const toolCount = { value: 0 };
|
|
374
|
+
if (body.tools !== undefined)
|
|
375
|
+
validateOpenAiTools(body.tools, toolCount);
|
|
376
|
+
if (body.tool_choice !== undefined) {
|
|
377
|
+
if (typeof body.tool_choice !== "string" || !OPENAI_TOOL_CHOICES.has(body.tool_choice)) {
|
|
378
|
+
throw new CodingProxyError(400, "openai_tool_choice_not_allowed");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (body.parallel_tool_calls !== undefined && typeof body.parallel_tool_calls !== "boolean") {
|
|
382
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
383
|
+
}
|
|
384
|
+
openAiOptional(body.reasoning, validateOpenAiReasoning);
|
|
385
|
+
if (body.include !== undefined) {
|
|
386
|
+
const include = body.include;
|
|
387
|
+
if (!Array.isArray(include) ||
|
|
388
|
+
new Set(include).size !== include.length ||
|
|
389
|
+
include.some((entry) => typeof entry !== "string" || !OPENAI_INCLUDES.has(entry))) {
|
|
390
|
+
throw new CodingProxyError(400, "openai_include_not_allowed");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
openAiOptional(body.prompt_cache_key, (value) => openAiString(value, MAX_OPENAI_ID_LENGTH));
|
|
394
|
+
openAiOptional(body.text, validateOpenAiText);
|
|
395
|
+
openAiOptional(body.client_metadata, validateOpenAiClientMetadata);
|
|
396
|
+
if (typeof body.input === "string")
|
|
397
|
+
return;
|
|
398
|
+
if (!Array.isArray(body.input) || body.input.length > MAX_OPENAI_ITEMS) {
|
|
399
|
+
throw new CodingProxyError(400, "invalid_openai_request");
|
|
400
|
+
}
|
|
401
|
+
for (const item of body.input)
|
|
402
|
+
validateOpenAiInputItem(item, toolCount);
|
|
403
|
+
}
|
|
37
404
|
function parseOpenAiRequest(rawBody) {
|
|
38
405
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
39
406
|
throw new CodingProxyError(413, "payload_too_large");
|
|
@@ -59,8 +426,7 @@ function parseOpenAiRequest(rawBody) {
|
|
|
59
426
|
}
|
|
60
427
|
if (body.stream !== true && body.stream !== false)
|
|
61
428
|
throw new CodingProxyError(400, "stream_required");
|
|
62
|
-
|
|
63
|
-
throw new CodingProxyError(400, "background_not_allowed");
|
|
429
|
+
validateOpenAiBody(body);
|
|
64
430
|
const normalized = { ...body, max_output_tokens: maxOutputTokens, store: false, background: false };
|
|
65
431
|
const encoded = JSON.stringify(normalized);
|
|
66
432
|
if (Buffer.byteLength(encoded) > PROXY_MAX_BODY_BYTES)
|
|
@@ -118,7 +484,7 @@ function validateCommandInput(value) {
|
|
|
118
484
|
(typeof input.timeout_ms !== "number" ||
|
|
119
485
|
!Number.isSafeInteger(input.timeout_ms) ||
|
|
120
486
|
input.timeout_ms < 1_000 ||
|
|
121
|
-
input.timeout_ms >
|
|
487
|
+
input.timeout_ms > MAX_COMMAND_TIMEOUT_MS)) {
|
|
122
488
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
123
489
|
}
|
|
124
490
|
}
|
|
@@ -227,11 +593,36 @@ function parseAnthropicBeta(value) {
|
|
|
227
593
|
const normalized = [...unique].sort();
|
|
228
594
|
return { header: normalized.join(","), values: new Set(normalized) };
|
|
229
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Models whose catalog entry says `thinkingMode: "manual"` take manual extended thinking
|
|
598
|
+
* (`{type: "enabled", budget_tokens}`) instead of adaptive thinking, and no effort level (Claude
|
|
599
|
+
* Haiku 4.5 in the shipped catalog, which returns a 400 for adaptive thinking). The pinned Claude
|
|
600
|
+
* Agent SDK sends it `budget_tokens` = max_tokens - 1 and no `output_config`
|
|
601
|
+
* (fixtures/anthropic-sdk-request-haiku-4-5.json). Every other model stays adaptive-only.
|
|
602
|
+
*/
|
|
603
|
+
/** Anthropic's minimum manual thinking budget. */
|
|
604
|
+
const MIN_THINKING_BUDGET_TOKENS = 1024;
|
|
230
605
|
function requireAnthropicBeta(values, beta) {
|
|
231
606
|
if (!values.has(beta))
|
|
232
607
|
throw new CodingProxyError(400, "anthropic_beta_required");
|
|
233
608
|
}
|
|
234
|
-
|
|
609
|
+
/**
|
|
610
|
+
* An `output_config` (top-level, or on an effort-only system message): only an effort level, and
|
|
611
|
+
* only one the run's catalog entry lists. Budget is still reserved from max_tokens per request, so
|
|
612
|
+
* effort only changes how much of it the model uses. Effort doesn't exist on a manual-thinking
|
|
613
|
+
* model; the SDK never sends it there.
|
|
614
|
+
*/
|
|
615
|
+
function validateEffortConfig(value, { thinkingMode, efforts }, betas) {
|
|
616
|
+
if (thinkingMode === "manual")
|
|
617
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
618
|
+
requireAnthropicBeta(betas, "effort-2025-11-24");
|
|
619
|
+
const output = record(value);
|
|
620
|
+
onlyKeys(output, ["effort"]);
|
|
621
|
+
if (typeof output.effort !== "string" || !efforts.includes(output.effort)) {
|
|
622
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function parseAnthropicRequest(rawBody, betaHeader, shape) {
|
|
235
626
|
const beta = parseAnthropicBeta(betaHeader);
|
|
236
627
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
237
628
|
throw new CodingProxyError(413, "payload_too_large");
|
|
@@ -272,12 +663,22 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
272
663
|
validateTextBlocks(body.system);
|
|
273
664
|
for (const value of body.messages) {
|
|
274
665
|
const message = record(value);
|
|
275
|
-
onlyKeys(message, ["role", "content"]);
|
|
666
|
+
onlyKeys(message, message.role === "system" ? ["role", "content", "output_config"] : ["role", "content"]);
|
|
276
667
|
if (message.role !== "user" && message.role !== "assistant" && message.role !== "system") {
|
|
277
668
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
278
669
|
}
|
|
279
670
|
if (message.role === "system") {
|
|
280
671
|
requireAnthropicBeta(beta.values, "mid-conversation-system-2026-04-07");
|
|
672
|
+
if (message.output_config !== undefined) {
|
|
673
|
+
// An effort-only system message (Claude Opus 5.5's per-turn effort): no content at all, and
|
|
674
|
+
// the same effort rule as the top-level output_config.
|
|
675
|
+
requireAnthropicBeta(beta.values, "per-turn-control-2026-07-01");
|
|
676
|
+
if (!Array.isArray(message.content) || message.content.length !== 0) {
|
|
677
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
678
|
+
}
|
|
679
|
+
validateEffortConfig(message.output_config, shape, beta.values);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
281
682
|
if (typeof message.content === "string") {
|
|
282
683
|
if (Buffer.byteLength(message.content) > MAX_TOOL_TEXT_BYTES) {
|
|
283
684
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
@@ -305,13 +706,27 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
305
706
|
if (typeof metadata.user_id !== "string")
|
|
306
707
|
throw new CodingProxyError(400, "invalid_anthropic_request");
|
|
307
708
|
}
|
|
709
|
+
const manualThinking = shape.thinkingMode === "manual";
|
|
308
710
|
if (body.thinking !== undefined) {
|
|
309
711
|
requireAnthropicBeta(beta.values, "interleaved-thinking-2025-05-14");
|
|
310
712
|
requireAnthropicBeta(beta.values, "thinking-token-count-2026-05-13");
|
|
311
713
|
const thinking = record(body.thinking);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
714
|
+
if (manualThinking) {
|
|
715
|
+
onlyKeys(thinking, ["type", "budget_tokens"]);
|
|
716
|
+
const budget = thinking.budget_tokens;
|
|
717
|
+
if (thinking.type !== "enabled" ||
|
|
718
|
+
typeof budget !== "number" ||
|
|
719
|
+
!Number.isSafeInteger(budget) ||
|
|
720
|
+
budget < MIN_THINKING_BUDGET_TOKENS ||
|
|
721
|
+
budget >= body.max_tokens) {
|
|
722
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
onlyKeys(thinking, ["type"]);
|
|
727
|
+
if (thinking.type !== "adaptive")
|
|
728
|
+
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
729
|
+
}
|
|
315
730
|
}
|
|
316
731
|
if (body.context_management !== undefined) {
|
|
317
732
|
requireAnthropicBeta(beta.values, "context-management-2025-06-27");
|
|
@@ -326,13 +741,8 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
326
741
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
327
742
|
}
|
|
328
743
|
}
|
|
329
|
-
if (body.output_config !== undefined)
|
|
330
|
-
|
|
331
|
-
const output = record(body.output_config);
|
|
332
|
-
onlyKeys(output, ["effort"]);
|
|
333
|
-
if (output.effort !== "high")
|
|
334
|
-
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
335
|
-
}
|
|
744
|
+
if (body.output_config !== undefined)
|
|
745
|
+
validateEffortConfig(body.output_config, shape, beta.values);
|
|
336
746
|
const normalized = { ...body };
|
|
337
747
|
delete normalized.metadata;
|
|
338
748
|
const encoded = JSON.stringify(normalized);
|
|
@@ -348,13 +758,22 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
348
758
|
anthropicBeta: beta.header,
|
|
349
759
|
};
|
|
350
760
|
}
|
|
351
|
-
function parseRequest(protocol, rawBody, anthropicBeta) {
|
|
761
|
+
function parseRequest(protocol, rawBody, anthropicBeta, shape) {
|
|
352
762
|
if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
|
|
353
763
|
throw new CodingProxyError(400, "invalid_anthropic_beta");
|
|
354
764
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
765
|
+
try {
|
|
766
|
+
return protocol === "anthropic-messages"
|
|
767
|
+
? parseAnthropicRequest(rawBody, anthropicBeta, shape)
|
|
768
|
+
: parseOpenAiRequest(rawBody);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
// JSON nested deeply enough (inside a free-form tool schema, say) overflows
|
|
772
|
+
// the stack when re-encoded. It is the client's request, not a proxy fault.
|
|
773
|
+
if (error instanceof RangeError)
|
|
774
|
+
throw new CodingProxyError(400, "request_nesting_too_deep");
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
358
777
|
}
|
|
359
778
|
function safeRequestKey(value, fingerprint) {
|
|
360
779
|
if (value === undefined)
|
|
@@ -414,10 +833,18 @@ export class CodingProxy {
|
|
|
414
833
|
});
|
|
415
834
|
this.now = options.now ?? (() => new Date());
|
|
416
835
|
this.audit = options.audit ?? (() => undefined);
|
|
836
|
+
// The fallback for sessions created before the catalog (no stored terms): the
|
|
837
|
+
// proxy has no catalog store, only the shipped catalog compiled into it.
|
|
417
838
|
this.getPricing =
|
|
418
839
|
options.pricing ??
|
|
419
|
-
((model, protocol) =>
|
|
420
|
-
|
|
840
|
+
((model, protocol) => {
|
|
841
|
+
const entry = shippedCatalog().require(model);
|
|
842
|
+
const expected = protocol === "anthropic-messages" ? "anthropic" : "openai";
|
|
843
|
+
if (entry.provider !== expected)
|
|
844
|
+
throw new Error("unknown_model");
|
|
845
|
+
return entry;
|
|
846
|
+
});
|
|
847
|
+
this.priceVersion = options.pricingVersion ?? `shipped:${SHIPPED_CATALOG_VERSION}`;
|
|
421
848
|
}
|
|
422
849
|
async createSession(input) {
|
|
423
850
|
if (input.protocol !== "openai-responses" && input.protocol !== "anthropic-messages") {
|
|
@@ -427,8 +854,16 @@ export class CodingProxy {
|
|
|
427
854
|
if (models.length < 1 || models.length > 8 || models.some((model) => !model || model.length > 100)) {
|
|
428
855
|
throw new Error("invalid_proxy_model_allowlist");
|
|
429
856
|
}
|
|
430
|
-
|
|
431
|
-
|
|
857
|
+
if (input.terms) {
|
|
858
|
+
const expected = input.protocol === "anthropic-messages" ? "anthropic" : "openai";
|
|
859
|
+
if (models.length !== 1 || input.terms.entry.modelId !== models[0] || input.terms.entry.provider !== expected) {
|
|
860
|
+
throw new Error("invalid_proxy_model_terms");
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
for (const model of models)
|
|
865
|
+
this.getPricing(model, input.protocol);
|
|
866
|
+
}
|
|
432
867
|
if (!Number.isFinite(input.budgetUsd) || input.budgetUsd <= 0)
|
|
433
868
|
throw new Error("invalid_proxy_budget");
|
|
434
869
|
if (input.deadlineAt.getTime() <= this.now().getTime())
|
|
@@ -437,6 +872,7 @@ export class CodingProxy {
|
|
|
437
872
|
throw new Error("invalid_proxy_credential_reference");
|
|
438
873
|
const capability = `rrp_${randomBytes(32).toString("base64url")}`;
|
|
439
874
|
const id = randomUUID();
|
|
875
|
+
const registryTokenHash = capabilityHash(deriveRegistryToken(capability));
|
|
440
876
|
await this.ledger.createSession({
|
|
441
877
|
id,
|
|
442
878
|
runId: input.runId,
|
|
@@ -446,6 +882,8 @@ export class CodingProxy {
|
|
|
446
882
|
allowedModels: models,
|
|
447
883
|
deadlineAt: input.deadlineAt,
|
|
448
884
|
budgetUsd: input.budgetUsd,
|
|
885
|
+
registryTokenHash,
|
|
886
|
+
terms: input.terms,
|
|
449
887
|
});
|
|
450
888
|
this.audit({ type: "session.created", runId: input.runId });
|
|
451
889
|
return {
|
|
@@ -463,27 +901,53 @@ export class CodingProxy {
|
|
|
463
901
|
for (const controller of this.activeRequests.get(sessionId) ?? [])
|
|
464
902
|
controller.abort();
|
|
465
903
|
}
|
|
904
|
+
/** Whether this session's run was refused a model request for budget. */
|
|
905
|
+
budgetExhausted(sessionId) {
|
|
906
|
+
return this.ledger.budgetExhausted(sessionId);
|
|
907
|
+
}
|
|
908
|
+
/** The code of the first upstream failure relayed for this session's run, if any. */
|
|
909
|
+
upstreamFailure(sessionId) {
|
|
910
|
+
return this.ledger.upstreamFailure(sessionId);
|
|
911
|
+
}
|
|
466
912
|
async execute(input, sink) {
|
|
467
913
|
const session = await this.authenticate(input.bearer);
|
|
468
914
|
if (session.protocol !== input.protocol) {
|
|
469
915
|
this.audit({ type: "request.rejected", runId: session.runId, reason: "protocol_mismatch" });
|
|
470
916
|
throw new CodingProxyError(403, "protocol_mismatch");
|
|
471
917
|
}
|
|
472
|
-
|
|
918
|
+
// A session from before the model catalog has no stored entry: take the shape from the shipped one.
|
|
919
|
+
const entry = session.terms?.entry ?? shippedCatalog().get(session.allowedModels[0]);
|
|
920
|
+
const shape = {
|
|
921
|
+
thinkingMode: entry?.thinkingMode ?? "adaptive",
|
|
922
|
+
efforts: entry?.efforts ?? [],
|
|
923
|
+
};
|
|
924
|
+
let parsed;
|
|
925
|
+
try {
|
|
926
|
+
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape);
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
// The session is already authenticated and no credential has been
|
|
930
|
+
// resolved; record the refusal against the run so a smuggling attempt
|
|
931
|
+
// is attributable, then refuse.
|
|
932
|
+
if (error instanceof CodingProxyError) {
|
|
933
|
+
this.audit({ type: "request.rejected", runId: session.runId, status: error.status, reason: error.code });
|
|
934
|
+
}
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
473
937
|
if (!session.allowedModels.includes(parsed.model)) {
|
|
474
938
|
this.audit({ type: "request.rejected", runId: session.runId, model: parsed.model, reason: "model_not_allowed" });
|
|
475
939
|
throw new CodingProxyError(403, "model_not_allowed");
|
|
476
940
|
}
|
|
477
|
-
let
|
|
941
|
+
let terms;
|
|
478
942
|
try {
|
|
479
|
-
|
|
943
|
+
terms = this.termsFor(session, parsed.model);
|
|
480
944
|
}
|
|
481
945
|
catch {
|
|
482
946
|
this.audit({ type: "request.rejected", runId: session.runId, model: parsed.model, reason: "unknown_model" });
|
|
483
947
|
throw new CodingProxyError(400, "unknown_model");
|
|
484
948
|
}
|
|
485
949
|
const requestKey = safeRequestKey(input.requestKey, parsed.fingerprint);
|
|
486
|
-
const snapshot = pricingSnapshot(
|
|
950
|
+
const snapshot = pricingSnapshot(terms.version, terms.pricing);
|
|
487
951
|
const reservationUsd = estimateReservationUsd(Buffer.byteLength(parsed.encoded), parsed.maxOutputTokens, snapshot);
|
|
488
952
|
const reservation = await this.ledger.reserve({
|
|
489
953
|
id: randomUUID(),
|
|
@@ -558,6 +1022,8 @@ export class CodingProxy {
|
|
|
558
1022
|
const headers = {
|
|
559
1023
|
"content-type": "application/json",
|
|
560
1024
|
accept: parsed.stream ? "text/event-stream" : "application/json",
|
|
1025
|
+
// The pinned fetch never decompresses, and the response is relayed as-is.
|
|
1026
|
+
"accept-encoding": "identity",
|
|
561
1027
|
};
|
|
562
1028
|
if (session.protocol === "anthropic-messages") {
|
|
563
1029
|
headers["x-api-key"] = key;
|
|
@@ -583,7 +1049,10 @@ export class CodingProxy {
|
|
|
583
1049
|
throw new CodingProxyError(502, controller.signal.aborted ? "upstream_aborted" : "upstream_transport_error");
|
|
584
1050
|
}
|
|
585
1051
|
if (!upstream.ok) {
|
|
1052
|
+
// Read while the deadline still bounds the body; the body itself is never relayed.
|
|
1053
|
+
const code = (await rejectedUpstreamErrorCode(upstream)) ?? `http_${upstream.status}`;
|
|
586
1054
|
cleanupActive();
|
|
1055
|
+
await this.recordUpstreamFailure(session, code);
|
|
587
1056
|
await this.ledger.release(request.id, upstream.status);
|
|
588
1057
|
this.audit({
|
|
589
1058
|
type: "request.released",
|
|
@@ -604,7 +1073,7 @@ export class CodingProxy {
|
|
|
604
1073
|
await this.forwardJson(session, request, upstream, sink);
|
|
605
1074
|
}
|
|
606
1075
|
catch (error) {
|
|
607
|
-
await this.markUncertain(session, request, upstream.status,
|
|
1076
|
+
await this.markUncertain(session, request, upstream.status, failureReason(error), responseHeadersOf(upstream));
|
|
608
1077
|
sink.destroy();
|
|
609
1078
|
throw error instanceof CodingProxyError ? error : new CodingProxyError(502, "invalid_upstream_response");
|
|
610
1079
|
}
|
|
@@ -612,6 +1081,21 @@ export class CodingProxy {
|
|
|
612
1081
|
cleanupActive();
|
|
613
1082
|
}
|
|
614
1083
|
}
|
|
1084
|
+
/** A session's model terms: its stored catalog entry, or the fallback for sessions from before the catalog. */
|
|
1085
|
+
termsFor(session, model) {
|
|
1086
|
+
if (session.terms) {
|
|
1087
|
+
return {
|
|
1088
|
+
pricing: session.terms.entry,
|
|
1089
|
+
version: session.terms.version,
|
|
1090
|
+
thinkingMode: session.terms.entry.thinkingMode,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
pricing: this.getPricing(model, session.protocol),
|
|
1095
|
+
version: this.priceVersion,
|
|
1096
|
+
thinkingMode: shippedCatalog().get(model)?.thinkingMode ?? "adaptive",
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
615
1099
|
async authenticate(bearer) {
|
|
616
1100
|
if (!bearer || bearer.length > 100)
|
|
617
1101
|
throw new CodingProxyError(401, "invalid_capability");
|
|
@@ -681,6 +1165,7 @@ export class CodingProxy {
|
|
|
681
1165
|
let buffer = "";
|
|
682
1166
|
let receivedBytes = 0;
|
|
683
1167
|
let completed = false;
|
|
1168
|
+
let failedWithoutUsage = false;
|
|
684
1169
|
const anthropicUsage = session.protocol === "anthropic-messages" ? new AnthropicSseUsageTracker() : undefined;
|
|
685
1170
|
const body = upstream.body;
|
|
686
1171
|
for await (const chunk of body) {
|
|
@@ -699,23 +1184,61 @@ export class CodingProxy {
|
|
|
699
1184
|
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
700
1185
|
continue;
|
|
701
1186
|
}
|
|
702
|
-
if (!terminal.usage)
|
|
703
|
-
|
|
1187
|
+
if (!terminal.usage) {
|
|
1188
|
+
if (!terminal.failure)
|
|
1189
|
+
throw new Error("terminal_usage_missing");
|
|
1190
|
+
// The upstream failed the response and reported no usage. Pass its failure on so the
|
|
1191
|
+
// client sees the real error instead of a cut connection; the reservation stays held.
|
|
1192
|
+
await this.recordUpstreamFailure(session, terminal.failure);
|
|
1193
|
+
if (!completed) {
|
|
1194
|
+
await this.markUncertain(session, request, upstream.status, `upstream_failed:${terminal.failure}`, responseHeadersOf(upstream));
|
|
1195
|
+
}
|
|
1196
|
+
failedWithoutUsage = true;
|
|
1197
|
+
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
704
1200
|
await this.complete(session, request, terminal.usage, upstream.status);
|
|
705
1201
|
completed = true;
|
|
706
1202
|
await safeWrite(sink, Buffer.from(`${frame}\n\n`), connected);
|
|
707
1203
|
}
|
|
708
1204
|
}
|
|
709
1205
|
buffer = (buffer + decoder.decode()).replaceAll("\r\n", "\n");
|
|
710
|
-
if (!completed)
|
|
1206
|
+
if (!completed && !failedWithoutUsage)
|
|
711
1207
|
throw new Error("terminal_usage_missing");
|
|
712
1208
|
if (buffer)
|
|
713
1209
|
await safeWrite(sink, Buffer.from(buffer), connected);
|
|
714
1210
|
if (connected.value)
|
|
715
1211
|
sink.end();
|
|
716
1212
|
}
|
|
717
|
-
|
|
1213
|
+
/** Best effort: the executor reads it to name the failure; it must never break the relay. */
|
|
1214
|
+
async recordUpstreamFailure(session, code) {
|
|
1215
|
+
await this.ledger.recordUpstreamFailure(session.id, code).catch(() => undefined);
|
|
1216
|
+
}
|
|
1217
|
+
async markUncertain(session, request, status, reason, headers = {}) {
|
|
718
1218
|
await this.ledger.markUncertain(request.id, status);
|
|
719
|
-
this.audit({ type: "request.uncertain", runId: session.runId, requestId: request.id, status, reason });
|
|
1219
|
+
this.audit({ type: "request.uncertain", runId: session.runId, requestId: request.id, status, reason, ...headers });
|
|
720
1220
|
}
|
|
721
1221
|
}
|
|
1222
|
+
/** The safe error code in a rejected upstream's JSON body, or undefined when it names none or cannot be read. */
|
|
1223
|
+
async function rejectedUpstreamErrorCode(upstream) {
|
|
1224
|
+
try {
|
|
1225
|
+
const bytes = await readBoundedBody(upstream.body, MAX_UPSTREAM_ERROR_BYTES);
|
|
1226
|
+
const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
1227
|
+
return value && typeof value === "object" ? safeUpstreamErrorCode(value) : undefined;
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/** The fixed code a relay failed with, or a generic one: error messages are not recorded. */
|
|
1234
|
+
function failureReason(error) {
|
|
1235
|
+
const message = error instanceof Error ? error.message : "";
|
|
1236
|
+
return /^[a-z_]{1,64}$/.test(message) ? message : "stream_read_failed";
|
|
1237
|
+
}
|
|
1238
|
+
/** The upstream's content type and encoding, trimmed to a short printable value, for the audit record. */
|
|
1239
|
+
function responseHeadersOf(upstream) {
|
|
1240
|
+
const clean = (value) => value ? value.replace(/[^\x20-\x7e]/g, "").slice(0, 100) || undefined : undefined;
|
|
1241
|
+
const contentType = clean(upstream.headers.get("content-type"));
|
|
1242
|
+
const contentEncoding = clean(upstream.headers.get("content-encoding"));
|
|
1243
|
+
return { ...(contentType ? { contentType } : {}), ...(contentEncoding ? { contentEncoding } : {}) };
|
|
1244
|
+
}
|