@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.
Files changed (275) hide show
  1. package/.env.example +22 -3
  2. package/README.md +11 -9
  3. package/dist/cli-help.d.ts +1 -1
  4. package/dist/cli-help.js +1 -0
  5. package/dist/cli.js +68 -10
  6. package/dist/coding/base-commit.d.ts +6 -0
  7. package/dist/coding/base-commit.js +12 -0
  8. package/dist/coding/protocol.d.ts +17 -1
  9. package/dist/coding/protocol.js +17 -6
  10. package/dist/coding/provider.d.ts +6 -1
  11. package/dist/coding/provider.js +16 -9
  12. package/dist/config/providers.d.ts +25 -0
  13. package/dist/config/providers.js +71 -0
  14. package/dist/core/attribution.d.ts +101 -0
  15. package/dist/core/attribution.js +208 -0
  16. package/dist/core/budget-groups.d.ts +17 -10
  17. package/dist/core/budget-groups.js +15 -12
  18. package/dist/core/coding-queue.d.ts +3 -0
  19. package/dist/core/coding-queue.js +6 -2
  20. package/dist/core/coding-service-status.d.ts +11 -0
  21. package/dist/core/coding-service-status.js +17 -0
  22. package/dist/core/cost-report.d.ts +88 -0
  23. package/dist/core/cost-report.js +248 -0
  24. package/dist/core/dispatch.d.ts +42 -2
  25. package/dist/core/dispatch.js +144 -26
  26. package/dist/core/engine-native.js +17 -4
  27. package/dist/core/glob.d.ts +10 -0
  28. package/dist/core/glob.js +33 -0
  29. package/dist/core/host-events.d.ts +24 -1
  30. package/dist/core/host-events.js +341 -1
  31. package/dist/core/host-status.d.ts +19 -2
  32. package/dist/core/host-status.js +47 -30
  33. package/dist/core/issue-bridge.d.ts +60 -0
  34. package/dist/core/issue-bridge.js +189 -0
  35. package/dist/core/issue-dedupe.d.ts +70 -0
  36. package/dist/core/issue-dedupe.js +255 -0
  37. package/dist/core/issue-events.d.ts +42 -0
  38. package/dist/core/issue-events.js +155 -0
  39. package/dist/core/issue-status.d.ts +29 -0
  40. package/dist/core/issue-status.js +241 -0
  41. package/dist/core/issue-tracker-tools.d.ts +64 -0
  42. package/dist/core/issue-tracker-tools.js +850 -0
  43. package/dist/core/model-usage.d.ts +10 -0
  44. package/dist/core/model-usage.js +24 -0
  45. package/dist/core/reconciler.d.ts +8 -4
  46. package/dist/core/reconciler.js +15 -4
  47. package/dist/core/review-host-tools.js +10 -3
  48. package/dist/core/run-pricing.d.ts +61 -0
  49. package/dist/core/run-pricing.js +56 -0
  50. package/dist/core/runner.d.ts +5 -2
  51. package/dist/core/runner.js +178 -27
  52. package/dist/core/scheduler.d.ts +4 -1
  53. package/dist/core/scheduler.js +3 -2
  54. package/dist/core/self-defects.d.ts +80 -0
  55. package/dist/core/self-defects.js +180 -0
  56. package/dist/core/tool-names.js +3 -0
  57. package/dist/core/webhooks.d.ts +9 -1
  58. package/dist/core/webhooks.js +19 -1
  59. package/dist/env.js +6 -1
  60. package/dist/generated/prisma/browser.d.ts +66 -0
  61. package/dist/generated/prisma/client.d.ts +66 -0
  62. package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
  63. package/dist/generated/prisma/enums.d.ts +7 -0
  64. package/dist/generated/prisma/enums.js +6 -0
  65. package/dist/generated/prisma/internal/class.d.ts +99 -0
  66. package/dist/generated/prisma/internal/class.js +4 -4
  67. package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
  68. package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
  69. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
  70. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
  71. package/dist/generated/prisma/models/Agent.d.ts +389 -1
  72. package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
  73. package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
  74. package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
  75. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  76. package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
  77. package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
  78. package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
  79. package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
  80. package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
  81. package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
  82. package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
  83. package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
  84. package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
  85. package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
  86. package/dist/generated/prisma/models/Run.d.ts +933 -1
  87. package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
  88. package/dist/generated/prisma/models/RunAttribution.js +1 -0
  89. package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
  90. package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
  91. package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
  92. package/dist/generated/prisma/models/RunModelUsage.js +1 -0
  93. package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
  94. package/dist/generated/prisma/models/WorkItem.js +1 -0
  95. package/dist/generated/prisma/models.d.ts +9 -0
  96. package/dist/help-index.json +355 -16
  97. package/dist/import/neutral-schema.d.ts +16 -16
  98. package/dist/knowledge/check.d.ts +13 -0
  99. package/dist/knowledge/check.js +69 -0
  100. package/dist/knowledge/cli.d.ts +14 -0
  101. package/dist/knowledge/cli.js +67 -0
  102. package/dist/knowledge/concept.d.ts +54 -0
  103. package/dist/knowledge/concept.js +78 -0
  104. package/dist/knowledge/note.d.ts +11 -0
  105. package/dist/knowledge/note.js +39 -0
  106. package/dist/knowledge/relevance.d.ts +11 -0
  107. package/dist/knowledge/relevance.js +14 -0
  108. package/dist/knowledge/span-hash.d.ts +3 -0
  109. package/dist/knowledge/span-hash.js +16 -0
  110. package/dist/mcp/auth/access.d.ts +4 -2
  111. package/dist/mcp/auth/ownership.d.ts +9 -9
  112. package/dist/mcp/auth/resource-server.d.ts +3 -1
  113. package/dist/mcp/auth/resource-server.js +18 -3
  114. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  115. package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
  116. package/dist/mcp/context.d.ts +3 -0
  117. package/dist/mcp/host-events/deliveries.d.ts +9 -0
  118. package/dist/mcp/host-events/deliveries.js +17 -0
  119. package/dist/mcp/host-events/github-ingress.d.ts +4 -2
  120. package/dist/mcp/host-events/github-ingress.js +4 -13
  121. package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
  122. package/dist/mcp/host-events/jira-ingress.js +92 -0
  123. package/dist/mcp/index.d.ts +2 -0
  124. package/dist/mcp/index.js +87 -9
  125. package/dist/mcp/server.js +5 -2
  126. package/dist/mcp/tools/agents.js +74 -3
  127. package/dist/mcp/tools/cost-report.d.ts +8 -0
  128. package/dist/mcp/tools/cost-report.js +60 -0
  129. package/dist/mcp/tools/issue-projects.d.ts +2 -0
  130. package/dist/mcp/tools/issue-projects.js +238 -0
  131. package/dist/mcp/tools/model-catalog.d.ts +22 -0
  132. package/dist/mcp/tools/model-catalog.js +423 -0
  133. package/dist/mcp/tools/repositories.js +2 -1
  134. package/dist/mcp/tools/tools.d.ts +2 -2
  135. package/dist/mcp/tools/trigger.js +33 -5
  136. package/dist/mcp/transport/streamable-http.d.ts +5 -0
  137. package/dist/mcp/transport/streamable-http.js +23 -1
  138. package/dist/mcp/webhooks/ingress.d.ts +2 -1
  139. package/dist/mcp/webhooks/ingress.js +9 -2
  140. package/dist/providers/auth/self-hosted.d.ts +8 -1
  141. package/dist/providers/auth/self-hosted.js +39 -2
  142. package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
  143. package/dist/providers/coding-proxy/memory-ledger.js +10 -1
  144. package/dist/providers/coding-proxy/metering.d.ts +2 -1
  145. package/dist/providers/coding-proxy/metering.js +13 -2
  146. package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
  147. package/dist/providers/coding-proxy/proxy.d.ts +12 -2
  148. package/dist/providers/coding-proxy/proxy.js +92 -30
  149. package/dist/providers/coding-proxy/types.d.ts +18 -1
  150. package/dist/providers/coding-proxy/types.js +12 -1
  151. package/dist/providers/engine/types.d.ts +19 -0
  152. package/dist/providers/executor/composition.js +9 -1
  153. package/dist/providers/executor/container.d.ts +30 -2
  154. package/dist/providers/executor/container.js +98 -17
  155. package/dist/providers/executor/dbos.d.ts +2 -0
  156. package/dist/providers/executor/dbos.js +7 -5
  157. package/dist/providers/executor/routing.d.ts +6 -0
  158. package/dist/providers/executor/routing.js +5 -0
  159. package/dist/providers/executor/types.d.ts +12 -0
  160. package/dist/providers/issue-tracker/adf.d.ts +31 -0
  161. package/dist/providers/issue-tracker/adf.js +181 -0
  162. package/dist/providers/issue-tracker/index.d.ts +5 -0
  163. package/dist/providers/issue-tracker/index.js +12 -0
  164. package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
  165. package/dist/providers/issue-tracker/jira-client.js +151 -0
  166. package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
  167. package/dist/providers/issue-tracker/jira-events.js +98 -0
  168. package/dist/providers/issue-tracker/jira.d.ts +116 -0
  169. package/dist/providers/issue-tracker/jira.js +502 -0
  170. package/dist/providers/issue-tracker/types.d.ts +269 -0
  171. package/dist/providers/issue-tracker/types.js +16 -0
  172. package/dist/providers/jobs/docker.d.ts +5 -1
  173. package/dist/providers/jobs/docker.js +61 -33
  174. package/dist/providers/jobs/kubernetes.d.ts +3 -0
  175. package/dist/providers/jobs/kubernetes.js +44 -4
  176. package/dist/providers/jobs/service-state.d.ts +22 -0
  177. package/dist/providers/jobs/service-state.js +17 -0
  178. package/dist/providers/llm/anthropic.d.ts +3 -3
  179. package/dist/providers/llm/anthropic.js +3 -9
  180. package/dist/providers/llm/bedrock.d.ts +3 -3
  181. package/dist/providers/llm/bedrock.js +3 -9
  182. package/dist/providers/llm/catalog-lookup.d.ts +10 -0
  183. package/dist/providers/llm/catalog-lookup.js +15 -0
  184. package/dist/providers/llm/catalog-shipped.d.ts +18 -0
  185. package/dist/providers/llm/catalog-shipped.js +197 -0
  186. package/dist/providers/llm/catalog-store.d.ts +58 -0
  187. package/dist/providers/llm/catalog-store.js +138 -0
  188. package/dist/providers/llm/catalog-types.d.ts +66 -0
  189. package/dist/providers/llm/catalog-types.js +64 -0
  190. package/dist/providers/llm/catalog.d.ts +61 -0
  191. package/dist/providers/llm/catalog.js +147 -0
  192. package/dist/providers/llm/claude-provider.d.ts +10 -14
  193. package/dist/providers/llm/claude-provider.js +11 -6
  194. package/dist/providers/llm/index.d.ts +9 -6
  195. package/dist/providers/llm/index.js +8 -5
  196. package/dist/providers/llm/openai.d.ts +14 -5
  197. package/dist/providers/llm/openai.js +24 -14
  198. package/dist/providers/llm/pricing-core.d.ts +5 -3
  199. package/dist/providers/llm/registration.js +8 -12
  200. package/dist/providers/llm/routing.d.ts +18 -17
  201. package/dist/providers/llm/routing.js +40 -24
  202. package/dist/providers/review-host/github-events.js +47 -1
  203. package/dist/providers/review-host/github.js +7 -6
  204. package/dist/providers/review-host/types.d.ts +25 -0
  205. package/dist/providers/vcs/git.js +2 -22
  206. package/dist/providers/vcs/github.d.ts +20 -0
  207. package/dist/providers/vcs/github.js +28 -2
  208. package/dist/providers/vcs/types.d.ts +6 -0
  209. package/dist/quickstart/index.d.ts +8 -0
  210. package/dist/quickstart/index.js +34 -34
  211. package/dist/serve.js +8 -2
  212. package/dist/viewer/api-schema.d.ts +2757 -0
  213. package/dist/viewer/api-schema.js +165 -0
  214. package/dist/viewer/build-schemas.d.ts +2 -0
  215. package/dist/viewer/build-schemas.js +18 -0
  216. package/dist/viewer/event-bus.d.ts +38 -0
  217. package/dist/viewer/event-bus.js +232 -0
  218. package/dist/viewer/graph.d.ts +40 -0
  219. package/dist/viewer/graph.js +243 -0
  220. package/dist/viewer/http.d.ts +30 -0
  221. package/dist/viewer/http.js +133 -0
  222. package/dist/viewer/run-detail.d.ts +4 -0
  223. package/dist/viewer/run-detail.js +61 -0
  224. package/dist/wardby-bin.js +5 -0
  225. package/docs/README.md +10 -0
  226. package/docs/agent-recipes.md +383 -0
  227. package/docs/code-review-agents.md +29 -2
  228. package/docs/coding-agent-setup.md +3 -0
  229. package/docs/coding-worker-isolation.md +39 -5
  230. package/docs/getting-started-gke.md +28 -11
  231. package/docs/getting-started-identity-provider.md +49 -38
  232. package/docs/getting-started.md +14 -0
  233. package/docs/jira-agents.md +649 -0
  234. package/docs/knowledge.md +387 -0
  235. package/docs/models.md +221 -0
  236. package/docs/security-deployment.md +19 -9
  237. package/docs/viewer-api.md +142 -0
  238. package/help/admin-viewer.md +39 -0
  239. package/help/agent-recipes.md +173 -0
  240. package/help/architecture-agent.md +189 -0
  241. package/help/builder-agent.md +80 -0
  242. package/help/code-review-agents.md +6 -0
  243. package/help/cost-attribution.md +67 -0
  244. package/help/creating-agents.md +22 -0
  245. package/help/deploy-gke.md +6 -0
  246. package/help/errors/model-unavailable.md +63 -0
  247. package/help/getting-started.md +1 -0
  248. package/help/github.md +18 -0
  249. package/help/identity-and-access.md +8 -3
  250. package/help/jira.md +135 -0
  251. package/help/knowledge.md +47 -0
  252. package/help/models.md +90 -0
  253. package/help/operating-agents.md +7 -1
  254. package/help/troubleshooting/budgets.md +6 -0
  255. package/package.json +5 -2
  256. package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
  257. package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
  258. package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
  259. package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
  260. package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
  261. package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
  262. package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
  263. package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
  264. package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
  265. package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
  266. package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
  267. package/prisma/schema.prisma +258 -2
  268. package/dist/mcp/tools/models.d.ts +0 -8
  269. package/dist/mcp/tools/models.js +0 -15
  270. package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
  271. package/dist/providers/llm/pricing-anthropic.js +0 -48
  272. package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
  273. package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
  274. package/dist/providers/llm/pricing.d.ts +0 -30
  275. 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
