melchizedek-agents 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -1
- package/DOCUMENTATION.md +66 -4
- package/README.md +2 -0
- package/config/agents/syndicate.schema.json +69 -0
- package/config/agents/syndicateSchema.yaml +17 -0
- package/db/hardening.sql +3 -1
- package/db/migrations/0016_span_tree.sql +52 -0
- package/db/migrations/0017_cache_pricing.sql +146 -0
- package/db/migrations/0018_queue_view.sql +82 -0
- package/db/telemetry.sql +36 -8
- package/dist/lib/a2a/app.d.ts +26 -4
- package/dist/lib/a2a/app.js +41 -6
- package/dist/lib/a2a/executor.d.ts +28 -1
- package/dist/lib/a2a/executor.js +65 -2
- package/dist/lib/a2a/feedback.d.ts +121 -0
- package/dist/lib/a2a/feedback.js +219 -0
- package/dist/lib/a2a/policy.d.ts +55 -5
- package/dist/lib/a2a/policy.js +56 -12
- package/dist/lib/compile.d.ts +2 -0
- package/dist/lib/compile.js +2 -0
- package/dist/lib/compileNative.js +2 -0
- package/dist/lib/index.d.ts +5 -2
- package/dist/lib/index.js +1 -0
- package/dist/lib/loadSyndicate.d.ts +14 -0
- package/dist/lib/loadSyndicate.js +19 -1
- package/dist/lib/memory/providers.js +7 -3
- package/dist/lib/observability/lineage.d.ts +19 -0
- package/dist/lib/observability/lineage.js +31 -0
- package/dist/lib/observability/metadata.d.ts +69 -0
- package/dist/lib/observability/metadata.js +153 -0
- package/dist/lib/observability/otlpFilter.js +1 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +31 -2
- package/dist/lib/observability/supabaseSpanExporter.js +45 -6
- package/dist/lib/observability/tracer.d.ts +17 -1
- package/dist/lib/observability/tracer.js +61 -3
- package/dist/lib/project.js +1 -1
- package/dist/lib/runtime/native/compaction.js +7 -3
- package/dist/lib/runtime/native/request.d.ts +2 -0
- package/dist/lib/runtime/native/step.js +1 -1
- package/dist/lib/runtime/native/telemetry.d.ts +10 -3
- package/dist/lib/runtime/native/telemetry.js +49 -4
- package/dist/lib/runtime/syndicateTurn.js +5 -1
- package/dist/lib/storage/postgres/index.d.ts +10 -0
- package/dist/lib/storage/postgres/index.js +6 -1
- package/dist/lib/syndicateSchema.d.ts +5 -0
- package/dist/lib/syndicateSchema.js +13 -0
- package/dist/lib/tools/taskTools.d.ts +6 -0
- package/dist/lib/tools/taskTools.js +24 -5
- package/dist/lib/workflow/toolNode.d.ts +1 -0
- package/dist/lib/workflow/toolNode.js +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,137 @@ Consumers of the package read this file; it records changes to the
|
|
|
4
4
|
**published API surface** (the exports map in `package.json`, the bins,
|
|
5
5
|
the starter pack and the templates), not the repo's full history.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 1.10.0 — 2026-10-20
|
|
8
|
+
|
|
9
|
+
A minor release for deployments: a served syndicate takes end-user
|
|
10
|
+
feedback at POST /feedback into the ledger (ADR 0140), budgets report
|
|
11
|
+
their state and can stop on a caller's own rule, and the job queue has a
|
|
12
|
+
read model with a retry.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **End-user feedback, `POST /feedback` and `POST /<agentId>/feedback`
|
|
17
|
+
(ADR 0140).** A product's thumbs or 1–5 rating of a served turn, named by
|
|
18
|
+
its A2A `taskId` or its `traceId`, with an optional `comment` (≤ 4,000
|
|
19
|
+
characters) and `userId`, lands in the ledger as one `adk_labels` row:
|
|
20
|
+
`ref = trace_id = <trace id>`, `labeler = 'end-user'`,
|
|
21
|
+
`fields = { score, userId }`, `note` = the comment through the telemetry
|
|
22
|
+
redactor. Behind the bearer, the authenticator and the task rate limit,
|
|
23
|
+
with a 32 KB body limit, the caller's own turns only, and one label per
|
|
24
|
+
(turn, user) per minute. New option `storage.feedback`; `postgresStorage`
|
|
25
|
+
supplies it when `ledger` is on. New subpath
|
|
26
|
+
`melchizedek-agents/a2a/feedback` and barrel exports: `parseFeedback`,
|
|
27
|
+
`feedbackRow`, `postgresFeedbackStore`, `END_USER_LABELER`, types
|
|
28
|
+
`FeedbackStore`, `FeedbackRow`, `FeedbackScore`, `FeedbackTurn`.
|
|
29
|
+
- **Budget hooks.** `budgets(config, opts)` takes `onBudgetState(state)`,
|
|
30
|
+
called with each subject's day totals, limits and level (`ok`, `warn`,
|
|
31
|
+
`over`) after every task and when a task arrives over a limit; `warnAt`
|
|
32
|
+
(default 0.8); and `hardStop`: `true` (default, as before), `false`
|
|
33
|
+
(limits only report), or a function returning a refusal reason, asked at
|
|
34
|
+
admission (a local server's own spend check). New types `BudgetState`,
|
|
35
|
+
`BudgetLevel`, `BudgetOptions`, `HardStop`.
|
|
36
|
+
- **The job queue's read model.** Migration `0018_queue_view` adds the view
|
|
37
|
+
`melchizedek_task_queue`: one row per background job with `attempts`,
|
|
38
|
+
`retries`, `max_attempts`, `dead_lettered`, `last_error`, the lease and
|
|
39
|
+
the checkpoint. `postgresStorage().retryTask(owner, id)` queues a failed
|
|
40
|
+
or cancelled job again with its attempts reset (`applyRetry` in
|
|
41
|
+
`lib/tools/taskTools.ts`, the rule `task_update`'s `queued` applies).
|
|
42
|
+
The view is revoked from the API roles in the migration and in
|
|
43
|
+
`db/hardening.sql`.
|
|
44
|
+
|
|
45
|
+
## 1.9.0 — 2026-10-20
|
|
46
|
+
|
|
47
|
+
Not published on its own: these changes ship in 1.10.0.
|
|
48
|
+
|
|
49
|
+
A minor release for observability: the ledger keeps a turn's span tree
|
|
50
|
+
with cache tokens and their prices (ADR 0139), and a syndicate's YAML
|
|
51
|
+
and an A2A request can label a turn with metadata, with a hook on every
|
|
52
|
+
task state (ADR 0141).
|
|
53
|
+
|
|
54
|
+
The ledger keeps a turn's span tree, counts prompt-cache tokens, and
|
|
55
|
+
prices them at their own rates (ADR 0139). **Apply the schema before
|
|
56
|
+
upgrading the engine:** `npx melchizedek-db apply --telemetry` runs the
|
|
57
|
+
new migrations 0016 and 0017 and `db/telemetry.sql`; the sinks write the
|
|
58
|
+
new columns, so a ledger without them refuses the rows (they are spooled
|
|
59
|
+
to the dead-letter file, and `telemetry:replay` re-sends them once the
|
|
60
|
+
schema is applied). Rows written before keep NULL in every new column.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **The span tree in `adk_telemetry` (migration `0016_span_tree`).** Every
|
|
65
|
+
span of a turn gets a row: the root, `agent.invoke <name>`, `model.call`,
|
|
66
|
+
`llm.request`, `tool.execute <name>`, `script.invoke` / `script.call`,
|
|
67
|
+
`workflow.invoke` / `node.execute`. New columns `parent_span_id` (the
|
|
68
|
+
nearest ancestor that has a row, read from the tracer's lineage when the
|
|
69
|
+
span starts and recorded as `ledger.parent_span_id` where it differs
|
|
70
|
+
from the direct parent), `kind` (`turn | agent | model | tool | script |
|
|
71
|
+
node`) and `error`; index `idx_adk_telemetry_parent`. A `model.call` row
|
|
72
|
+
carries no tokens and no payload (its `llm.request` rows and
|
|
73
|
+
`adk_payloads` hold them). New exports on `./observability/*`:
|
|
74
|
+
`ledgerSpanKind`, `LedgerSpanKind`, `LEDGER_PARENT_ATTRIBUTE`
|
|
75
|
+
(`lineage`); `isLedgerSpan`, `ledgerParentOf`, `spanFailed`, and the
|
|
76
|
+
new `TelemetryRow` / `TurnRow` fields (`supabaseSpanExporter`).
|
|
77
|
+
- **Tool arguments and results on tool spans.** `tool.args` and
|
|
78
|
+
`tool.result`, JSON, run through the telemetry redactor and then cut at
|
|
79
|
+
8,192 characters (`TOOL_ATTRIBUTE_MAX_CHARS`, with
|
|
80
|
+
`tool.args_truncated` / `tool.result_truncated`), on the loop's tool
|
|
81
|
+
calls and a tool node's. `OTEL_EXPORT_CONTENT=off` drops `tool.result`
|
|
82
|
+
as it drops `tool.args`.
|
|
83
|
+
- **Cache tokens and finish reasons.** `llm.request` records
|
|
84
|
+
`llm.tokens.cache_read` and `llm.tokens.cache_write` (parts of
|
|
85
|
+
`llm.tokens.input`, set when the provider reports them) and
|
|
86
|
+
`llm.finish_reason`; `model.call` records `llm.finish_reason` too.
|
|
87
|
+
`adk_telemetry` and `adk_turns` gain `cache_read_tokens` and
|
|
88
|
+
`cache_write_tokens` (a turn's summed over its calls; the root span's
|
|
89
|
+
`syndicate.tokens.cache_read` / `.cache_write`), and `syndicate.usage`
|
|
90
|
+
carries `cache_read` / `cache_write` per model. `LlmCallMeta.source`
|
|
91
|
+
lets a caller of `traceLlmGeneration` hand the contract response each
|
|
92
|
+
`LlmResponse` came from, which is where a cache write is read.
|
|
93
|
+
- **Cache pricing (migration `0017_cache_pricing`).** `model_pricing`
|
|
94
|
+
gains `cache_read_per_mtok` and `cache_write_per_mtok` (NULL prices the
|
|
95
|
+
token as input, so every existing row prices as before);
|
|
96
|
+
`melchizedek_cost_usd(model, input, output, thinking, cache_read,
|
|
97
|
+
cache_write)` prices them (the four-argument form stays);
|
|
98
|
+
`melchizedek_usage_cost` and the ledger trigger read them. Seeded from
|
|
99
|
+
the vendors' lists, checked 2026-10-20: the Anthropic rows 0015 seeded
|
|
100
|
+
gain their cache rates (5-minute write) while they still hold 0015's
|
|
101
|
+
rates, `claude-opus-5-5` and `claude-sonnet-5-5` are added, and OpenAI's
|
|
102
|
+
`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol` /
|
|
103
|
+
`-terra` / `-luna`, `gpt-5.5`, `gpt-5.4` (under-272K rates), `gpt-5.2`,
|
|
104
|
+
`gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano` get rows. Gemini, Grok and
|
|
105
|
+
Kimi rows get no cache rates.
|
|
106
|
+
- **`metadata:` in the syndicate YAML (ADR 0141).** `{ key: value }` on
|
|
107
|
+
the syndicate and on any agent: strings (at most 256 characters),
|
|
108
|
+
numbers or booleans, at most 32 keys, each key
|
|
109
|
+
`[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}`. The syndicate's become
|
|
110
|
+
`meta.<key>` on every root span of a turn, so on
|
|
111
|
+
`adk_turns.attributes`; an agent's become `meta.agent.<key>` on its
|
|
112
|
+
`agent.invoke` span. A value shaped like a key or token is dropped at
|
|
113
|
+
load with a warning (`LoadSyndicateOptions.onWarning`) naming the key,
|
|
114
|
+
never the value; string values pass through the telemetry redactor. An
|
|
115
|
+
attribute an in-process caller passes in `trace.attributes` wins a key
|
|
116
|
+
both name. `config/agents/syndicate.schema.json` is regenerated.
|
|
117
|
+
- **Per-request metadata over A2A (ADR 0141).** `X-Melch-Meta-<key>`
|
|
118
|
+
headers and the scalar entries of the A2A message's `metadata` object
|
|
119
|
+
become `meta.<key>` on the turn's root span, with the YAML's limits and
|
|
120
|
+
redaction; at most 32 keys a turn, the YAML's first. A header wins a key
|
|
121
|
+
both name; a key the YAML declares, or one starting `agent.`, is never
|
|
122
|
+
taken from a request; a credential-shaped value is dropped with a
|
|
123
|
+
warning. A malformed `X-Melch-Meta-*` header is refused with 400; a
|
|
124
|
+
message entry that is not metadata is skipped. `A2AContext.meta` holds
|
|
125
|
+
the checked headers.
|
|
126
|
+
- **`A2AAppOptions.onTaskState(record)` (ADR 0141).** Called after each
|
|
127
|
+
new state the executor publishes, with
|
|
128
|
+
`{ taskId, contextId, state, syndicate, agentId, traceId?, at }`:
|
|
129
|
+
`submitted`, `working` (with the first progress line), then the state
|
|
130
|
+
the task ends in. Barrel types `TaskStateRecord`, `TaskStateName`,
|
|
131
|
+
`Metadata`, `MetadataValue`; the helpers are in
|
|
132
|
+
`melchizedek-agents/observability/metadata`.
|
|
133
|
+
|
|
134
|
+
### Docs
|
|
135
|
+
|
|
136
|
+
- `tasks/get` and the states a polling caller reads, in DOCUMENTATION.md
|
|
137
|
+
§6 and the A2A wiki doc.
|
|
8
138
|
|
|
9
139
|
## 1.8.0 — 2026-10-18
|
|
10
140
|
|
package/DOCUMENTATION.md
CHANGED
|
@@ -126,6 +126,7 @@ Field reference:
|
|
|
126
126
|
|---|---|---|
|
|
127
127
|
| `syndicate_name` | root | Display name. On the CLI it also namespaces memory user keys; on the A2A server memory is keyed by the caller's silo across every long-term syndicate that server serves. |
|
|
128
128
|
| `memory_system` | root | `internal-only` (nothing persists — on the server too: its transcripts stay in process memory), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
|
|
129
|
+
| `metadata` | root, any agent | Static labels for the ledger ([ADR 0141](./wiki/decisions/0141-metadata-and-the-task-state-hook.md)): `{ key: value }` with string (at most 256 characters), number or boolean values, at most 32 keys, each key `[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}`. The syndicate's become `meta.<key>` on every root span of a turn, so on its `adk_turns.attributes` (`meta.tags: "a,b"` gives the turn tags); an agent's become `meta.agent.<key>` on its `agent.invoke` span. A value shaped like a key or token is dropped at load with a warning naming the key; string values pass through the telemetry redactor. An A2A caller can add keys of its own (§6, Headers) but never set one named here. |
|
|
129
130
|
| `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
|
|
130
131
|
| `memory_extraction_rules` | root | Domain rules appended to the shared fact-extraction prompt for THIS syndicate only (requires `memory_system: "long-term"`). The extraction prompt is global — anything domain-specific belongs here, never edited into it. Unset, the prompt renders byte-identical to before the slot existed (§4). |
|
|
131
132
|
| `memory_extraction_model` | root | The model that distils this syndicate's turns into memory records (long-term only); any model id. Default: the deployment's `MEMORY_EXTRACTION_MODEL`. |
|
|
@@ -192,7 +193,7 @@ function, and every adapter recognises it by marker
|
|
|
192
193
|
| `inspect_image` | Contract | **Blind visual inventory** of a file under `outputs/`: subjects with exact counts, composition, light, palette, medium cues, artifacts — zero quality judgments. Its signature accepts *only* a file path, so an orchestrator cannot leak expectations into the observation (see `image_production.yaml`). |
|
|
193
194
|
| `task_add` / `task_list` / `task_get` / `task_update` | Contract | A to-do list and job queue. Default: a single-user JSON file (`MELCHIZEDEK_TASKS_FILE`, default `outputs/tasks.json`), so every caller of a shared endpoint shares one list. With `DATABASE_URL` (migration 0009) each caller has its own list, scoped by the caller's scope key, and any number of workers take jobs safely. |
|
|
194
195
|
| `ask_user` | Contract, long-running | Asks the person one question (optionally with `options`) and ends the turn `input-required`; the next message on the conversation is the call's result. Any agent but a workflow node's; inside a delegated subagent the question reaches the person with the agent path (§6, Questions). |
|
|
195
|
-
| `task_queue` | Contract | Queues a background job (a self-contained instruction). The tool only writes the queue; `npm run assistant:worker` (`melchizedek-worker`) claims each job, runs it through one agent compiled from YAML (default: the Assistant's Worker), and writes the result back for `task_get`. `--once` drains and exits, for cron. A run is durable (ADR 0113): it is checkpointed beside the job at every step boundary (migration 0014 on Postgres), a job claimed again resumes from its last checkpoint (inside a dispatch route too, ADR 0121), a checkpoint above `MELCHIZEDEK_CHECKPOINT_MAX_BYTES` (default 5 MiB of JSON; `TaskBackendOptions.checkpointMaxBytes`, `postgresStorage({ taskQueue })`) is skipped and the previous one kept, SIGTERM re-queues the job in hand, and `task_update` can cancel a running job. |
|
|
196
|
+
| `task_queue` | Contract | Queues a background job (a self-contained instruction). The tool only writes the queue; `npm run assistant:worker` (`melchizedek-worker`) claims each job, runs it through one agent compiled from YAML (default: the Assistant's Worker), and writes the result back for `task_get`. `--once` drains and exits, for cron. A run is durable (ADR 0113): it is checkpointed beside the job at every step boundary (migration 0014 on Postgres), a job claimed again resumes from its last checkpoint (inside a dispatch route too, ADR 0121), a checkpoint above `MELCHIZEDEK_CHECKPOINT_MAX_BYTES` (default 5 MiB of JSON; `TaskBackendOptions.checkpointMaxBytes`, `postgresStorage({ taskQueue })`) is skipped and the previous one kept, SIGTERM re-queues the job in hand, and `task_update` can cancel a running job. On Postgres the view `melchizedek_task_queue` (migration 0018) lists every job with `attempts`, `retries`, `dead_lettered`, `last_error`, its lease and checkpoint, and `postgresStorage().retryTask(owner, id)` queues a failed or cancelled job again. |
|
|
196
197
|
|
|
197
198
|
**MCP tools** are the exception to the registry: a subagent with
|
|
198
199
|
`mcp_server_url:` (or several servers under `mcp_servers:`) in its YAML
|
|
@@ -626,8 +627,14 @@ row per turn with input, output, the responding agent, the plan-dispatch
|
|
|
626
627
|
route, errors, tokens, model-vs-tool latency, the tool calls with their
|
|
627
628
|
responses, the ids that join it to `adk_sessions` (`session_id`,
|
|
628
629
|
`invocation_id`), provenance (`config_hash`, `engine_version`) and a
|
|
629
|
-
full-text `search` column; `adk_telemetry` — one row per
|
|
630
|
-
|
|
630
|
+
full-text `search` column; `adk_telemetry` — one row per span of the
|
|
631
|
+
turn's tree (the root, `agent.invoke`, `model.call`, `llm.request`,
|
|
632
|
+
`tool.execute`, script and workflow spans), each with `parent_span_id`
|
|
633
|
+
(the nearest ancestor that has a row), `kind` (`turn | agent | model |
|
|
634
|
+
tool | script | node`) and `error` (migration 0016, ADR 0139); a tool
|
|
635
|
+
span carries `tool.args` and `tool.result` (redacted, then cut at 8 KB),
|
|
636
|
+
and tokens sit on the `llm.request` rows and the root, never on a
|
|
637
|
+
`model.call` row; and `adk_payloads` — full prompts and responses per model call,
|
|
631
638
|
kept by policy (`TELEMETRY_PAYLOADS=off|errors|sample|all`,
|
|
632
639
|
`TELEMETRY_PAYLOAD_SAMPLE`, `TELEMETRY_PAYLOAD_TTL_DAYS`) and expired by
|
|
633
640
|
`melchizedek_prune_telemetry()`. A row from a failed call's `llm.request`
|
|
@@ -648,6 +655,14 @@ input summed per call), thinking at the output rate unless the row sets
|
|
|
648
655
|
never a partial sum; `ollama/*` costs zero. Add or change a rate with an
|
|
649
656
|
`INSERT … ON CONFLICT (model) DO UPDATE` on `model_pricing`; rows already
|
|
650
657
|
written keep the rate in force when they were written (ADR 0136).
|
|
658
|
+
Prompt-cache tokens are parts of the input: each `llm.request` records
|
|
659
|
+
`llm.tokens.cache_read` / `llm.tokens.cache_write` (and
|
|
660
|
+
`llm.finish_reason`), `adk_telemetry` and `adk_turns` carry
|
|
661
|
+
`cache_read_tokens` / `cache_write_tokens` (the turn's summed over its
|
|
662
|
+
calls), and `syndicate.usage` carries `cache_read` / `cache_write` per
|
|
663
|
+
model. `model_pricing.cache_read_per_mtok` / `cache_write_per_mtok` price
|
|
664
|
+
them (migration 0017; NULL prices them as input), seeded for Anthropic and
|
|
665
|
+
OpenAI from their price lists (ADR 0139).
|
|
651
666
|
Operate it with `npm run telemetry:stats`,
|
|
652
667
|
`telemetry:prune` and `telemetry:replay` (the exporter spools failed
|
|
653
668
|
batches to `outputs/telemetry-deadletter.ndjson`).
|
|
@@ -700,6 +715,7 @@ README has the commands.
|
|
|
700
715
|
| `GET /<agentId>/.well-known/agent-card.json` | bearer | another syndicate's card; its URLs point at `/<agentId>/a2a/...` |
|
|
701
716
|
| `POST /<agentId>/a2a/jsonrpc`, `/<agentId>/a2a/rest` | bearer | another syndicate (`A2A_SERVED_AGENTS` restricts which) |
|
|
702
717
|
| `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows), ledger rows, A2A tasks and the third-party tokens held for its tools, including conversations whose sessions have expired, with per-store counts; `?all=1` covers every memory namespace |
|
|
718
|
+
| `POST /feedback`, `/<agentId>/feedback` | bearer | an end user's rating of a turn (ADR 0140): `{ "taskId" or "traceId", "score": "up" \| "down" \| 1–5, "comment"?, "userId"? }` → `201 { ok, traceId }`; one `adk_labels` row (below). 501 without the ledger |
|
|
703
719
|
|
|
704
720
|
"bearer" means the credential `A2A_AUTH` asks for (below); with no
|
|
705
721
|
`A2A_SERVER_SECRET` and no authenticator the server binds `127.0.0.1` only.
|
|
@@ -745,6 +761,38 @@ or pass your own `resolveRequest`.
|
|
|
745
761
|
The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
|
|
746
762
|
when Postgres or Supabase is configured, else in process memory. A store
|
|
747
763
|
that cannot be read refuses the task.
|
|
764
|
+
`budgets(config, { onBudgetState, warnAt, hardStop })` adds two hooks.
|
|
765
|
+
`onBudgetState(state)` is called with each subject's standing after every
|
|
766
|
+
task (`cause: 'record'`) and when a task arrives over a limit
|
|
767
|
+
(`cause: 'admit'`, `stopped` saying whether it was refused):
|
|
768
|
+
`{ subject: 'caller:<name>' | 'scope:<hash>', day, usage, limits?,
|
|
769
|
+
level: 'ok' | 'warn' | 'over', reached?, agentId }`, `warn` from `warnAt`
|
|
770
|
+
(default 0.8) of any limit, so a consumer can mirror a deployment's
|
|
771
|
+
spend. `hardStop: false` makes the limits advisory (reported, not
|
|
772
|
+
enforced); a function `(subject) => reason | undefined` is an outside
|
|
773
|
+
stop asked at admission after the limits, whose reason refuses the task
|
|
774
|
+
(a desktop app serving locally passes its monthly spend check, with
|
|
775
|
+
`budgets({}, { hardStop })`). It may throw; that refuses too.
|
|
776
|
+
- **End-user feedback** (`POST /feedback`, `lib/a2a/feedback.ts`, ADR 0140):
|
|
777
|
+
a product's thumbs or 1–5 rating of a turn, named by its A2A `taskId` (a
|
|
778
|
+
dispatch task's answer, not its classifier turn) or its `traceId`, with an
|
|
779
|
+
optional `comment` (at most 4,000 characters) and the product's own
|
|
780
|
+
`userId` ([A-Za-z0-9._@:+/-], at most 128; a credential-shaped one is
|
|
781
|
+
refused). It writes one `adk_labels` row: `ref` and `trace_id` the turn's
|
|
782
|
+
trace id, `labeler = 'end-user'`, `fields = { score, userId }` (`score`
|
|
783
|
+
the string or the number as sent, `userId` null when none), `note` the
|
|
784
|
+
comment through the telemetry redactor (`TELEMETRY_REDACT`), null when
|
|
785
|
+
none. The route is behind the bearer, the authenticator and the task rate
|
|
786
|
+
limit like a task, with its own 32 KB body limit. A caller rates only
|
|
787
|
+
turns whose `user_id` is its scope (or beneath a scope that owns nested
|
|
788
|
+
ones) unless it holds an operator credential; another's turn answers the
|
|
789
|
+
same 404 as a missing one, and a turn reaches the ledger a few seconds
|
|
790
|
+
after it ends. One label per turn per `userId` (else per scope) per
|
|
791
|
+
minute, per process: 429 with `Retry-After`. Answers never repeat what
|
|
792
|
+
was sent. It needs the ledger in Postgres: `postgresStorage({ ledger:
|
|
793
|
+
true })` supplies `storage.feedback`, and `postgresFeedbackStore(pool)`
|
|
794
|
+
builds one on any pool with `db/telemetry.sql` applied; the supabase-js
|
|
795
|
+
path has none.
|
|
748
796
|
- **One record per task** (option `onTaskEnd`): task id, trace id, agent,
|
|
749
797
|
caller, a hash of the scope, status, reason, duration, model calls and
|
|
750
798
|
tokens. The trace id is the one the turn's spans and its ledger row carry,
|
|
@@ -755,6 +803,14 @@ or pass your own `resolveRequest`.
|
|
|
755
803
|
record carries `callerTraceId`. The turn keeps a trace id of its own, since
|
|
756
804
|
ledger attribution and erasure key on it and a caller must not choose it.
|
|
757
805
|
`A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
|
|
806
|
+
- **Every state a task enters** (option `onTaskState`): `{ taskId, contextId,
|
|
807
|
+
state, syndicate, agentId, traceId?, at }` after each new state the server
|
|
808
|
+
publishes: `submitted`, `working` (with the first progress line), then
|
|
809
|
+
`completed`, `failed`, `canceled`, `rejected` or `input-required`. A repeat
|
|
810
|
+
of the state is not reported again; `traceId` is there once the turn's root
|
|
811
|
+
span started; `at` is ms since the epoch; a throwing hook is ignored. For a
|
|
812
|
+
live view of work in flight. A caller polling instead reads the same state
|
|
813
|
+
with `tasks/get` (below).
|
|
758
814
|
- **The audit trail** (`melchizedek_audit`, `db/migrations/0012_audit_log.sql`;
|
|
759
815
|
option `audit`, supplied by `postgresStorage`): one row per failed
|
|
760
816
|
authentication (`auth.failure`), task outcome (`task.end`), erasure
|
|
@@ -825,6 +881,7 @@ sending it regulated data.
|
|
|
825
881
|
| `X-API-Key` | BYOK mode only: the caller's model key (see above) |
|
|
826
882
|
| `X-Provider` | BYOK mode only: which provider `X-API-Key` belongs to (default `google`) |
|
|
827
883
|
| `X-Surface`, `X-Surface-Guild`, `-Channel`, `-User` | optional, telemetry only |
|
|
884
|
+
| `X-Melch-Meta-<key>` | optional, telemetry only: `meta.<key>` (the key lowercased) on the turn's root span, with the limits of the YAML's `metadata:`. A header outside them is refused with 400. The A2A message's own `metadata` object adds its scalar entries the same way (a header wins a key both name; other entries are skipped). At most 32 keys a turn, the YAML's first; a key the YAML declares, or one starting `agent.`, is never taken from a request; a credential-shaped value is dropped ([ADR 0141](./wiki/decisions/0141-metadata-and-the-task-state-hook.md)). |
|
|
828
885
|
|
|
829
886
|
#### Sessions
|
|
830
887
|
|
|
@@ -840,7 +897,12 @@ transcripts in process memory even when Supabase is configured.
|
|
|
840
897
|
(Method names below are 0.3's; 1.0 clients use `SendMessage`,
|
|
841
898
|
`GetTask`, `SendStreamingMessage`, `CancelTask` and the 1.0 enum spellings.)
|
|
842
899
|
`message/send` blocks until the turn finishes unless the request sets
|
|
843
|
-
`configuration.blocking: false`; then poll `tasks/get
|
|
900
|
+
`configuration.blocking: false`; then poll `tasks/get` with the returned
|
|
901
|
+
task id (`{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"id":"<task id>"}}`):
|
|
902
|
+
`result.status.state` is `submitted`, `working`, `input-required` (an
|
|
903
|
+
approval, a question or a consent waits; the status message says which),
|
|
904
|
+
`completed`, `failed`, `canceled` or `rejected`, and once it is final,
|
|
905
|
+
`result.status.message` carries the answer or the reason. `message/stream`
|
|
844
906
|
emits `[STATUS]` progress updates (tool calls, the chosen route, guard
|
|
845
907
|
notes) and the answer as the final status message. With
|
|
846
908
|
`A2A_STREAM_TEXT=true` (`streamText` in code) it also streams the answer as
|
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ console.log(result.status, result.text);
|
|
|
106
106
|
|
|
107
107
|
`runSyndicateTurn` is the same runtime the server, CLI, and eval harness use. It runs every syndicate on the engine's own agent loop and, for a workflow, its own scheduler (the `native` runtime, the only one). `MELCHIZEDEK_RUNTIME=adk` is an error that names 1.0.0; `npx melchizedek-doctor` prints the runtime in use (ADR 0107). To read syndicates from somewhere else, pass `loadSyndicate(file, { agentsDir })` or set `MELCHIZEDEK_AGENTS_DIR`. To serve inside your own Express app, mount `(await createA2AApp({ defaultSyndicate: 'mine.yaml', serverSecret })).app`. `events.onEvent` and `events.onTextDelta` follow the answering agent; `events.onAgentEvent(event, branch)` follows every delegated subagent live, its thinking, text and tool calls, with `branch.path` and `branch.callId` saying where it runs (ADR 0135).
|
|
108
108
|
|
|
109
|
+
A served syndicate also takes end-user ratings at `POST /feedback` (`{ traceId | taskId, score: 'up' | 'down' | 1–5, comment?, userId? }`, into the ledger's `adk_labels`; `melchizedek-agents/a2a/feedback`, ADR 0140), and `budgets(config, { onBudgetState, hardStop })` from `melchizedek-agents/a2a/policy` mirrors each caller's spend and takes an outside stop. On Postgres, `melchizedek_task_queue` (migration 0018) is the job queue's read model and `postgresStorage().retryTask(owner, id)` its Retry.
|
|
110
|
+
|
|
109
111
|
The example syndicates ship inside the package at `node_modules/melchizedek-agents/config/agents/examples/`; copy one out as a starting point.
|
|
110
112
|
|
|
111
113
|
### Custom Tools and Guards
|
|
@@ -1028,6 +1028,29 @@
|
|
|
1028
1028
|
}
|
|
1029
1029
|
},
|
|
1030
1030
|
"additionalProperties": false
|
|
1031
|
+
},
|
|
1032
|
+
"metadata": {
|
|
1033
|
+
"type": "object",
|
|
1034
|
+
"propertyNames": {
|
|
1035
|
+
"type": "string",
|
|
1036
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
|
|
1037
|
+
},
|
|
1038
|
+
"additionalProperties": {
|
|
1039
|
+
"anyOf": [
|
|
1040
|
+
{
|
|
1041
|
+
"type": "string",
|
|
1042
|
+
"maxLength": 256
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
"type": "number"
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
"type": "boolean"
|
|
1049
|
+
}
|
|
1050
|
+
]
|
|
1051
|
+
},
|
|
1052
|
+
"description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.agent.<key>` on this agent's agent.invoke span. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
|
|
1053
|
+
"maxProperties": 32
|
|
1031
1054
|
}
|
|
1032
1055
|
},
|
|
1033
1056
|
"required": [
|
|
@@ -2064,6 +2087,29 @@
|
|
|
2064
2087
|
},
|
|
2065
2088
|
"additionalProperties": false
|
|
2066
2089
|
},
|
|
2090
|
+
"metadata": {
|
|
2091
|
+
"type": "object",
|
|
2092
|
+
"propertyNames": {
|
|
2093
|
+
"type": "string",
|
|
2094
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
|
|
2095
|
+
},
|
|
2096
|
+
"additionalProperties": {
|
|
2097
|
+
"anyOf": [
|
|
2098
|
+
{
|
|
2099
|
+
"type": "string",
|
|
2100
|
+
"maxLength": 256
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
"type": "number"
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
"type": "boolean"
|
|
2107
|
+
}
|
|
2108
|
+
]
|
|
2109
|
+
},
|
|
2110
|
+
"description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.agent.<key>` on this agent's agent.invoke span. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
|
|
2111
|
+
"maxProperties": 32
|
|
2112
|
+
},
|
|
2067
2113
|
"yaml_reference": {
|
|
2068
2114
|
"description": "A whole nested syndicate (filename under the agents dir) used as this subagent. An approval request or ask_user question raised inside it pauses the turn with the agent path (ADR 0110, ADR 0111): a delegate syndicate's gates anywhere, a dispatch syndicate's on its routes, which it classifies and runs as at the top, its route's answer being its own (ADR 0120), and a workflow's ask_user nodes and gates, wherever each runs: delegated to as a subagent, as a dispatch route or as a workflow node (ADR 0119).",
|
|
2069
2115
|
"type": "string",
|
|
@@ -2384,6 +2430,29 @@
|
|
|
2384
2430
|
},
|
|
2385
2431
|
"additionalProperties": false
|
|
2386
2432
|
},
|
|
2433
|
+
"metadata": {
|
|
2434
|
+
"type": "object",
|
|
2435
|
+
"propertyNames": {
|
|
2436
|
+
"type": "string",
|
|
2437
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$"
|
|
2438
|
+
},
|
|
2439
|
+
"additionalProperties": {
|
|
2440
|
+
"anyOf": [
|
|
2441
|
+
{
|
|
2442
|
+
"type": "string",
|
|
2443
|
+
"maxLength": 256
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
"type": "number"
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
"type": "boolean"
|
|
2450
|
+
}
|
|
2451
|
+
]
|
|
2452
|
+
},
|
|
2453
|
+
"description": "Labels for the ledger: strings (at most 256 characters), numbers or booleans, at most 32 keys. Each becomes `meta.<key>` on every turn's root span, so on the ledger row (adk_turns.attributes); a key the YAML names cannot be set by an A2A caller. A value shaped like a key or token is dropped at load with a warning; string values pass through the telemetry redactor (ADR 0141).",
|
|
2454
|
+
"maxProperties": 32
|
|
2455
|
+
},
|
|
2387
2456
|
"variables": {
|
|
2388
2457
|
"description": "Defaults for {{token}} interpolation. {{current_date}} is built in.",
|
|
2389
2458
|
"type": "object",
|
|
@@ -53,6 +53,18 @@ syndicate_name: "My Syndicate"
|
|
|
53
53
|
variables:
|
|
54
54
|
my_variable: "default_value"
|
|
55
55
|
|
|
56
|
+
# [optional] Static labels for the ledger (ADR 0141): each becomes
|
|
57
|
+
# `meta.<key>` on the turn's root span, so on its adk_turns row; `tags`
|
|
58
|
+
# (comma-separated) gives the turn tags. Strings (at most 256 characters),
|
|
59
|
+
# numbers or booleans; keys [A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}; at most 32.
|
|
60
|
+
# ⚠ A value shaped like a key or token is dropped at load with a warning.
|
|
61
|
+
# An A2A caller's own metadata (X-Melch-Meta-*, Message.metadata) never
|
|
62
|
+
# overrides a key named here.
|
|
63
|
+
# metadata:
|
|
64
|
+
# environment: "prod"
|
|
65
|
+
# team: "growth"
|
|
66
|
+
# tags: "nightly,eu"
|
|
67
|
+
|
|
56
68
|
# [optional] Self-correction, ON by default: the engine's reflect-and-retry,
|
|
57
69
|
# ported from ADK's plugins (ADR 0034, ADR 0075). model_errors: retries of a model
|
|
58
70
|
# reply the engine marks malformed (default 2). tool_errors: retries of a tool that threw, with
|
|
@@ -323,6 +335,11 @@ orchestrator:
|
|
|
323
335
|
# stderr are each cut at 20,000 characters, with a line saying so.
|
|
324
336
|
# Maps to: tools[] (a toolset) and instruction (the appended index)
|
|
325
337
|
|
|
338
|
+
# [optional] Labels for this agent's agent.invoke span, each as
|
|
339
|
+
# `meta.agent.<key>`; the same rules as the syndicate's metadata.
|
|
340
|
+
# metadata:
|
|
341
|
+
# role: "researcher"
|
|
342
|
+
|
|
326
343
|
# [optional] Few-shot exchanges the engine adds to every request's
|
|
327
344
|
# instruction (lib/tools/examples.ts), in the block ADK's ExampleTool
|
|
328
345
|
# wrote; the model never calls it.
|
package/db/hardening.sql
CHANGED
|
@@ -84,7 +84,9 @@ BEGIN
|
|
|
84
84
|
END LOOP;
|
|
85
85
|
-- The production view and the retention function read/delete those
|
|
86
86
|
-- tables on behalf of the caller; neither belongs on the public API.
|
|
87
|
-
|
|
87
|
+
-- So does the job queue's read model (migration 0018).
|
|
88
|
+
FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly',
|
|
89
|
+
'melchizedek_task_queue'] LOOP
|
|
88
90
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
89
91
|
PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
|
|
90
92
|
END IF;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0016_span_tree — the ledger keeps a turn's span tree and its cache tokens
|
|
3
|
+
-- (ADR 0139)
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- adk_telemetry holds one row per span the ledger keeps: the turn's root,
|
|
6
|
+
-- each agent run, model step and provider call, tool call, script run and
|
|
7
|
+
-- call, workflow walk and node run (lib/observability/lineage.ts
|
|
8
|
+
-- ledgerSpanKind). Each row now says where it sits:
|
|
9
|
+
-- parent_span_id the nearest ancestor span the ledger keeps a row for;
|
|
10
|
+
-- NULL for a root, and for every row written before 0016
|
|
11
|
+
-- kind turn | agent | model | tool | script | node; NULL for a
|
|
12
|
+
-- row written before 0016
|
|
13
|
+
-- error the span failed (an error status or code, a tool's error
|
|
14
|
+
-- response); NULL for a row written before 0016
|
|
15
|
+
-- The full span (attributes, events, status) stays in `span`.
|
|
16
|
+
--
|
|
17
|
+
-- Cache tokens, the parts of input_tokens a provider read from or wrote to
|
|
18
|
+
-- its prompt cache (lib/models/contract.ts Usage), priced at their own rates
|
|
19
|
+
-- by 0017_cache_pricing:
|
|
20
|
+
-- adk_telemetry.cache_read_tokens / cache_write_tokens one call's (an
|
|
21
|
+
-- llm.request row), or the turn's sum (a root row)
|
|
22
|
+
-- adk_turns.cache_read_tokens / cache_write_tokens summed over every
|
|
23
|
+
-- call of the turn, as a provider bills them
|
|
24
|
+
-- NULL where nothing was reported, and on every row written before 0016.
|
|
25
|
+
--
|
|
26
|
+
-- The ledger tables are optional: db/telemetry.sql creates them, after the
|
|
27
|
+
-- migrations, and adds the same columns for a fresh install. This migration
|
|
28
|
+
-- upgrades a ledger that already exists in place, and does nothing where
|
|
29
|
+
-- there is none. Numbers and span ids only: nothing personal, nothing secret.
|
|
30
|
+
-- Idempotent; safe to re-run.
|
|
31
|
+
-- ============================================================================
|
|
32
|
+
|
|
33
|
+
DO $span_tree$
|
|
34
|
+
BEGIN
|
|
35
|
+
IF to_regclass('public.adk_telemetry') IS NOT NULL THEN
|
|
36
|
+
EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS parent_span_id TEXT';
|
|
37
|
+
EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS kind TEXT';
|
|
38
|
+
EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS error BOOLEAN';
|
|
39
|
+
EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS cache_read_tokens INTEGER';
|
|
40
|
+
EXECUTE 'ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS cache_write_tokens INTEGER';
|
|
41
|
+
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_adk_telemetry_parent ON adk_telemetry (trace_id, parent_span_id)';
|
|
42
|
+
END IF;
|
|
43
|
+
IF to_regclass('public.adk_turns') IS NOT NULL THEN
|
|
44
|
+
EXECUTE 'ALTER TABLE adk_turns ADD COLUMN IF NOT EXISTS cache_read_tokens INTEGER';
|
|
45
|
+
EXECUTE 'ALTER TABLE adk_turns ADD COLUMN IF NOT EXISTS cache_write_tokens INTEGER';
|
|
46
|
+
END IF;
|
|
47
|
+
END
|
|
48
|
+
$span_tree$;
|
|
49
|
+
|
|
50
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
51
|
+
VALUES (16, '0016_span_tree')
|
|
52
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0017_cache_pricing — prompt-cache reads and writes at their own rates
|
|
3
|
+
-- (ADR 0139, extends ADR 0136)
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- model_pricing gains two rates, USD per million tokens:
|
|
6
|
+
-- cache_read_per_mtok a token read from the provider's prompt cache
|
|
7
|
+
-- cache_write_per_mtok a token written to it (Anthropic's 5-minute cache,
|
|
8
|
+
-- the TTL the engine's requests get; OpenAI's 30-minute
|
|
9
|
+
-- write where its price list names one)
|
|
10
|
+
-- NULL prices that token as input, which is what every row meant before:
|
|
11
|
+
-- a cache token is an input token (lib/models/contract.ts Usage counts both
|
|
12
|
+
-- inside inputTokens), so a row without cache rates prices exactly as 0015.
|
|
13
|
+
--
|
|
14
|
+
-- The cost functions take the two counts:
|
|
15
|
+
-- melchizedek_cost_usd(model, input, output, thinking, cache_read, cache_write)
|
|
16
|
+
-- input less both cache counts at the input rate, each cache count at
|
|
17
|
+
-- its rate, output, thinking at the thinking rate (else output's)
|
|
18
|
+
-- melchizedek_cost_usd(model, input, output, thinking) the same with no
|
|
19
|
+
-- cache tokens, kept for a caller written against 0015
|
|
20
|
+
-- melchizedek_usage_cost(usage) reads each model's cache_read and
|
|
21
|
+
-- cache_write beside input, output and thinking in syndicate.usage;
|
|
22
|
+
-- melchizedek_usage_text_cost (db/telemetry.sql) calls it, so the
|
|
23
|
+
-- ledger trigger prices a turn's cache tokens too.
|
|
24
|
+
--
|
|
25
|
+
-- The seed: rates checked against the vendors' own price lists on
|
|
26
|
+
-- 2026-10-20 (platform.claude.com/docs/en/about-claude/pricing and
|
|
27
|
+
-- developers.openai.com/api/docs/pricing, standard tier), with verified_at
|
|
28
|
+
-- set to that day. An Anthropic row 0015 seeded gains its cache rates only
|
|
29
|
+
-- while it still holds 0015's input and output rates and no cache rate: an
|
|
30
|
+
-- operator's own rates are never overwritten. New rows never overwrite a
|
|
31
|
+
-- row that exists. Models the engine names but the lists price by prompt
|
|
32
|
+
-- length beyond a flat row (claude-haiku-5-5, gpt-5.5 and gpt-5.4 over
|
|
33
|
+
-- 272K) are left as noted below. Gemini, Grok and Kimi rows get no cache
|
|
34
|
+
-- rates here (none was checked): their cache tokens price as input.
|
|
35
|
+
--
|
|
36
|
+
-- Nothing secret, nothing personal. Idempotent; safe to re-run.
|
|
37
|
+
-- ============================================================================
|
|
38
|
+
|
|
39
|
+
ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_read_per_mtok NUMERIC CHECK (cache_read_per_mtok >= 0);
|
|
40
|
+
ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_per_mtok NUMERIC CHECK (cache_write_per_mtok >= 0);
|
|
41
|
+
|
|
42
|
+
-- Anthropic: the rows 0015 seeded, at the rates it seeded them.
|
|
43
|
+
UPDATE model_pricing p
|
|
44
|
+
SET cache_read_per_mtok = s.cache_read,
|
|
45
|
+
cache_write_per_mtok = s.cache_write,
|
|
46
|
+
verified_at = '2026-10-20',
|
|
47
|
+
source = 'platform.claude.com/docs/en/about-claude/pricing',
|
|
48
|
+
note = 'Anthropic first-party, checked 2026-10-20; cache write is the 5-minute rate',
|
|
49
|
+
updated_at = now()
|
|
50
|
+
FROM (VALUES
|
|
51
|
+
('claude-fable-5-1', 10.00, 50.00, 0.25, 12.50),
|
|
52
|
+
('claude-fable-5', 10.00, 50.00, 1.00, 12.50),
|
|
53
|
+
('claude-opus-5', 5.00, 25.00, 0.50, 6.25),
|
|
54
|
+
('claude-opus-4-8', 5.00, 25.00, 0.50, 6.25),
|
|
55
|
+
('claude-opus-4-7', 5.00, 25.00, 0.50, 6.25),
|
|
56
|
+
('claude-opus-4-6', 5.00, 25.00, 0.50, 6.25),
|
|
57
|
+
('claude-sonnet-5', 2.00, 10.00, 0.20, 2.50),
|
|
58
|
+
('claude-sonnet-4-6', 3.00, 15.00, 0.30, 3.75),
|
|
59
|
+
('claude-haiku-4-5', 1.00, 5.00, 0.10, 1.25)
|
|
60
|
+
) AS s(model, input, output, cache_read, cache_write)
|
|
61
|
+
WHERE p.model = s.model
|
|
62
|
+
AND p.input_per_mtok = s.input
|
|
63
|
+
AND p.output_per_mtok = s.output
|
|
64
|
+
AND p.cache_read_per_mtok IS NULL
|
|
65
|
+
AND p.cache_write_per_mtok IS NULL;
|
|
66
|
+
|
|
67
|
+
INSERT INTO model_pricing (model, input_per_mtok, output_per_mtok, cache_read_per_mtok, cache_write_per_mtok, verified_at, source, note) VALUES
|
|
68
|
+
-- Anthropic models 0015 did not seed. claude-haiku-5-5 is left out: its
|
|
69
|
+
-- price doubles past a 100K-token prompt, which a flat row cannot say.
|
|
70
|
+
('claude-opus-5-5', 4.00, 20.00, 0.20, 5.00, '2026-10-20', 'platform.claude.com/docs/en/about-claude/pricing', 'Anthropic first-party, checked 2026-10-20; cache write is the 5-minute rate'),
|
|
71
|
+
('claude-sonnet-5-5', 2.00, 10.00, 0.10, 2.50, '2026-10-20', 'platform.claude.com/docs/en/about-claude/pricing', 'Anthropic first-party, checked 2026-10-20; cache write is the 5-minute rate'),
|
|
72
|
+
-- OpenAI, standard tier. Reasoning tokens bill as output (thinking NULL).
|
|
73
|
+
-- A NULL cache write: the list names no write rate, so a write prices as input.
|
|
74
|
+
('gpt-6-astra', 10.00, 50.00, 1.00, 12.50, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
75
|
+
('gpt-6.1-sol', 2.00, 10.00, 0.10, 2.50, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
76
|
+
('gpt-6-sol', 2.00, 10.00, 0.20, 2.50, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
77
|
+
('gpt-6-luna', 0.10, 0.50, 0.01, 0.125, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
78
|
+
('gpt-5.6-sol', 4.00, 20.00, 0.40, 5.00, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
79
|
+
('gpt-5.6-terra', 2.00, 12.00, 0.20, 2.50, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
80
|
+
('gpt-5.6-luna', 0.20, 1.20, 0.02, 0.25, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; cache write is the 30-minute rate'),
|
|
81
|
+
('gpt-5.5', 5.00, 30.00, 0.50, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; the rate for prompts under 272K tokens'),
|
|
82
|
+
('gpt-5.4', 2.50, 15.00, 0.25, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20; the rate for prompts under 272K tokens'),
|
|
83
|
+
('gpt-5.2', 1.75, 14.00, 0.175, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20'),
|
|
84
|
+
('gpt-5.1', 1.25, 10.00, 0.125, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20'),
|
|
85
|
+
('gpt-5', 1.25, 10.00, 0.125, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20'),
|
|
86
|
+
('gpt-5-mini', 0.25, 2.00, 0.025, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20'),
|
|
87
|
+
('gpt-5-nano', 0.05, 0.40, 0.005, NULL, '2026-10-20', 'developers.openai.com/api/docs/pricing', 'OpenAI standard tier, checked 2026-10-20')
|
|
88
|
+
ON CONFLICT (model) DO NOTHING;
|
|
89
|
+
|
|
90
|
+
-- The row that prices `model`: its own, else its prefix's `<prefix>/*`.
|
|
91
|
+
CREATE OR REPLACE FUNCTION melchizedek_cost_usd(model TEXT, input BIGINT, output BIGINT, thinking BIGINT, cache_read BIGINT, cache_write BIGINT)
|
|
92
|
+
RETURNS NUMERIC
|
|
93
|
+
LANGUAGE sql
|
|
94
|
+
STABLE
|
|
95
|
+
SET search_path = public
|
|
96
|
+
AS $$
|
|
97
|
+
SELECT (greatest(coalesce(input, 0) - coalesce(cache_read, 0) - coalesce(cache_write, 0), 0) * p.input_per_mtok
|
|
98
|
+
+ coalesce(cache_read, 0) * coalesce(p.cache_read_per_mtok, p.input_per_mtok)
|
|
99
|
+
+ coalesce(cache_write, 0) * coalesce(p.cache_write_per_mtok, p.input_per_mtok)
|
|
100
|
+
+ coalesce(output, 0) * p.output_per_mtok
|
|
101
|
+
+ coalesce(thinking, 0) * coalesce(p.thinking_per_mtok, p.output_per_mtok)) / 1000000
|
|
102
|
+
FROM model_pricing p
|
|
103
|
+
WHERE p.model = melchizedek_cost_usd.model
|
|
104
|
+
OR (position('/' IN melchizedek_cost_usd.model) > 0
|
|
105
|
+
AND p.model = split_part(melchizedek_cost_usd.model, '/', 1) || '/*')
|
|
106
|
+
ORDER BY (p.model = melchizedek_cost_usd.model) DESC
|
|
107
|
+
LIMIT 1
|
|
108
|
+
$$;
|
|
109
|
+
|
|
110
|
+
CREATE OR REPLACE FUNCTION melchizedek_cost_usd(model TEXT, input BIGINT, output BIGINT, thinking BIGINT)
|
|
111
|
+
RETURNS NUMERIC
|
|
112
|
+
LANGUAGE sql
|
|
113
|
+
STABLE
|
|
114
|
+
SET search_path = public
|
|
115
|
+
AS $$
|
|
116
|
+
SELECT melchizedek_cost_usd(model, input, output, thinking, 0::bigint, 0::bigint)
|
|
117
|
+
$$;
|
|
118
|
+
|
|
119
|
+
CREATE OR REPLACE FUNCTION melchizedek_usage_cost(usage JSONB)
|
|
120
|
+
RETURNS NUMERIC
|
|
121
|
+
LANGUAGE sql
|
|
122
|
+
STABLE
|
|
123
|
+
SET search_path = public
|
|
124
|
+
AS $$
|
|
125
|
+
SELECT CASE
|
|
126
|
+
WHEN usage IS NULL OR jsonb_typeof(usage) <> 'object' THEN NULL
|
|
127
|
+
-- bool_and over no rows is NULL: a turn that made no model call costs nothing.
|
|
128
|
+
WHEN coalesce(bool_and(c.cost IS NOT NULL), true) THEN coalesce(sum(c.cost), 0)
|
|
129
|
+
ELSE NULL
|
|
130
|
+
END
|
|
131
|
+
FROM (
|
|
132
|
+
SELECT melchizedek_cost_usd(
|
|
133
|
+
e.key,
|
|
134
|
+
CASE WHEN jsonb_typeof(e.value->'input') = 'number' THEN (e.value->>'input')::numeric::bigint END,
|
|
135
|
+
CASE WHEN jsonb_typeof(e.value->'output') = 'number' THEN (e.value->>'output')::numeric::bigint END,
|
|
136
|
+
CASE WHEN jsonb_typeof(e.value->'thinking') = 'number' THEN (e.value->>'thinking')::numeric::bigint END,
|
|
137
|
+
CASE WHEN jsonb_typeof(e.value->'cache_read') = 'number' THEN (e.value->>'cache_read')::numeric::bigint END,
|
|
138
|
+
CASE WHEN jsonb_typeof(e.value->'cache_write') = 'number' THEN (e.value->>'cache_write')::numeric::bigint END
|
|
139
|
+
) AS cost
|
|
140
|
+
FROM jsonb_each(CASE WHEN jsonb_typeof(usage) = 'object' THEN usage ELSE '{}'::jsonb END) e
|
|
141
|
+
) c
|
|
142
|
+
$$;
|
|
143
|
+
|
|
144
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
145
|
+
VALUES (17, '0017_cache_pricing')
|
|
146
|
+
ON CONFLICT (version) DO NOTHING;
|