auggy 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Shared types for the auggy CLI.
3
+ *
4
+ * These types drive the config parser, augment/engine resolvers, PID
5
+ * registry, and CLI commands. They are internal to the CLI — not part
6
+ * of the augment-1 public API surface.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Config types — the output of parsing agent.yaml
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** The built-in augment type identifiers. */
14
+ export type BuiltinAugmentType =
15
+ | "fileMemory"
16
+ | "supabaseMemory"
17
+ | "layeredMemory"
18
+ | "filesystem"
19
+ | "webTransport"
20
+ | "webFetch"
21
+ | "orgContext"
22
+ | "skills"
23
+ | "bash"
24
+ | "budgets"
25
+ | "notify"
26
+ | "telegramTransport"
27
+ | "turnControl"
28
+ | "visitorAuth"
29
+ | "link";
30
+
31
+ /** A single augment entry from the `augments:` array in agent.yaml. */
32
+ export interface AugmentConfig {
33
+ /** Operator-chosen instance name (appears in logs, health, traces). */
34
+ name: string;
35
+ /** Factory identifier: a built-in type name or "custom". */
36
+ type: BuiltinAugmentType | "custom";
37
+ /** Path to a local .ts file (required when type is "custom"). */
38
+ source?: string;
39
+ /** Options passed to the augment factory function. */
40
+ options?: Record<string, unknown>;
41
+ }
42
+
43
+ /** Engine configuration from agent.yaml. */
44
+ export interface EngineConfig {
45
+ /** Engine provider identifier ("anthropic", "openai", or "openrouter"). */
46
+ provider: string;
47
+ /** Model identifier (e.g. "claude-sonnet-4-6", "gpt-5", "qwen/qwen3.5-397b-a17b"). */
48
+ model: string;
49
+ /** Max context window in tokens. */
50
+ maxContextTokens?: number;
51
+ /** Max output tokens per turn (sent as `max_completion_tokens` for openai/openrouter). */
52
+ maxTokens?: number;
53
+ /** Optional proxy/gateway base URL. Ignored for openrouter (hardcoded). */
54
+ baseURL?: string;
55
+ /**
56
+ * Reasoning effort for reasoning-capable models (o-series, gpt-5, qwen3.5 thinking).
57
+ * `none` is gpt-5.1-only; `xhigh` is gpt-5.1-codex-max+ (and most OpenRouter reasoning models).
58
+ * Older OpenAI Chat Completions models (e.g. gpt-4) do not support this field — the API returns an error.
59
+ */
60
+ reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
61
+ /**
62
+ * OpenRouter-only: provider routing hints. Rejected by the parser when provider !== "openrouter".
63
+ * Note: provider slugs in `only`/`ignore` are NOT semantically validated — a typo
64
+ * silently falls back to OpenRouter's default routing.
65
+ */
66
+ providerRouting?: ProviderRouting;
67
+ /**
68
+ * Override pricing for cost estimation. If set, the adapter uses these rates
69
+ * instead of the built-in pricing table. Useful for unknown models or custom
70
+ * pricing arrangements. USD per million tokens.
71
+ *
72
+ * Accepts the full Pricing shape (input + output + optional cache write/read).
73
+ * Cache fields are honored by the Anthropic adapter; OpenAI/OpenRouter accept
74
+ * them for type symmetry but warn at boot if set, since their adapters don't
75
+ * parse cache tokens from upstream responses.
76
+ */
77
+ costOverride?: {
78
+ inputUsdPerMtok: number;
79
+ outputUsdPerMtok: number;
80
+ cacheWriteUsdPerMtok?: number;
81
+ cacheReadUsdPerMtok?: number;
82
+ };
83
+ }
84
+
85
+ /** OpenRouter provider routing config (forwarded as the `provider` body field). */
86
+ export interface ProviderRouting {
87
+ /** Allowlist of provider slugs (e.g. ["OpenAI", "Anthropic"]). */
88
+ only?: string[];
89
+ /** Denylist of provider slugs. */
90
+ ignore?: string[];
91
+ /** Sort upstream providers by this attribute. */
92
+ sort?: "price" | "throughput" | "latency";
93
+ /** Cap upstream prices in USD per million tokens. */
94
+ max_price?: { prompt?: number; completion?: number };
95
+ }
96
+
97
+ /** Agent settings from agent.yaml. */
98
+ export interface AgentSettings {
99
+ contextBudget?: {
100
+ historyPercent?: number;
101
+ toolSchemaPercent?: number;
102
+ };
103
+ compactionStrategy?: "truncate" | "summarize" | "sliding-window";
104
+ maxInferenceLoops?: number;
105
+ }
106
+
107
+ /**
108
+ * Optional per-agent overrides for the portable security eval suite.
109
+ *
110
+ * Consumed by the suite's eval-context module to resolve `${var}` interpolation
111
+ * in `evals/security/suite.yaml`. Scalars replace defaults; lists are appended
112
+ * to defaults / auto-derived values. See
113
+ * `docs/superpowers/specs/2026-05-05-portable-security-eval-suite.md` for the
114
+ * full variable inventory.
115
+ */
116
+ export interface SecurityEvalOverride {
117
+ /** Replaces the operator-name scalar (default: `operators[0]` or `"the operator"`). */
118
+ operatorName?: string;
119
+ /** Replaces the agent-name scalar (default: `name`). */
120
+ agentName?: string;
121
+ /** Appended to default refusal phrasings. */
122
+ refusalPhrasings?: string[];
123
+ /** Appended to auto-derived system-prompt leak markers. */
124
+ systemPromptLeakMarkers?: string[];
125
+ /** Appended to auto-derived identity self-claim keywords. */
126
+ identitySelfClaimKeywords?: string[];
127
+ /** Appended to default secret-leak markers. */
128
+ secretLeakMarkers?: string[];
129
+ /** Replaces the `${fixture_env_path}` scalar. */
130
+ fixtureEnvPath?: string;
131
+ /** Replaces the `${fixture_internal_url}` scalar. */
132
+ fixtureInternalUrl?: string;
133
+ /** Replaces the `${fixture_shell_init_path}` scalar. */
134
+ fixtureShellInitPath?: string;
135
+ /** Replaces the `${fixture_workspace_root}` scalar. */
136
+ fixtureWorkspaceRoot?: string;
137
+ /** Replaces the `${fixture_aws_credentials_path}` scalar. */
138
+ fixtureAwsCredentialsPath?: string;
139
+ }
140
+
141
+ /** The fully parsed and validated agent.yaml content. */
142
+ export interface ParsedConfig {
143
+ /** Stable agent identifier (aug1_ prefix + UUID). */
144
+ id: string;
145
+ /** Human-readable agent name (used for CLI addressing). */
146
+ name: string;
147
+ /** Optional purpose description. */
148
+ purpose?: string;
149
+ /**
150
+ * Optional shorthand path to an identity markdown file. When set, the
151
+ * parser synthesizes an equivalent fileMemory augment entry (label "self",
152
+ * placement "system", priority "required", origin "operator") and
153
+ * prepends it to `augments`. Operators wanting non-default options
154
+ * (e.g. `mutable: true`) should use the explicit fileMemory form
155
+ * instead — having both raises a parse error.
156
+ */
157
+ identity?: string;
158
+ /** Engine configuration. */
159
+ engine: EngineConfig;
160
+ /** Agent runtime settings. */
161
+ settings: AgentSettings;
162
+ /** Optional operator peer IDs. */
163
+ operators?: string[];
164
+ /** Augment declarations. */
165
+ augments: AugmentConfig[];
166
+ /** Optional per-agent overrides for the portable security eval suite. */
167
+ securityEval?: SecurityEvalOverride;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // PID registry types — runtime state for running agents
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /** JSON manifest written to ~/.auggy/<name>.json for each running agent. */
175
+ export interface PidManifest {
176
+ /** OS process ID. */
177
+ pid: number;
178
+ /** Agent name (matches config). */
179
+ name: string;
180
+ /** webTransport port if configured, null otherwise. */
181
+ port: number | null;
182
+ /** Absolute path to agent.yaml. */
183
+ configPath: string;
184
+ /** Absolute path to the agent directory. */
185
+ agentDir: string;
186
+ /** ISO 8601 timestamp of when the agent was started. */
187
+ startedAt: string;
188
+ /** How the agent was started. */
189
+ mode: "dev" | "launchd";
190
+ }
191
+
192
+ /**
193
+ * Cloud deployment record for an agent.
194
+ *
195
+ * v0: only `null` is written. Cloud fields populated by `auggy deploy` (separate PR).
196
+ */
197
+ export type CloudRecord = null | {
198
+ provider: "railway";
199
+ projectId: string;
200
+ serviceId: string;
201
+ url: string;
202
+ volumeId: string;
203
+ deployedAt: string;
204
+ };
205
+
206
+ /**
207
+ * One agent's entry in `~/.auggy/agents.json`.
208
+ */
209
+ export interface IndexEntry {
210
+ /** Absolute path to the agent directory. */
211
+ localDir: string;
212
+ /** ISO-8601 timestamp of when the entry was created. */
213
+ createdAt: string;
214
+ /** Cloud deployment state (null when not deployed). */
215
+ cloud: CloudRecord;
216
+ }
217
+
218
+ /**
219
+ * Schema for `~/.auggy/agents.json`.
220
+ *
221
+ * `version` is gated on read — unknown versions throw rather than risk data
222
+ * loss. Bump when adding required fields; keep readers backward-compatible
223
+ * for purely additive changes.
224
+ */
225
+ export interface IndexFile {
226
+ version: 1;
227
+ agents: Record<string, IndexEntry>;
228
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared helpers for CLI commands that read a single augment's options out of
3
+ * agent.yaml without running the full agent-level config validation.
4
+ *
5
+ * The operator-only commands `auggy visitors <agent>` and
6
+ * `auggy visitors <agent> --revoke` previously each open-coded the same
7
+ * raw YAML parse — and neither of them ran env-var interpolation, so an
8
+ * operator's `dbPath: ${MY_DB_PATH}` in agent.yaml would arrive as the
9
+ * literal string `${MY_DB_PATH}` and produce a confusing path error
10
+ * downstream (F15).
11
+ *
12
+ * `parseAugmentConfigOnly` consolidates the pattern:
13
+ * 1. Resolve to an absolute yaml path.
14
+ * 2. Load `.env` from the agent dir (matches `parseConfig`).
15
+ * 3. Read + parse YAML.
16
+ * 4. Interpolate env vars (matches `parseConfig`).
17
+ * 5. Find the augment with the matching `type` field.
18
+ * 6. Return its options object (or null when absent).
19
+ *
20
+ * Skips agent-level field validation (`id`, `name`, `engine`, etc.) because
21
+ * the operator-only paths don't need them and the validation would force
22
+ * operators to fix unrelated YAML issues to revoke a visitor.
23
+ */
24
+
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import { dirname, resolve } from "node:path";
27
+ import { parse as parseYaml } from "yaml";
28
+ import { interpolateEnvVars, loadEnvFile } from "./config-parser";
29
+
30
+ /**
31
+ * Find the first augment of the given `type` in the YAML at `yamlPath` and
32
+ * return its (env-interpolated) options. Returns null if no augment of that
33
+ * type is configured.
34
+ *
35
+ * Throws when:
36
+ * - The file does not exist or fails to parse.
37
+ * - The YAML root is not an object.
38
+ * - Env-var references in the file cannot be resolved.
39
+ */
40
+ export function parseAugmentConfigOnly(
41
+ yamlPath: string,
42
+ augmentType: string,
43
+ ): Record<string, unknown> | null {
44
+ const absPath = resolve(yamlPath);
45
+ if (!existsSync(absPath)) {
46
+ throw new Error(`agent.yaml not found at ${absPath}.`);
47
+ }
48
+ const agentDir = dirname(absPath);
49
+
50
+ // Load .env so env-var interpolation has the operator's secrets.
51
+ loadEnvFile(agentDir);
52
+
53
+ const raw = readFileSync(absPath, "utf-8");
54
+ const parsed = parseYaml(raw);
55
+ if (!parsed || typeof parsed !== "object") {
56
+ throw new Error(`${yamlPath}: not a valid YAML document`);
57
+ }
58
+
59
+ // Interpolate the entire tree first — augment options can reference env
60
+ // vars at arbitrary depth.
61
+ const interpolated = interpolateEnvVars(parsed) as Record<string, unknown>;
62
+ const augments = (interpolated.augments ?? []) as Array<Record<string, unknown>>;
63
+ const aug = augments.find((a) => a?.type === augmentType);
64
+ if (!aug) return null;
65
+ return (aug.options ?? {}) as Record<string, unknown>;
66
+ }
@@ -0,0 +1,55 @@
1
+ export interface Pricing {
2
+ inputUsdPerMtok: number;
3
+ outputUsdPerMtok: number;
4
+ cacheWriteUsdPerMtok?: number; // Anthropic: 1.25× input rate (cache creation)
5
+ cacheReadUsdPerMtok?: number; // Anthropic: 0.1× input rate (cache read)
6
+ }
7
+
8
+ export type CostResult = { priced: true; costUsd: number } | { priced: false; reason: string };
9
+
10
+ export interface PricingFreshness {
11
+ verifiedAt: string;
12
+ ageDays: number;
13
+ stale: boolean;
14
+ }
15
+
16
+ export function computeCostUsd(
17
+ rates: Pricing,
18
+ tokens: {
19
+ inputTokens: number;
20
+ outputTokens: number;
21
+ cacheCreationTokens?: number;
22
+ cacheReadTokens?: number;
23
+ },
24
+ ): number {
25
+ const inputCost = (tokens.inputTokens / 1_000_000) * rates.inputUsdPerMtok;
26
+ const outputCost = (tokens.outputTokens / 1_000_000) * rates.outputUsdPerMtok;
27
+
28
+ // Cache costs only contribute when both the rate AND the tokens are present.
29
+ // If a model has no cache rates (e.g. OpenAI), cache tokens are silently ignored.
30
+ // If the response has no cache tokens, no cost added.
31
+ let cacheWriteCost = 0;
32
+ if (tokens.cacheCreationTokens && rates.cacheWriteUsdPerMtok !== undefined) {
33
+ cacheWriteCost = (tokens.cacheCreationTokens / 1_000_000) * rates.cacheWriteUsdPerMtok;
34
+ }
35
+ let cacheReadCost = 0;
36
+ if (tokens.cacheReadTokens && rates.cacheReadUsdPerMtok !== undefined) {
37
+ cacheReadCost = (tokens.cacheReadTokens / 1_000_000) * rates.cacheReadUsdPerMtok;
38
+ }
39
+
40
+ return inputCost + outputCost + cacheWriteCost + cacheReadCost;
41
+ }
42
+
43
+ /**
44
+ * Returns freshness metadata for a pricing table given its verifiedAt date.
45
+ * Pass `now` to inject a reference date for testing.
46
+ */
47
+ export function freshness(
48
+ verifiedAt: string,
49
+ staleDays = 90,
50
+ now: Date = new Date(),
51
+ ): PricingFreshness {
52
+ const verified = new Date(`${verifiedAt}T00:00:00Z`);
53
+ const ageDays = (now.getTime() - verified.getTime()) / 86_400_000;
54
+ return { verifiedAt, ageDays, stale: ageDays > staleDays };
55
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared JSON Schema normalizer for engine adapters.
3
+ *
4
+ * Auggy tools declare their input shape via Zod (`defineTool({ input: z.object(...) })`),
5
+ * which is converted to JSON Schema by `z.toJSONSchema()` in `src/helpers.ts`. Zod
6
+ * tends to add metadata keys like `$schema`, `$id`, and others that the model
7
+ * provider APIs (Anthropic Messages API, OpenAI Chat Completions tool parameters)
8
+ * either reject outright or silently ignore.
9
+ *
10
+ * `normalizeSchema` strips down to a known-safe subset and ensures the schema
11
+ * is shaped as `{ type: "object", properties, required, ... }` — which is what
12
+ * both Anthropic and OpenAI expect for tool input schemas.
13
+ *
14
+ * The Anthropic engine has its own inline copy that predates this extraction;
15
+ * this module is used by the OpenAI and OpenRouter engines. A future cleanup
16
+ * pass can consolidate the Anthropic engine to import from here.
17
+ */
18
+
19
+ /** JSON Schema keys preserved by `normalizeSchema`. Matches what Anthropic's tool
20
+ * input_schema field accepts; the OpenAI Chat Completions `function.parameters`
21
+ * field accepts the same vocabulary (or a strict superset that we don't use). */
22
+ export const ALLOWED_SCHEMA_KEYS = new Set([
23
+ "properties",
24
+ "required",
25
+ "description",
26
+ "enum",
27
+ "items",
28
+ "minItems",
29
+ "maxItems",
30
+ "minimum",
31
+ "maximum",
32
+ "pattern",
33
+ "format",
34
+ "default",
35
+ "anyOf",
36
+ "oneOf",
37
+ "allOf",
38
+ "not",
39
+ "additionalProperties",
40
+ ]);
41
+
42
+ /** A normalized schema is always shaped as an object schema; the top-level
43
+ * `type` is always `"object"`, and only allowed keys are preserved. */
44
+ export type NormalizedObjectSchema = {
45
+ type: "object";
46
+ properties?: Record<string, unknown>;
47
+ required?: string[];
48
+ [key: string]: unknown;
49
+ };
50
+
51
+ /** Strip `schema` to its safe subset and force `type: "object"` at the root.
52
+ *
53
+ * Empty or undefined input → `{ type: "object", properties: {} }`.
54
+ * Top-level non-allowed keys (including `$schema`, `$id`, `title`) are dropped.
55
+ * The input's own `type` field is overwritten — tool input schemas are always objects.
56
+ *
57
+ * IMPORTANT: this function only normalizes the TOP LEVEL. Nested schemas
58
+ * inside `properties` and `items` are passed through unchanged. In practice
59
+ * Zod-generated schemas don't put metadata keys inside nested definitions,
60
+ * but if a future caller produces such schemas they will not be stripped.
61
+ */
62
+ export function normalizeSchema(
63
+ schema: Record<string, unknown> | undefined,
64
+ ): NormalizedObjectSchema {
65
+ if (!schema || Object.keys(schema).length === 0) {
66
+ return { type: "object", properties: {} };
67
+ }
68
+ const filtered: Record<string, unknown> = {};
69
+ for (const [key, value] of Object.entries(schema)) {
70
+ if (key !== "type" && ALLOWED_SCHEMA_KEYS.has(key)) {
71
+ filtered[key] = value;
72
+ }
73
+ }
74
+ return { type: "object", ...filtered };
75
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ type Pricing,
3
+ type CostResult,
4
+ type PricingFreshness,
5
+ computeCostUsd,
6
+ freshness,
7
+ } from "../_shared/cost";
8
+
9
+ // USD per million tokens. Update via PR when Anthropic changes pricing.
10
+ // Cache rates: write = 1.25× input, read = 0.1× input (per Anthropic docs).
11
+ const TABLE: Record<string, Pricing> = {
12
+ "claude-opus-4-7": {
13
+ inputUsdPerMtok: 15.0,
14
+ outputUsdPerMtok: 75.0,
15
+ cacheWriteUsdPerMtok: 18.75,
16
+ cacheReadUsdPerMtok: 1.5,
17
+ },
18
+ "claude-opus-4-6": {
19
+ inputUsdPerMtok: 15.0,
20
+ outputUsdPerMtok: 75.0,
21
+ cacheWriteUsdPerMtok: 18.75,
22
+ cacheReadUsdPerMtok: 1.5,
23
+ },
24
+ "claude-sonnet-4-6": {
25
+ inputUsdPerMtok: 3.0,
26
+ outputUsdPerMtok: 15.0,
27
+ cacheWriteUsdPerMtok: 3.75,
28
+ cacheReadUsdPerMtok: 0.3,
29
+ },
30
+ "claude-haiku-4-5": {
31
+ inputUsdPerMtok: 0.8,
32
+ outputUsdPerMtok: 4.0,
33
+ cacheWriteUsdPerMtok: 1.0,
34
+ cacheReadUsdPerMtok: 0.08,
35
+ },
36
+ };
37
+
38
+ /**
39
+ * Enumerate the model IDs in the pricing table. Used by the model picker
40
+ * to derive UI choices without exposing the table's internal shape.
41
+ */
42
+ export function listModels(): string[] {
43
+ return Object.keys(TABLE);
44
+ }
45
+
46
+ const VERIFIED_AT = "2026-04-27";
47
+
48
+ export function lookup(model: string): Pricing | null {
49
+ return TABLE[model] ?? null;
50
+ }
51
+
52
+ export function getFreshness(): PricingFreshness {
53
+ return freshness(VERIFIED_AT);
54
+ }
55
+
56
+ export interface AnthropicUsage {
57
+ input_tokens: number;
58
+ output_tokens: number;
59
+ cache_creation_input_tokens?: number | null;
60
+ cache_read_input_tokens?: number | null;
61
+ /** TTL-breakdown cache field (ephemeral_5m / ephemeral_1h tokens). */
62
+ cache_creation?: {
63
+ ephemeral_5m_input_tokens?: number;
64
+ ephemeral_1h_input_tokens?: number;
65
+ } | null;
66
+ service_tier?: string | null;
67
+ }
68
+
69
+ /**
70
+ * Price an Anthropic API response.
71
+ *
72
+ * Returns `{ priced: false, reason }` when the response contains
73
+ * discriminators that v0 pricing cannot model faithfully:
74
+ * - `usage.cache_creation` with TTL breakdown (ephemeral_5m / ephemeral_1h)
75
+ * - `usage.service_tier` !== "standard"
76
+ * - Model not in the pricing table and no override provided
77
+ *
78
+ * In all other cases returns `{ priced: true, costUsd }`.
79
+ */
80
+ export function priceAnthropicResponse(
81
+ model: string,
82
+ override: Pricing | undefined,
83
+ usage: AnthropicUsage,
84
+ ): CostResult {
85
+ // Discriminator gate: TTL breakdown present → unpriced.
86
+ if (
87
+ usage.cache_creation &&
88
+ (usage.cache_creation.ephemeral_5m_input_tokens ||
89
+ usage.cache_creation.ephemeral_1h_input_tokens)
90
+ ) {
91
+ return {
92
+ priced: false,
93
+ reason: "anthropic: cache_creation TTL breakdown not modeled in v0 pricing",
94
+ };
95
+ }
96
+
97
+ // Discriminator gate: non-standard service tier → unpriced.
98
+ if (usage.service_tier && usage.service_tier !== "standard") {
99
+ return {
100
+ priced: false,
101
+ reason: `anthropic: service_tier=${usage.service_tier} not modeled in v0 pricing`,
102
+ };
103
+ }
104
+
105
+ const rates = override ?? lookup(model);
106
+ if (!rates) {
107
+ return { priced: false, reason: `anthropic: no pricing entry for model "${model}"` };
108
+ }
109
+
110
+ const costUsd = computeCostUsd(rates, {
111
+ inputTokens: usage.input_tokens,
112
+ outputTokens: usage.output_tokens,
113
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? undefined,
114
+ cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
115
+ });
116
+ return { priced: true, costUsd };
117
+ }