- const result = await resolveWebhookRun(webhookId, secret, db, executor, codingTask);
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:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname))));
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?.includes(p.redirectUri) || !metadata.grant_types?.includes("authorization_code"))
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 { CreateProxySessionInput, ProxyLedger, ProxyRequest, ProxySession, ProxyUsage, ReserveProxyRequestInput, ReserveProxyRequestResult } from "./types.js";
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 = { ...input, allowedModels: [...input.allowedModels], status: "active" };
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
- return { version, ...pricing };
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}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
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.$executeRaw `
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 { type ModelPricing } from "../llm/pricing.js";
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 { getModelPricing, PRICING_VERSION } from "../llm/pricing.js";
3
- import { getAnthropicPricing } from "../llm/pricing-anthropic.js";
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
- const APPROVED_ANTHROPIC_BETAS = new Set(CLAUDE_CODE_ANTHROPIC_BETAS);
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 that take manual extended thinking (`{type: "enabled", budget_tokens}`) instead of adaptive
592
- * thinking, and no effort level: Claude Haiku 4.5, which returns a 400 for adaptive thinking. The
593
- * pinned Claude Agent SDK sends it `budget_tokens` = max_tokens - 1 and no `output_config`
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
- function parseAnthropicRequest(rawBody, betaHeader) {
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 = MANUAL_THINKING_MODELS.has(body.model);
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
- // Effort doesn't exist on a manual-thinking model; the SDK never sends it there.
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) => (protocol === "anthropic-messages" ? getAnthropicPricing(model) : getModelPricing(model)));
815
- this.priceVersion = options.pricingVersion ?? PRICING_VERSION;
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
- for (const model of models)
826
- this.getPricing(model, input.protocol);
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 pricing;
941
+ let terms;
895
942
  try {
896
- pricing = this.getPricing(parsed.model, session.protocol);
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(this.priceVersion, pricing);
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 { ModelPricing } from "../llm/pricing.js";
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
- export {};
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
+ }