@wardby/cli 0.3.0 → 0.4.1
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 +22 -3
- package/README.md +11 -9
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +68 -10
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/protocol.d.ts +17 -1
- package/dist/coding/protocol.js +17 -6
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/config/providers.d.ts +25 -0
- package/dist/config/providers.js +71 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +17 -10
- package/dist/core/budget-groups.js +15 -12
- 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/dispatch.d.ts +42 -2
- package/dist/core/dispatch.js +144 -26
- package/dist/core/engine-native.js +17 -4
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/host-events.d.ts +24 -1
- package/dist/core/host-events.js +341 -1
- package/dist/core/host-status.d.ts +19 -2
- package/dist/core/host-status.js +47 -30
- 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/reconciler.d.ts +8 -4
- package/dist/core/reconciler.js +15 -4
- package/dist/core/review-host-tools.js +10 -3
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +5 -2
- package/dist/core/runner.js +178 -27
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/tool-names.js +3 -0
- package/dist/core/webhooks.d.ts +9 -1
- package/dist/core/webhooks.js +19 -1
- package/dist/env.js +6 -1
- package/dist/generated/prisma/browser.d.ts +66 -0
- package/dist/generated/prisma/client.d.ts +66 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
- package/dist/generated/prisma/enums.d.ts +7 -0
- package/dist/generated/prisma/enums.js +6 -0
- package/dist/generated/prisma/internal/class.d.ts +99 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
- package/dist/generated/prisma/models/Agent.d.ts +389 -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 +1 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.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/Run.d.ts +933 -1
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.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/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +9 -0
- package/dist/help-index.json +355 -16
- package/dist/import/neutral-schema.d.ts +16 -16
- 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 +4 -2
- package/dist/mcp/auth/ownership.d.ts +9 -9
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +18 -3
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
- package/dist/mcp/context.d.ts +3 -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 +4 -2
- package/dist/mcp/host-events/github-ingress.js +4 -13
- 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 +2 -0
- package/dist/mcp/index.js +87 -9
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/tools/agents.js +74 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.js +2 -1
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/trigger.js +33 -5
- package/dist/mcp/transport/streamable-http.d.ts +5 -0
- package/dist/mcp/transport/streamable-http.js +23 -1
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/self-hosted.d.ts +8 -1
- package/dist/providers/auth/self-hosted.js +39 -2
- package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/memory-ledger.js +10 -1
- package/dist/providers/coding-proxy/metering.d.ts +2 -1
- package/dist/providers/coding-proxy/metering.js +13 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
- package/dist/providers/coding-proxy/proxy.d.ts +12 -2
- package/dist/providers/coding-proxy/proxy.js +92 -30
- package/dist/providers/coding-proxy/types.d.ts +18 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/executor/composition.js +9 -1
- package/dist/providers/executor/container.d.ts +30 -2
- package/dist/providers/executor/container.js +98 -17
- package/dist/providers/executor/dbos.d.ts +2 -0
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/routing.d.ts +6 -0
- package/dist/providers/executor/routing.js +5 -0
- package/dist/providers/executor/types.d.ts +12 -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/docker.d.ts +5 -1
- package/dist/providers/jobs/docker.js +61 -33
- package/dist/providers/jobs/kubernetes.d.ts +3 -0
- package/dist/providers/jobs/kubernetes.js +44 -4
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -9
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -9
- 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-provider.d.ts +10 -14
- package/dist/providers/llm/claude-provider.js +11 -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 +18 -17
- package/dist/providers/llm/routing.js +40 -24
- package/dist/providers/review-host/github-events.js +47 -1
- package/dist/providers/review-host/github.js +7 -6
- package/dist/providers/review-host/types.d.ts +25 -0
- package/dist/providers/vcs/git.js +2 -22
- package/dist/providers/vcs/github.d.ts +20 -0
- package/dist/providers/vcs/github.js +28 -2
- package/dist/providers/vcs/types.d.ts +6 -0
- package/dist/quickstart/index.d.ts +8 -0
- package/dist/quickstart/index.js +34 -34
- 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 +5 -0
- package/docs/README.md +10 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/code-review-agents.md +29 -2
- package/docs/coding-agent-setup.md +3 -0
- package/docs/coding-worker-isolation.md +39 -5
- package/docs/getting-started-gke.md +28 -11
- package/docs/getting-started-identity-provider.md +49 -38
- package/docs/getting-started.md +14 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/security-deployment.md +19 -9
- 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 +6 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +22 -0
- package/help/deploy-gke.md +6 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/getting-started.md +1 -0
- package/help/github.md +18 -0
- package/help/identity-and-access.md +8 -3
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/models.md +90 -0
- package/help/operating-agents.md +7 -1
- package/help/troubleshooting/budgets.md +6 -0
- package/package.json +5 -2
- 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 +258 -2
- 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 -14
- package/dist/providers/llm/pricing-anthropic.js +0 -48
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
- package/dist/providers/llm/pricing.d.ts +0 -30
- package/dist/providers/llm/pricing.js +0 -74
|
@@ -9,7 +9,7 @@ function extractCodingTask(req) {
|
|
|
9
9
|
const parsed = CodingTaskOverrideSchema.safeParse(req.body.task);
|
|
10
10
|
return parsed.success ? parsed.data : undefined;
|
|
11
11
|
}
|
|
12
|
-
export async function handleWebhookIngress(webhookId, req, db, executor) {
|
|
12
|
+
export async function handleWebhookIngress(webhookId, req, db, executor, issueTrackers) {
|
|
13
13
|
const secret = extractSecret(req);
|
|
14
14
|
if (!secret) {
|
|
15
15
|
return { status: 401, body: { error: "invalid_secret", error_description: "No webhook secret presented." } };
|
|
@@ -18,11 +18,18 @@ export async function handleWebhookIngress(webhookId, req, db, executor) {
|
|
|
18
18
|
if (req.body.task !== undefined && codingTask === undefined) {
|
|
19
19
|
return { status: 400, body: { error: "invalid_task" } };
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
// `wardbyIssue`, not `issue`: forwarded third-party payloads (GitHub, Jira)
|
|
22
|
+
// carry their own top-level `issue` object, which is not ours to read.
|
|
23
|
+
const result = await resolveWebhookRun(webhookId, secret, db, executor, codingTask, {
|
|
24
|
+
issue: req.body.wardbyIssue,
|
|
25
|
+
issueTrackers,
|
|
26
|
+
});
|
|
22
27
|
if (result.ok) {
|
|
23
28
|
return { status: 202, body: { runId: result.runId } };
|
|
24
29
|
}
|
|
25
30
|
switch (result.reason) {
|
|
31
|
+
case "invalid_issue":
|
|
32
|
+
return { status: 400, body: { error: "invalid_issue", error_description: result.message } };
|
|
26
33
|
case "not_found":
|
|
27
34
|
return { status: 404, body: { error: "not_found" } };
|
|
28
35
|
case "invalid_secret":
|
|
@@ -4,6 +4,13 @@ import { Credentials } from "../../mcp/auth/self-hosted/credentials.js";
|
|
|
4
4
|
import { Sessions } from "../../mcp/auth/self-hosted/session.js";
|
|
5
5
|
import type { AuthorizeParams, RegisterClientParams, SelfHostedAuthConfig, TokenParams, TokenResult } from "./authorization-server.js";
|
|
6
6
|
export type { AuthorizeParams, RegisterClientParams, SelfHostedAuthConfig, TokenParams, TokenResult, } from "./authorization-server.js";
|
|
7
|
+
/**
|
|
8
|
+
* Whether `requested` is one of a client's registered redirect URIs. A native
|
|
9
|
+
* app's loopback redirect may differ from its registration only in port: it
|
|
10
|
+
* listens on whatever port the OS hands it (RFC 8252 §7.3). Everything else —
|
|
11
|
+
* scheme, host, path, query — must match exactly, with no userinfo or fragment.
|
|
12
|
+
*/
|
|
13
|
+
export declare function redirectMatches(registered: readonly string[], requested: string): boolean;
|
|
7
14
|
export declare class SelfHostedAuthProvider implements AuthProvider {
|
|
8
15
|
readonly db: PrismaClient;
|
|
9
16
|
readonly credentials: Credentials;
|
|
@@ -39,8 +46,8 @@ export declare class SelfHostedAuthProvider implements AuthProvider {
|
|
|
39
46
|
clientId: string;
|
|
40
47
|
};
|
|
41
48
|
} & {
|
|
42
|
-
id: string;
|
|
43
49
|
createdAt: Date;
|
|
50
|
+
id: string;
|
|
44
51
|
state: string | null;
|
|
45
52
|
clientId: string;
|
|
46
53
|
expiresAt: Date;
|
|
@@ -7,13 +7,49 @@ import { SCOPES_SUPPORTED, limitScope } from "../../mcp/auth/resource-server.js"
|
|
|
7
7
|
import { canonicalUrl } from "../../mcp/transport/http-limits.js";
|
|
8
8
|
import { Credentials, DAY, decodeKey, lockUser } from "../../mcp/auth/self-hosted/credentials.js";
|
|
9
9
|
import { Sessions } from "../../mcp/auth/self-hosted/session.js";
|
|
10
|
+
const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
11
|
+
/**
|
|
12
|
+
* Whether `requested` is one of a client's registered redirect URIs. A native
|
|
13
|
+
* app's loopback redirect may differ from its registration only in port: it
|
|
14
|
+
* listens on whatever port the OS hands it (RFC 8252 §7.3). Everything else —
|
|
15
|
+
* scheme, host, path, query — must match exactly, with no userinfo or fragment.
|
|
16
|
+
*/
|
|
17
|
+
export function redirectMatches(registered, requested) {
|
|
18
|
+
if (registered.includes(requested))
|
|
19
|
+
return true;
|
|
20
|
+
let want;
|
|
21
|
+
try {
|
|
22
|
+
want = new URL(requested);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (want.protocol !== "http:" || !LOOPBACK_HOSTS.includes(want.hostname))
|
|
28
|
+
return false;
|
|
29
|
+
return registered.some((value) => {
|
|
30
|
+
try {
|
|
31
|
+
const have = new URL(value);
|
|
32
|
+
return (have.protocol === "http:" &&
|
|
33
|
+
have.hostname === want.hostname &&
|
|
34
|
+
have.pathname === want.pathname &&
|
|
35
|
+
have.search === want.search &&
|
|
36
|
+
!want.username &&
|
|
37
|
+
!want.password &&
|
|
38
|
+
!requested.includes("#") // URL.hash is "" for a bare "#"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
10
46
|
function validRedirect(value) {
|
|
11
47
|
try {
|
|
12
48
|
const u = new URL(value);
|
|
13
49
|
return (!value.includes("#") &&
|
|
14
50
|
!u.username &&
|
|
15
51
|
!u.password &&
|
|
16
|
-
(u.protocol === "https:" || (u.protocol === "http:" &&
|
|
52
|
+
(u.protocol === "https:" || (u.protocol === "http:" && LOOPBACK_HOSTS.includes(u.hostname))));
|
|
17
53
|
}
|
|
18
54
|
catch {
|
|
19
55
|
return false;
|
|
@@ -104,7 +140,8 @@ export class SelfHostedAuthProvider {
|
|
|
104
140
|
throw new Error("Invalid resource.");
|
|
105
141
|
const client = await this.db.oAuthClient.findUnique({ where: { clientId: p.clientId } });
|
|
106
142
|
const metadata = client?.metadata;
|
|
107
|
-
if (!metadata?.redirect_uris
|
|
143
|
+
if (!redirectMatches(metadata?.redirect_uris ?? [], p.redirectUri) ||
|
|
144
|
+
!metadata?.grant_types?.includes("authorization_code"))
|
|
108
145
|
throw new Error("Invalid client or redirect.");
|
|
109
146
|
const scope = limitScope(p.scope, SCOPES_SUPPORTED);
|
|
110
147
|
const request = await this.db.oAuthAuthorizationRequest.create({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CreateProxySessionInput, type ProxyLedger, type ProxyRequest, type ProxySession, type ProxyUsage, type ReserveProxyRequestInput, type ReserveProxyRequestResult } from "./types.js";
|
|
2
2
|
/** Deterministic contract adapter. Sharing one instance simulates a proxy restart. */
|
|
3
3
|
export declare class MemoryProxyLedger implements ProxyLedger {
|
|
4
4
|
private readonly sessions;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeProxyModelTerms, } from "./types.js";
|
|
1
2
|
function cloneSession(session) {
|
|
2
3
|
return {
|
|
3
4
|
...session,
|
|
@@ -5,6 +6,9 @@ function cloneSession(session) {
|
|
|
5
6
|
deadlineAt: new Date(session.deadlineAt),
|
|
6
7
|
budgetExhaustedAt: session.budgetExhaustedAt ? new Date(session.budgetExhaustedAt) : null,
|
|
7
8
|
upstreamFailure: session.upstreamFailure ?? null,
|
|
9
|
+
terms: session.terms
|
|
10
|
+
? { version: session.terms.version, entry: { ...session.terms.entry, efforts: [...session.terms.entry.efforts] } }
|
|
11
|
+
: null,
|
|
8
12
|
};
|
|
9
13
|
}
|
|
10
14
|
function cloneRequest(request) {
|
|
@@ -26,7 +30,12 @@ export class MemoryProxyLedger {
|
|
|
26
30
|
}
|
|
27
31
|
if (this.sessionByCapability.has(input.capabilityHash))
|
|
28
32
|
throw new Error("proxy_capability_collision");
|
|
29
|
-
const session = {
|
|
33
|
+
const session = {
|
|
34
|
+
...input,
|
|
35
|
+
allowedModels: [...input.allowedModels],
|
|
36
|
+
status: "active",
|
|
37
|
+
terms: input.terms ? normalizeProxyModelTerms(input.terms) : null,
|
|
38
|
+
};
|
|
30
39
|
this.sessions.set(session.id, session);
|
|
31
40
|
this.sessionByCapability.set(session.capabilityHash, session.id);
|
|
32
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ModelPricing } from "../llm/pricing.js";
|
|
1
|
+
import { type ModelPricing } from "../llm/pricing-core.js";
|
|
2
2
|
import type { PricingSnapshot, ProxyUsage } from "./types.js";
|
|
3
3
|
export declare function parseAuthoritativeUsage(value: unknown): ProxyUsage;
|
|
4
4
|
export declare function parseAnthropicAuthoritativeUsage(value: unknown): ProxyUsage;
|
|
@@ -21,4 +21,5 @@ export declare class AnthropicSseUsageTracker {
|
|
|
21
21
|
private stopped;
|
|
22
22
|
consume(frame: string): TerminalUsage;
|
|
23
23
|
}
|
|
24
|
+
/** The rates a request is billed at, tagged with their version. Picks only the price fields: a catalog entry's routing/shape fields stay out of the ledger. */
|
|
24
25
|
export declare function pricingSnapshot(version: string, pricing: ModelPricing): PricingSnapshot;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { computeCost } from "../llm/pricing.js";
|
|
2
|
+
import { computeCost } from "../llm/pricing-core.js";
|
|
3
3
|
const MAX_SAFE_TOKENS = 2_000_000_000;
|
|
4
4
|
function tokenCount(value, name) {
|
|
5
5
|
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_SAFE_TOKENS) {
|
|
@@ -182,6 +182,17 @@ export class AnthropicSseUsageTracker {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
/** The rates a request is billed at, tagged with their version. Picks only the price fields: a catalog entry's routing/shape fields stay out of the ledger. */
|
|
185
186
|
export function pricingSnapshot(version, pricing) {
|
|
186
|
-
|
|
187
|
+
const snapshot = {
|
|
188
|
+
version,
|
|
189
|
+
encoding: pricing.encoding,
|
|
190
|
+
inputPerMTok: pricing.inputPerMTok,
|
|
191
|
+
outputPerMTok: pricing.outputPerMTok,
|
|
192
|
+
};
|
|
193
|
+
if (pricing.cachedInputPerMTok !== undefined)
|
|
194
|
+
snapshot.cachedInputPerMTok = pricing.cachedInputPerMTok;
|
|
195
|
+
if (pricing.cacheWritePerMTok !== undefined)
|
|
196
|
+
snapshot.cacheWritePerMTok = pricing.cacheWritePerMTok;
|
|
197
|
+
return snapshot;
|
|
187
198
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { logger } from "../../core/logger.js";
|
|
2
|
+
import { normalizeProxyModelTerms } from "./types.js";
|
|
3
|
+
const log = logger.child({ module: "coding-proxy-ledger" });
|
|
1
4
|
/** How long a ledger transaction waits for a pool connection (Prisma's
|
|
2
5
|
* default is 2 s). A model request fails outright when this runs out, so it
|
|
3
6
|
* gets more room than the default; its pool is also kept apart from the
|
|
@@ -31,8 +34,21 @@ function sessionFromRow(row) {
|
|
|
31
34
|
registryTokenHash: row.registryTokenHash,
|
|
32
35
|
budgetExhaustedAt: row.budgetExhaustedAt ?? null,
|
|
33
36
|
upstreamFailure: row.upstreamFailure ?? null,
|
|
37
|
+
terms: sessionTerms(row),
|
|
34
38
|
};
|
|
35
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The session's stored model terms. Null only when both columns are null (a session from before
|
|
42
|
+
* the catalog, which prices from the shipped fallback); terms that are partly present or cannot be
|
|
43
|
+
* read throw, so the session is refused rather than billed at fallback rates.
|
|
44
|
+
*/
|
|
45
|
+
function sessionTerms(row) {
|
|
46
|
+
const version = row.pricingVersion ?? null;
|
|
47
|
+
const stored = row.catalogEntry ?? null;
|
|
48
|
+
if (version === null && stored === null)
|
|
49
|
+
return null;
|
|
50
|
+
return normalizeProxyModelTerms({ version, entry: stored });
|
|
51
|
+
}
|
|
36
52
|
function requestFromRow(row) {
|
|
37
53
|
const pricing = row.pricingSnapshot;
|
|
38
54
|
const hasUsage = row.inputTokens !== null && row.outputTokens !== null;
|
|
@@ -81,19 +97,22 @@ export class PrismaProxyLedger {
|
|
|
81
97
|
}
|
|
82
98
|
async createSession(input) {
|
|
83
99
|
const models = JSON.stringify(input.allowedModels);
|
|
100
|
+
const terms = input.terms ? normalizeProxyModelTerms(input.terms) : undefined;
|
|
84
101
|
await this.db.$executeRaw `
|
|
85
102
|
INSERT INTO "CodingProxySession"
|
|
86
103
|
("id", "runId", "capabilityHash", "credentialRef", "protocol", "allowedModels", "deadlineAt",
|
|
87
|
-
"budgetUsd", "status", "registryTokenHash", "createdAt", "updatedAt")
|
|
104
|
+
"budgetUsd", "status", "registryTokenHash", "pricingVersion", "catalogEntry", "createdAt", "updatedAt")
|
|
88
105
|
VALUES
|
|
89
106
|
(${input.id}, ${input.runId}, ${input.capabilityHash}, ${input.credentialRef}, ${input.protocol}, ${models}::jsonb,
|
|
90
|
-
${input.deadlineAt}, ${input.budgetUsd}, 'active', ${input.registryTokenHash},
|
|
107
|
+
${input.deadlineAt}, ${input.budgetUsd}, 'active', ${input.registryTokenHash},
|
|
108
|
+
${terms?.version ?? null}, ${terms ? JSON.stringify(terms.entry) : null}::jsonb,
|
|
109
|
+
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
91
110
|
`;
|
|
92
111
|
}
|
|
93
112
|
async findSessionByCapabilityHash(capabilityHash) {
|
|
94
113
|
const rows = await this.db.$queryRaw `
|
|
95
114
|
SELECT "id", "runId", "capabilityHash", "credentialRef", "protocol", "allowedModels", "deadlineAt", "budgetUsd", "status", "registryTokenHash",
|
|
96
|
-
"budgetExhaustedAt", "upstreamFailure"
|
|
115
|
+
"budgetExhaustedAt", "upstreamFailure", "pricingVersion", "catalogEntry"
|
|
97
116
|
FROM "CodingProxySession" WHERE "capabilityHash" = ${capabilityHash}
|
|
98
117
|
`;
|
|
99
118
|
return rows[0] ? sessionFromRow(rows[0]) : null;
|
|
@@ -194,20 +213,54 @@ export class PrismaProxyLedger {
|
|
|
194
213
|
"completedAt" = CURRENT_TIMESTAMP, "updatedAt" = CURRENT_TIMESTAMP
|
|
195
214
|
WHERE "id" = ${requestId} AND "status" IN ('reserved', 'uncertain')
|
|
196
215
|
`;
|
|
197
|
-
await tx.$
|
|
216
|
+
const [updatedRun] = await tx.$queryRaw `
|
|
198
217
|
UPDATE "Run" AS r
|
|
199
|
-
SET "tokensIn" = totals."tokensIn", "tokensOut" = totals."tokensOut", "costUsd" = totals."costUsd"
|
|
218
|
+
SET "tokensIn" = totals."tokensIn", "tokensOut" = totals."tokensOut", "costUsd" = totals."costUsd",
|
|
219
|
+
"turns" = totals."turns"
|
|
200
220
|
FROM (
|
|
201
221
|
SELECT s."runId", COALESCE(SUM(q."inputTokens"), 0)::integer AS "tokensIn",
|
|
202
222
|
COALESCE(SUM(q."outputTokens"), 0)::integer AS "tokensOut",
|
|
203
|
-
COALESCE(SUM(q."actualCostUsd"), 0) AS "costUsd"
|
|
223
|
+
COALESCE(SUM(q."actualCostUsd"), 0) AS "costUsd",
|
|
224
|
+
COUNT(*)::integer AS "turns"
|
|
204
225
|
FROM "CodingProxySession" s
|
|
205
226
|
JOIN "CodingProxyRequest" q ON q."sessionId" = s."id" AND q."status" = 'completed'
|
|
206
227
|
WHERE s."id" = ${prior.sessionId}
|
|
207
228
|
GROUP BY s."runId"
|
|
208
229
|
) AS totals
|
|
209
230
|
WHERE r."id" = totals."runId"
|
|
231
|
+
RETURNING r."id"
|
|
210
232
|
`;
|
|
233
|
+
// Per-model usage (RunModelUsage), recomputed for this request's model and set, never incremented.
|
|
234
|
+
// Best effort inside a savepoint: Run totals and the ledger are the budget truth, so a failed
|
|
235
|
+
// write here (e.g. a role without the RunModelUsage grant) must never fail the completion.
|
|
236
|
+
await tx.$executeRaw `SAVEPOINT run_model_usage`;
|
|
237
|
+
try {
|
|
238
|
+
await tx.$executeRaw `
|
|
239
|
+
INSERT INTO "RunModelUsage"
|
|
240
|
+
("runId", "model", "freshInputTokens", "cachedInputTokens", "cacheWriteTokens", "outputTokens", "costUsd")
|
|
241
|
+
SELECT s."runId", q."model",
|
|
242
|
+
COALESCE(SUM(COALESCE(q."inputTokens", 0) - COALESCE(q."cachedInputTokens", 0)), 0)::integer,
|
|
243
|
+
COALESCE(SUM(q."cachedInputTokens"), 0)::integer,
|
|
244
|
+
COALESCE(SUM(q."cacheWriteTokens"), 0)::integer,
|
|
245
|
+
COALESCE(SUM(q."outputTokens"), 0)::integer,
|
|
246
|
+
COALESCE(SUM(q."actualCostUsd"), 0)
|
|
247
|
+
FROM "CodingProxySession" s
|
|
248
|
+
JOIN "CodingProxyRequest" q ON q."sessionId" = s."id" AND q."status" = 'completed'
|
|
249
|
+
WHERE s."id" = ${prior.sessionId} AND q."model" = ${prior.model}
|
|
250
|
+
GROUP BY s."runId", q."model"
|
|
251
|
+
ON CONFLICT ("runId", "model") DO UPDATE SET
|
|
252
|
+
"freshInputTokens" = EXCLUDED."freshInputTokens",
|
|
253
|
+
"cachedInputTokens" = EXCLUDED."cachedInputTokens",
|
|
254
|
+
"cacheWriteTokens" = EXCLUDED."cacheWriteTokens",
|
|
255
|
+
"outputTokens" = EXCLUDED."outputTokens",
|
|
256
|
+
"costUsd" = EXCLUDED."costUsd"
|
|
257
|
+
`;
|
|
258
|
+
await tx.$executeRaw `RELEASE SAVEPOINT run_model_usage`;
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
await tx.$executeRaw `ROLLBACK TO SAVEPOINT run_model_usage`;
|
|
262
|
+
log.warn({ err, runId: updatedRun?.id, sessionId: prior.sessionId, model: prior.model }, "could not record the run's model usage");
|
|
263
|
+
}
|
|
211
264
|
const request = await requestById(tx, requestId);
|
|
212
265
|
if (!request)
|
|
213
266
|
throw new Error("proxy_completion_not_persisted");
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { CredentialResolver, ProxyAuditSink, ProxyLedger, ProxyProtocol } from "./types.js";
|
|
1
|
+
import type { ModelPricing } from "../llm/pricing-core.js";
|
|
2
|
+
import type { CredentialResolver, ProxyAuditSink, ProxyLedger, ProxyModelTerms, ProxyProtocol } from "./types.js";
|
|
3
3
|
export declare const PROXY_MAX_BODY_BYTES: number;
|
|
4
4
|
export declare const PROXY_MAX_OUTPUT_TOKENS = 1000000;
|
|
5
5
|
export declare const PROXY_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
6
6
|
export declare const CLAUDE_CODE_ANTHROPIC_BETAS: readonly ["claude-code-20250219", "context-management-2025-06-27", "effort-2025-11-24", "interleaved-thinking-2025-05-14", "mid-conversation-system-2026-04-07", "prompt-caching-scope-2026-01-05", "thinking-token-count-2026-05-13"];
|
|
7
|
+
/**
|
|
8
|
+
* Betas Claude Code sends only for some models. per-turn-control is sent for Claude Opus 5.5; it is
|
|
9
|
+
* required only by an effort-only system message (per-turn effort), and every body field is still
|
|
10
|
+
* checked against the reviewed shape, so the header alone unlocks nothing.
|
|
11
|
+
*/
|
|
12
|
+
export declare const OPTIONAL_ANTHROPIC_BETAS: readonly ["per-turn-control-2026-07-01"];
|
|
7
13
|
/**
|
|
8
14
|
* The longest run_command timeout the Claude tool runner offers the model (its MAX_TIMEOUT_MS,
|
|
9
15
|
* src/claude-tool-runner/command.mjs; the proxy tests keep the two equal). A lower limit here let the
|
|
@@ -35,6 +41,8 @@ export interface CreateCodingProxySession {
|
|
|
35
41
|
allowedModels: string[];
|
|
36
42
|
deadlineAt: Date;
|
|
37
43
|
budgetUsd: number;
|
|
44
|
+
/** The run's catalog entry recorded at dispatch; the session then prices and shapes requests from it. */
|
|
45
|
+
terms?: ProxyModelTerms;
|
|
38
46
|
}
|
|
39
47
|
export interface CreatedCodingProxySession {
|
|
40
48
|
id: string;
|
|
@@ -76,6 +84,8 @@ export declare class CodingProxy {
|
|
|
76
84
|
/** The code of the first upstream failure relayed for this session's run, if any. */
|
|
77
85
|
upstreamFailure(sessionId: string): Promise<string | null>;
|
|
78
86
|
execute(input: ExecuteProxyRequest, sink: ProxyResponseSink): Promise<void>;
|
|
87
|
+
/** A session's model terms: its stored catalog entry, or the fallback for sessions from before the catalog. */
|
|
88
|
+
private termsFor;
|
|
79
89
|
private authenticate;
|
|
80
90
|
private trackActive;
|
|
81
91
|
private handleDuplicate;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { shippedCatalog } from "../llm/catalog.js";
|
|
3
|
+
import { SHIPPED_CATALOG_VERSION } from "../llm/catalog-shipped.js";
|
|
4
4
|
import { deriveRegistryToken } from "../../coding/registry/token.js";
|
|
5
5
|
import { AnthropicSseUsageTracker, actualCostUsd, estimateReservationUsd, fingerprintRequest, parseAnthropicAuthoritativeUsage, parseAuthoritativeUsage, pricingSnapshot, safeUpstreamErrorCode, terminalUsageFromSseFrame, } from "./metering.js";
|
|
6
6
|
import { createPinnedProxyFetch } from "./secure-fetch.js";
|
|
@@ -20,7 +20,13 @@ const MAX_UPSTREAM_JSON_BYTES = 16 * 1024 * 1024;
|
|
|
20
20
|
/** A rejected upstream's error body is read only this far, for its error code. */
|
|
21
21
|
const MAX_UPSTREAM_ERROR_BYTES = 64 * 1024;
|
|
22
22
|
const REQUEST_KEY_PATTERN = /^[\x21-\x7e]{1,200}$/;
|
|
23
|
-
|
|
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]);
|
|
24
30
|
const WARDBY_COMMAND_TOOL = "mcp__wardby_tools__run_command";
|
|
25
31
|
const STRUCTURED_OUTPUT_TOOL = "StructuredOutput";
|
|
26
32
|
const MAX_COMMAND_BYTES = 16 * 1024;
|
|
@@ -588,19 +594,35 @@ function parseAnthropicBeta(value) {
|
|
|
588
594
|
return { header: normalized.join(","), values: new Set(normalized) };
|
|
589
595
|
}
|
|
590
596
|
/**
|
|
591
|
-
* Models
|
|
592
|
-
* thinking, and no effort level
|
|
593
|
-
*
|
|
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`
|
|
594
601
|
* (fixtures/anthropic-sdk-request-haiku-4-5.json). Every other model stays adaptive-only.
|
|
595
602
|
*/
|
|
596
|
-
const MANUAL_THINKING_MODELS = new Set(["claude-haiku-4-5"]);
|
|
597
603
|
/** Anthropic's minimum manual thinking budget. */
|
|
598
604
|
const MIN_THINKING_BUDGET_TOKENS = 1024;
|
|
599
605
|
function requireAnthropicBeta(values, beta) {
|
|
600
606
|
if (!values.has(beta))
|
|
601
607
|
throw new CodingProxyError(400, "anthropic_beta_required");
|
|
602
608
|
}
|
|
603
|
-
|
|
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) {
|
|
604
626
|
const beta = parseAnthropicBeta(betaHeader);
|
|
605
627
|
if (Buffer.byteLength(rawBody) > PROXY_MAX_BODY_BYTES)
|
|
606
628
|
throw new CodingProxyError(413, "payload_too_large");
|
|
@@ -641,12 +663,22 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
641
663
|
validateTextBlocks(body.system);
|
|
642
664
|
for (const value of body.messages) {
|
|
643
665
|
const message = record(value);
|
|
644
|
-
onlyKeys(message, ["role", "content"]);
|
|
666
|
+
onlyKeys(message, message.role === "system" ? ["role", "content", "output_config"] : ["role", "content"]);
|
|
645
667
|
if (message.role !== "user" && message.role !== "assistant" && message.role !== "system") {
|
|
646
668
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
647
669
|
}
|
|
648
670
|
if (message.role === "system") {
|
|
649
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
|
+
}
|
|
650
682
|
if (typeof message.content === "string") {
|
|
651
683
|
if (Buffer.byteLength(message.content) > MAX_TOOL_TEXT_BYTES) {
|
|
652
684
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
@@ -674,7 +706,7 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
674
706
|
if (typeof metadata.user_id !== "string")
|
|
675
707
|
throw new CodingProxyError(400, "invalid_anthropic_request");
|
|
676
708
|
}
|
|
677
|
-
const manualThinking =
|
|
709
|
+
const manualThinking = shape.thinkingMode === "manual";
|
|
678
710
|
if (body.thinking !== undefined) {
|
|
679
711
|
requireAnthropicBeta(beta.values, "interleaved-thinking-2025-05-14");
|
|
680
712
|
requireAnthropicBeta(beta.values, "thinking-token-count-2026-05-13");
|
|
@@ -709,16 +741,8 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
709
741
|
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
710
742
|
}
|
|
711
743
|
}
|
|
712
|
-
if (body.output_config !== undefined)
|
|
713
|
-
|
|
714
|
-
if (manualThinking)
|
|
715
|
-
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
716
|
-
requireAnthropicBeta(beta.values, "effort-2025-11-24");
|
|
717
|
-
const output = record(body.output_config);
|
|
718
|
-
onlyKeys(output, ["effort"]);
|
|
719
|
-
if (output.effort !== "high")
|
|
720
|
-
throw new CodingProxyError(400, "unsupported_anthropic_feature");
|
|
721
|
-
}
|
|
744
|
+
if (body.output_config !== undefined)
|
|
745
|
+
validateEffortConfig(body.output_config, shape, beta.values);
|
|
722
746
|
const normalized = { ...body };
|
|
723
747
|
delete normalized.metadata;
|
|
724
748
|
const encoded = JSON.stringify(normalized);
|
|
@@ -734,13 +758,13 @@ function parseAnthropicRequest(rawBody, betaHeader) {
|
|
|
734
758
|
anthropicBeta: beta.header,
|
|
735
759
|
};
|
|
736
760
|
}
|
|
737
|
-
function parseRequest(protocol, rawBody, anthropicBeta) {
|
|
761
|
+
function parseRequest(protocol, rawBody, anthropicBeta, shape) {
|
|
738
762
|
if (protocol !== "anthropic-messages" && anthropicBeta !== undefined) {
|
|
739
763
|
throw new CodingProxyError(400, "invalid_anthropic_beta");
|
|
740
764
|
}
|
|
741
765
|
try {
|
|
742
766
|
return protocol === "anthropic-messages"
|
|
743
|
-
? parseAnthropicRequest(rawBody, anthropicBeta)
|
|
767
|
+
? parseAnthropicRequest(rawBody, anthropicBeta, shape)
|
|
744
768
|
: parseOpenAiRequest(rawBody);
|
|
745
769
|
}
|
|
746
770
|
catch (error) {
|
|
@@ -809,10 +833,18 @@ export class CodingProxy {
|
|
|
809
833
|
});
|
|
810
834
|
this.now = options.now ?? (() => new Date());
|
|
811
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.
|
|
812
838
|
this.getPricing =
|
|
813
839
|
options.pricing ??
|
|
814
|
-
((model, protocol) =>
|
|
815
|
-
|
|
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}`;
|
|
816
848
|
}
|
|
817
849
|
async createSession(input) {
|
|
818
850
|
if (input.protocol !== "openai-responses" && input.protocol !== "anthropic-messages") {
|
|
@@ -822,8 +854,16 @@ export class CodingProxy {
|
|
|
822
854
|
if (models.length < 1 || models.length > 8 || models.some((model) => !model || model.length > 100)) {
|
|
823
855
|
throw new Error("invalid_proxy_model_allowlist");
|
|
824
856
|
}
|
|
825
|
-
|
|
826
|
-
|
|
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
|
+
}
|
|
827
867
|
if (!Number.isFinite(input.budgetUsd) || input.budgetUsd <= 0)
|
|
828
868
|
throw new Error("invalid_proxy_budget");
|
|
829
869
|
if (input.deadlineAt.getTime() <= this.now().getTime())
|
|
@@ -843,6 +883,7 @@ export class CodingProxy {
|
|
|
843
883
|
deadlineAt: input.deadlineAt,
|
|
844
884
|
budgetUsd: input.budgetUsd,
|
|
845
885
|
registryTokenHash,
|
|
886
|
+
terms: input.terms,
|
|
846
887
|
});
|
|
847
888
|
this.audit({ type: "session.created", runId: input.runId });
|
|
848
889
|
return {
|
|
@@ -874,9 +915,15 @@ export class CodingProxy {
|
|
|
874
915
|
this.audit({ type: "request.rejected", runId: session.runId, reason: "protocol_mismatch" });
|
|
875
916
|
throw new CodingProxyError(403, "protocol_mismatch");
|
|
876
917
|
}
|
|
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
|
+
};
|
|
877
924
|
let parsed;
|
|
878
925
|
try {
|
|
879
|
-
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta);
|
|
926
|
+
parsed = parseRequest(input.protocol, input.rawBody, input.anthropicBeta, shape);
|
|
880
927
|
}
|
|
881
928
|
catch (error) {
|
|
882
929
|
// The session is already authenticated and no credential has been
|
|
@@ -891,16 +938,16 @@ export class CodingProxy {
|
|
|
891
938
|
this.audit({ type: "request.rejected", runId: session.runId, model: parsed.model, reason: "model_not_allowed" });
|
|
892
939
|
throw new CodingProxyError(403, "model_not_allowed");
|
|
893
940
|
}
|
|
894
|
-
let
|
|
941
|
+
let terms;
|
|
895
942
|
try {
|
|
896
|
-
|
|
943
|
+
terms = this.termsFor(session, parsed.model);
|
|
897
944
|
}
|
|
898
945
|
catch {
|
|
899
946
|
this.audit({ type: "request.rejected", runId: session.runId, model: parsed.model, reason: "unknown_model" });
|
|
900
947
|
throw new CodingProxyError(400, "unknown_model");
|
|
901
948
|
}
|
|
902
949
|
const requestKey = safeRequestKey(input.requestKey, parsed.fingerprint);
|
|
903
|
-
const snapshot = pricingSnapshot(
|
|
950
|
+
const snapshot = pricingSnapshot(terms.version, terms.pricing);
|
|
904
951
|
const reservationUsd = estimateReservationUsd(Buffer.byteLength(parsed.encoded), parsed.maxOutputTokens, snapshot);
|
|
905
952
|
const reservation = await this.ledger.reserve({
|
|
906
953
|
id: randomUUID(),
|
|
@@ -1034,6 +1081,21 @@ export class CodingProxy {
|
|
|
1034
1081
|
cleanupActive();
|
|
1035
1082
|
}
|
|
1036
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
|
+
}
|
|
1037
1099
|
async authenticate(bearer) {
|
|
1038
1100
|
if (!bearer || bearer.length > 100)
|
|
1039
1101
|
throw new CodingProxyError(401, "invalid_capability");
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CatalogEntry } from "../llm/catalog-types.js";
|
|
2
|
+
import type { ModelPricing } from "../llm/pricing-core.js";
|
|
2
3
|
export type ProxySessionStatus = "active" | "cancelled";
|
|
3
4
|
export type ProxyRequestStatus = "reserved" | "completed" | "released" | "uncertain";
|
|
4
5
|
export type ProxyProtocol = "openai-responses" | "anthropic-messages";
|
|
5
6
|
export interface PricingSnapshot extends ModelPricing {
|
|
6
7
|
version: string;
|
|
7
8
|
}
|
|
9
|
+
/** The model terms a coding run was dispatched under: its catalog entry and that entry's price version. */
|
|
10
|
+
export interface ProxyModelTerms {
|
|
11
|
+
version: string;
|
|
12
|
+
entry: CatalogEntry;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validates and copies model terms, for both ledgers on write and the database ledger on read.
|
|
16
|
+
* Throws invalid_proxy_session_terms: unreadable terms must refuse the session, never fall back.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeProxyModelTerms(terms: {
|
|
19
|
+
version: unknown;
|
|
20
|
+
entry: unknown;
|
|
21
|
+
}): ProxyModelTerms;
|
|
8
22
|
export interface ProxyUsage {
|
|
9
23
|
inputTokens: number;
|
|
10
24
|
outputTokens: number;
|
|
@@ -27,6 +41,8 @@ export interface ProxySession {
|
|
|
27
41
|
budgetExhaustedAt?: Date | null;
|
|
28
42
|
/** The code of the first upstream failure the proxy relayed for this session. */
|
|
29
43
|
upstreamFailure?: string | null;
|
|
44
|
+
/** The run's catalog entry, copied at session creation; null on sessions from before the catalog. */
|
|
45
|
+
terms?: ProxyModelTerms | null;
|
|
30
46
|
}
|
|
31
47
|
export interface ProxyRequest {
|
|
32
48
|
id: string;
|
|
@@ -51,6 +67,7 @@ export interface CreateProxySessionInput {
|
|
|
51
67
|
deadlineAt: Date;
|
|
52
68
|
budgetUsd: number;
|
|
53
69
|
registryTokenHash: string;
|
|
70
|
+
terms?: ProxyModelTerms;
|
|
54
71
|
}
|
|
55
72
|
export interface ReserveProxyRequestInput {
|
|
56
73
|
id: string;
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { parseStoredEntry } from "../llm/catalog-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validates and copies model terms, for both ledgers on write and the database ledger on read.
|
|
4
|
+
* Throws invalid_proxy_session_terms: unreadable terms must refuse the session, never fall back.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeProxyModelTerms(terms) {
|
|
7
|
+
const entry = parseStoredEntry(terms.entry);
|
|
8
|
+
if (typeof terms.version !== "string" || terms.version.length === 0 || !entry) {
|
|
9
|
+
throw new Error("invalid_proxy_session_terms");
|
|
10
|
+
}
|
|
11
|
+
return { version: terms.version, entry };
|
|
12
|
+
}
|