melchizedek-agents 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/CHANGELOG.md +199 -0
- package/DOCUMENTATION.md +84 -6
- package/ONBOARDING.md +3 -3
- package/README.md +2 -1
- package/config/agents/examples/connectors.yaml +4 -1
- package/config/agents/examples/event_assistant.yaml +82 -0
- package/config/agents/syndicate.schema.json +80 -6
- package/db/hardening.sql +43 -14
- package/db/migrations/0015_model_pricing.sql +107 -0
- package/db/telemetry.sql +79 -1
- package/dist/lib/a2a/app.js +2 -1
- package/dist/lib/chatgpt/oauth.d.ts +4 -3
- package/dist/lib/chatgpt/oauth.js +36 -12
- package/dist/lib/chatgpt/state.js +4 -4
- package/dist/lib/compile.js +15 -2
- package/dist/lib/compileNative.js +1 -0
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.js +4 -0
- package/dist/lib/memory/providers.js +2 -1
- package/dist/lib/models/claudeAdapter.js +2 -1
- package/dist/lib/models/credentialSource.d.ts +28 -0
- package/dist/lib/models/credentialSource.js +49 -0
- package/dist/lib/models/endpoints.js +2 -1
- package/dist/lib/models/gateway.js +2 -1
- package/dist/lib/models/gatewayAdapter.js +2 -1
- package/dist/lib/models/geminiAdapter.js +2 -1
- package/dist/lib/models/gptAdapter.js +13 -2
- package/dist/lib/models/grokAdapter.js +2 -1
- package/dist/lib/models/kimiAdapter.js +2 -1
- package/dist/lib/models/providerMap.js +3 -2
- package/dist/lib/observability/embeddings.js +2 -1
- package/dist/lib/observability/lineage.js +11 -3
- package/dist/lib/observability/supabaseSpanExporter.d.ts +19 -1
- package/dist/lib/observability/supabaseSpanExporter.js +37 -1
- package/dist/lib/observability/tracer.d.ts +18 -0
- package/dist/lib/observability/tracer.js +60 -2
- package/dist/lib/paths.d.ts +31 -0
- package/dist/lib/paths.js +76 -0
- package/dist/lib/runtime/credentials.d.ts +21 -0
- package/dist/lib/runtime/credentials.js +32 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +6 -0
- package/dist/lib/runtime/native/delegate.d.ts +2 -0
- package/dist/lib/runtime/native/delegate.js +39 -3
- package/dist/lib/runtime/native/interrupts.d.ts +33 -3
- package/dist/lib/runtime/native/interrupts.js +99 -9
- package/dist/lib/runtime/syndicateTurn.d.ts +14 -1
- package/dist/lib/runtime/syndicateTurn.js +45 -11
- package/dist/lib/runtime/turnControl.d.ts +27 -0
- package/dist/lib/storage/postgres/index.d.ts +25 -0
- package/dist/lib/storage/postgres/index.js +10 -1
- package/dist/lib/syndicateSchema.d.ts +26 -4
- package/dist/lib/syndicateSchema.js +63 -1
- package/dist/lib/tools/generateImageTool.js +6 -6
- package/dist/lib/tools/inspectImageTool.d.ts +2 -0
- package/dist/lib/tools/inspectImageTool.js +17 -10
- package/dist/lib/tools/mcpToolFactory.d.ts +34 -1
- package/dist/lib/tools/mcpToolFactory.js +200 -27
- package/dist/lib/tools/mcpTransports.d.ts +4 -2
- package/dist/lib/tools/mcpTransports.js +4 -2
- package/dist/lib/tools/taskTools.js +3 -3
- package/dist/lib/tools/xApiSearchTool.js +2 -1
- package/dist/lib/workflow/agentNode.d.ts +19 -3
- package/dist/lib/workflow/agentNode.js +106 -13
- package/dist/lib/workflow/resume.d.ts +8 -7
- package/dist/lib/workflow/resume.js +17 -10
- package/dist/lib/workflow/turn.d.ts +12 -2
- package/dist/lib/workflow/turn.js +22 -12
- package/dist/scripts/a2a_server.js +5 -0
- package/package.json +22 -15
package/db/hardening.sql
CHANGED
|
@@ -30,6 +30,27 @@
|
|
|
30
30
|
-- The A2A server checks at boot whether this file has been applied and
|
|
31
31
|
-- prints a prominent warning if not (see lib/persistence/supabaseProvider.ts).
|
|
32
32
|
|
|
33
|
+
-- ── The API roles ────────────────────────────────────────────────────────
|
|
34
|
+
-- anon and authenticated are Supabase's PostgREST roles. Plain Postgres and
|
|
35
|
+
-- embedded Postgres (PGlite) have no REST API and normally no such roles;
|
|
36
|
+
-- there, the PUBLIC revokes and RLS below are the whole lockdown, and the
|
|
37
|
+
-- revokes by role name are skipped rather than failing. These two helpers
|
|
38
|
+
-- live in pg_temp: they exist for this session only and leave nothing behind.
|
|
39
|
+
CREATE OR REPLACE FUNCTION pg_temp.melchizedek_api_roles() RETURNS text
|
|
40
|
+
LANGUAGE sql AS $$
|
|
41
|
+
SELECT string_agg(quote_ident(rolname), ', ' ORDER BY rolname)
|
|
42
|
+
FROM pg_roles WHERE rolname IN ('anon', 'authenticated');
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION pg_temp.melchizedek_revoke_api(target text) RETURNS void
|
|
46
|
+
LANGUAGE plpgsql AS $$
|
|
47
|
+
DECLARE roles text := pg_temp.melchizedek_api_roles();
|
|
48
|
+
BEGIN
|
|
49
|
+
IF roles IS NOT NULL THEN
|
|
50
|
+
EXECUTE format('REVOKE ALL ON %s FROM %s', target, roles);
|
|
51
|
+
END IF;
|
|
52
|
+
END $$;
|
|
53
|
+
|
|
33
54
|
-- ── Tier 1: lock the public API paths ────────────────────────────────────
|
|
34
55
|
|
|
35
56
|
-- Enable RLS. With RLS on and NO policies defined, anon/authenticated get
|
|
@@ -40,8 +61,11 @@ ALTER TABLE adk_sessions ENABLE ROW LEVEL SECURITY;
|
|
|
40
61
|
-- Belt and suspenders: also revoke the default table privileges from the
|
|
41
62
|
-- API roles, so even a future accidentally-created permissive policy
|
|
42
63
|
-- cannot re-expose the tables.
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
DO $$
|
|
65
|
+
BEGIN
|
|
66
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_memory_facts');
|
|
67
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_sessions');
|
|
68
|
+
END $$;
|
|
45
69
|
|
|
46
70
|
-- Optional observability ledger (db/telemetry.sql). Guarded: the tables only
|
|
47
71
|
-- exist when the operator opted into TELEMETRY_SUPABASE. adk_turns holds
|
|
@@ -51,17 +75,18 @@ DO $$
|
|
|
51
75
|
DECLARE t text;
|
|
52
76
|
BEGIN
|
|
53
77
|
FOREACH t IN ARRAY ARRAY['adk_telemetry', 'adk_turns', 'adk_payloads', 'adk_verdicts', 'adk_labels',
|
|
54
|
-
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage', 'melchizedek_audit'
|
|
78
|
+
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage', 'melchizedek_audit',
|
|
79
|
+
'model_pricing'] LOOP
|
|
55
80
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
56
81
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
|
57
|
-
|
|
82
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
|
|
58
83
|
END IF;
|
|
59
84
|
END LOOP;
|
|
60
85
|
-- The production view and the retention function read/delete those
|
|
61
86
|
-- tables on behalf of the caller; neither belongs on the public API.
|
|
62
87
|
FOREACH t IN ARRAY ARRAY['adk_turns_production', 'adk_kpi_daily', 'adk_kpi_routes_daily', 'adk_kpi_judges_daily', 'adk_kpi_hourly'] LOOP
|
|
63
88
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
64
|
-
|
|
89
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('%I', t));
|
|
65
90
|
END IF;
|
|
66
91
|
END LOOP;
|
|
67
92
|
-- Function privileges for these are handled in the FUNCTIONS block below.
|
|
@@ -73,7 +98,7 @@ DO $$
|
|
|
73
98
|
BEGIN
|
|
74
99
|
IF to_regclass('public.melchizedek_schema_version') IS NOT NULL THEN
|
|
75
100
|
EXECUTE 'ALTER TABLE melchizedek_schema_version ENABLE ROW LEVEL SECURITY';
|
|
76
|
-
|
|
101
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_schema_version');
|
|
77
102
|
END IF;
|
|
78
103
|
END $$;
|
|
79
104
|
|
|
@@ -84,29 +109,29 @@ DO $$
|
|
|
84
109
|
BEGIN
|
|
85
110
|
IF to_regclass('public.adk_agent_registry') IS NOT NULL THEN
|
|
86
111
|
EXECUTE 'ALTER TABLE adk_agent_registry ENABLE ROW LEVEL SECURITY';
|
|
87
|
-
|
|
112
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry');
|
|
88
113
|
END IF;
|
|
89
114
|
-- The task tools' lists and job queue (migration 0009).
|
|
90
115
|
IF to_regclass('public.melchizedek_tasks') IS NOT NULL THEN
|
|
91
116
|
EXECUTE 'ALTER TABLE melchizedek_tasks ENABLE ROW LEVEL SECURITY';
|
|
92
117
|
EXECUTE 'ALTER TABLE melchizedek_task_owners ENABLE ROW LEVEL SECURITY';
|
|
93
|
-
|
|
118
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tasks, melchizedek_task_owners');
|
|
94
119
|
END IF;
|
|
95
120
|
-- The memory processed marker (migration 0007): identifiers only.
|
|
96
121
|
IF to_regclass('public.melchizedek_memory_ingest') IS NOT NULL THEN
|
|
97
122
|
EXECUTE 'ALTER TABLE melchizedek_memory_ingest ENABLE ROW LEVEL SECURITY';
|
|
98
|
-
|
|
123
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_memory_ingest');
|
|
99
124
|
END IF;
|
|
100
125
|
-- Third-party tokens held for tools (migration 0013): ciphertext only,
|
|
101
126
|
-- still nothing an API role should read.
|
|
102
127
|
IF to_regclass('public.melchizedek_tool_credentials') IS NOT NULL THEN
|
|
103
128
|
EXECUTE 'ALTER TABLE melchizedek_tool_credentials ENABLE ROW LEVEL SECURITY';
|
|
104
|
-
|
|
129
|
+
PERFORM pg_temp.melchizedek_revoke_api('melchizedek_tool_credentials');
|
|
105
130
|
END IF;
|
|
106
131
|
-- Its history (migration 0005): every definition an id has held.
|
|
107
132
|
IF to_regclass('public.adk_agent_registry_versions') IS NOT NULL THEN
|
|
108
133
|
EXECUTE 'ALTER TABLE adk_agent_registry_versions ENABLE ROW LEVEL SECURITY';
|
|
109
|
-
|
|
134
|
+
PERFORM pg_temp.melchizedek_revoke_api('adk_agent_registry_versions');
|
|
110
135
|
END IF;
|
|
111
136
|
END $$;
|
|
112
137
|
|
|
@@ -139,9 +164,12 @@ BEGIN
|
|
|
139
164
|
'melchizedek_registry_versions_immutable', 'melchizedek_registry_publish',
|
|
140
165
|
'melchizedek_registry_activate', 'melchizedek_memory_commit',
|
|
141
166
|
'melchizedek_memory_dimensions', 'melchizedek_prune_memory_ingest',
|
|
142
|
-
'melchizedek_prune_memory', 'melchizedek_prune_audit'
|
|
167
|
+
'melchizedek_prune_memory', 'melchizedek_prune_audit',
|
|
168
|
+
'melchizedek_cost_usd', 'melchizedek_usage_cost', 'melchizedek_usage_text_cost',
|
|
169
|
+
'melchizedek_turns_cost', 'melchizedek_telemetry_cost')
|
|
143
170
|
LOOP
|
|
144
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC
|
|
171
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC', fn);
|
|
172
|
+
PERFORM pg_temp.melchizedek_revoke_api(format('FUNCTION %s', fn));
|
|
145
173
|
IF has_service_role THEN
|
|
146
174
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);
|
|
147
175
|
END IF;
|
|
@@ -170,9 +198,10 @@ AS $$
|
|
|
170
198
|
'melchizedek_audit', 'melchizedek_tool_credentials');
|
|
171
199
|
$$;
|
|
172
200
|
|
|
173
|
-
REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC
|
|
201
|
+
REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC;
|
|
174
202
|
DO $$
|
|
175
203
|
BEGIN
|
|
204
|
+
PERFORM pg_temp.melchizedek_revoke_api('FUNCTION melchizedek_rls_status()');
|
|
176
205
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
177
206
|
GRANT EXECUTE ON FUNCTION melchizedek_rls_status() TO service_role;
|
|
178
207
|
END IF;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0015_model_pricing — what a model call costs, so the ledger records dollars
|
|
3
|
+
-- (ADR 0136)
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- model_pricing: USD per million tokens, one row per model id as the YAML
|
|
6
|
+
-- declares it (`claude-opus-5`, `gemini-3.8-flash`, `grok-4.7`). A row whose
|
|
7
|
+
-- id ends in `/*` prices every id under that prefix (`ollama/*`: local
|
|
8
|
+
-- models cost nothing). `thinking_per_mtok` NULL means thinking bills at the
|
|
9
|
+
-- output rate, as every provider here bills it. `verified_at` NULL means
|
|
10
|
+
-- nobody has checked the row against the vendor's price list: spend built on
|
|
11
|
+
-- it is a steering estimate (the convention ymir_model_pricing set).
|
|
12
|
+
--
|
|
13
|
+
-- Seeded from melch-obs's pricing.toml (2026-10). The seed never overwrites a
|
|
14
|
+
-- row: an operator's own rates survive a re-run. A rate change ships as a
|
|
15
|
+
-- later migration that UPDATEs the row and its verified_at.
|
|
16
|
+
--
|
|
17
|
+
-- The cost functions below are what db/telemetry.sql's ledger trigger calls
|
|
18
|
+
-- to fill adk_turns.cost_usd and adk_telemetry.cost_usd at write time:
|
|
19
|
+
-- melchizedek_cost_usd(model, input, output, thinking) one call's cost,
|
|
20
|
+
-- NULL for a model with no row
|
|
21
|
+
-- melchizedek_usage_cost(usage) a turn's cost from its per-model usage
|
|
22
|
+
-- ({"<model>": {"input": n, "output": n, "thinking": n}}), NULL when
|
|
23
|
+
-- any model in it has no row: a partial sum would read as the total.
|
|
24
|
+
-- Ledger output_tokens exclude thinking (genaiMapping's usage meaning), so
|
|
25
|
+
-- thinking is priced on top of output.
|
|
26
|
+
--
|
|
27
|
+
-- Nothing secret, nothing personal; hardening.sql closes it to the API roles
|
|
28
|
+
-- like every other table. Idempotent; safe to re-run.
|
|
29
|
+
-- ============================================================================
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
32
|
+
model TEXT PRIMARY KEY,
|
|
33
|
+
input_per_mtok NUMERIC NOT NULL CHECK (input_per_mtok >= 0),
|
|
34
|
+
output_per_mtok NUMERIC NOT NULL CHECK (output_per_mtok >= 0),
|
|
35
|
+
thinking_per_mtok NUMERIC CHECK (thinking_per_mtok >= 0),
|
|
36
|
+
verified_at TIMESTAMPTZ,
|
|
37
|
+
source TEXT,
|
|
38
|
+
note TEXT,
|
|
39
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
INSERT INTO model_pricing (model, input_per_mtok, output_per_mtok, verified_at, source, note) VALUES
|
|
43
|
+
-- Anthropic first-party rates; pricing.toml dates the check to the month (2026-06).
|
|
44
|
+
('claude-fable-5-1', 10.00, 50.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
45
|
+
('claude-fable-5', 10.00, 50.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
46
|
+
('claude-opus-5', 5.00, 25.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
47
|
+
('claude-opus-4-8', 5.00, 25.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
48
|
+
('claude-opus-4-7', 5.00, 25.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
49
|
+
('claude-opus-4-6', 5.00, 25.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
50
|
+
('claude-sonnet-5', 2.00, 10.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
51
|
+
('claude-sonnet-4-6', 3.00, 15.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
52
|
+
('claude-haiku-4-5', 1.00, 5.00, '2026-06-01', 'melch-obs/pricing.toml', 'Anthropic first-party, checked 2026-06'),
|
|
53
|
+
('gemini-3.5-flash-lite', 0.30, 2.50, '2026-10-01', 'melch-obs/pricing.toml', 'Gemini API standard tier, checked 2026-10-01'),
|
|
54
|
+
-- Carried over from ymir_model_pricing, never checked against a vendor list.
|
|
55
|
+
('gemini-3.1-flash-lite', 0.10, 0.40, NULL, 'melch-obs/pricing.toml', 'unverified estimate'),
|
|
56
|
+
('gemini-3.7-flash', 0.30, 2.50, NULL, 'melch-obs/pricing.toml', 'unverified estimate'),
|
|
57
|
+
('gemini-3.8-flash', 0.30, 2.50, NULL, 'melch-obs/pricing.toml', 'placeholder at 3.7''s rate, unverified'),
|
|
58
|
+
('grok-4.5', 3.00, 15.00, NULL, 'melch-obs/pricing.toml', 'unverified estimate'),
|
|
59
|
+
('grok-4.7', 3.00, 15.00, NULL, 'melch-obs/pricing.toml', 'unverified estimate'),
|
|
60
|
+
-- A local model bills nothing; that needs no vendor list.
|
|
61
|
+
('ollama/*', 0, 0, '2026-10-15', 'engine', 'local models: no provider charge')
|
|
62
|
+
ON CONFLICT (model) DO NOTHING;
|
|
63
|
+
|
|
64
|
+
-- The row that prices `model`: its own, else its prefix's `<prefix>/*`.
|
|
65
|
+
CREATE OR REPLACE FUNCTION melchizedek_cost_usd(model TEXT, input BIGINT, output BIGINT, thinking BIGINT)
|
|
66
|
+
RETURNS NUMERIC
|
|
67
|
+
LANGUAGE sql
|
|
68
|
+
STABLE
|
|
69
|
+
SET search_path = public
|
|
70
|
+
AS $$
|
|
71
|
+
SELECT (coalesce(input, 0) * p.input_per_mtok
|
|
72
|
+
+ coalesce(output, 0) * p.output_per_mtok
|
|
73
|
+
+ coalesce(thinking, 0) * coalesce(p.thinking_per_mtok, p.output_per_mtok)) / 1000000
|
|
74
|
+
FROM model_pricing p
|
|
75
|
+
WHERE p.model = melchizedek_cost_usd.model
|
|
76
|
+
OR (position('/' IN melchizedek_cost_usd.model) > 0
|
|
77
|
+
AND p.model = split_part(melchizedek_cost_usd.model, '/', 1) || '/*')
|
|
78
|
+
ORDER BY (p.model = melchizedek_cost_usd.model) DESC
|
|
79
|
+
LIMIT 1
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
CREATE OR REPLACE FUNCTION melchizedek_usage_cost(usage JSONB)
|
|
83
|
+
RETURNS NUMERIC
|
|
84
|
+
LANGUAGE sql
|
|
85
|
+
STABLE
|
|
86
|
+
SET search_path = public
|
|
87
|
+
AS $$
|
|
88
|
+
SELECT CASE
|
|
89
|
+
WHEN usage IS NULL OR jsonb_typeof(usage) <> 'object' THEN NULL
|
|
90
|
+
-- bool_and over no rows is NULL: a turn that made no model call costs nothing.
|
|
91
|
+
WHEN coalesce(bool_and(c.cost IS NOT NULL), true) THEN coalesce(sum(c.cost), 0)
|
|
92
|
+
ELSE NULL
|
|
93
|
+
END
|
|
94
|
+
FROM (
|
|
95
|
+
SELECT melchizedek_cost_usd(
|
|
96
|
+
e.key,
|
|
97
|
+
CASE WHEN jsonb_typeof(e.value->'input') = 'number' THEN (e.value->>'input')::numeric::bigint END,
|
|
98
|
+
CASE WHEN jsonb_typeof(e.value->'output') = 'number' THEN (e.value->>'output')::numeric::bigint END,
|
|
99
|
+
CASE WHEN jsonb_typeof(e.value->'thinking') = 'number' THEN (e.value->>'thinking')::numeric::bigint END
|
|
100
|
+
) AS cost
|
|
101
|
+
FROM jsonb_each(CASE WHEN jsonb_typeof(usage) = 'object' THEN usage ELSE '{}'::jsonb END) e
|
|
102
|
+
) c
|
|
103
|
+
$$;
|
|
104
|
+
|
|
105
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
106
|
+
VALUES (15, '0015_model_pricing')
|
|
107
|
+
ON CONFLICT (version) DO NOTHING;
|
package/db/telemetry.sql
CHANGED
|
@@ -163,6 +163,83 @@ CREATE INDEX IF NOT EXISTS idx_adk_turns_surface_user
|
|
|
163
163
|
-- at CREATE time. It does not gain these columns until it is re-created, so
|
|
164
164
|
-- run the whole file, in order.
|
|
165
165
|
|
|
166
|
+
-- ── Cost: dollars per turn and per model call (ADR 0136) ─────────────────
|
|
167
|
+
-- cost_usd is filled at write time by the trigger below from model_pricing
|
|
168
|
+
-- (db/migrations/0015_model_pricing.sql), so both sinks and a replayed
|
|
169
|
+
-- dead-letter spool get it, and the price table is the one source:
|
|
170
|
+
-- adk_telemetry an llm.request row: its model and tokens. A root row: the
|
|
171
|
+
-- turn's per-model usage on the span (syndicate.usage).
|
|
172
|
+
-- adk_turns the turn's per-model usage (attributes->'syndicate.usage',
|
|
173
|
+
-- the tracer sums each model's calls), so a turn that used
|
|
174
|
+
-- two models is priced per model, input summed per call.
|
|
175
|
+
-- NULL when a model has no price row (a partial sum would read as the
|
|
176
|
+
-- total), or when the row carries no usage (a row written before this
|
|
177
|
+
-- column). A value the writer set itself is kept. The rate in force at
|
|
178
|
+
-- write time is the rate recorded: a later price change does not rewrite
|
|
179
|
+
-- history. Re-price on purpose with an UPDATE ... SET cost_usd = ... .
|
|
180
|
+
ALTER TABLE adk_turns ADD COLUMN IF NOT EXISTS cost_usd NUMERIC;
|
|
181
|
+
ALTER TABLE adk_telemetry ADD COLUMN IF NOT EXISTS cost_usd NUMERIC;
|
|
182
|
+
|
|
183
|
+
-- SECURITY DEFINER: the inserting role (service_role, or an application's
|
|
184
|
+
-- own role) need not be able to execute the pricing functions or read
|
|
185
|
+
-- model_pricing. Each reads only NEW and the price table. Unreadable usage
|
|
186
|
+
-- leaves the cost NULL; it never fails the insert.
|
|
187
|
+
CREATE OR REPLACE FUNCTION melchizedek_usage_text_cost(usage_text TEXT)
|
|
188
|
+
RETURNS NUMERIC
|
|
189
|
+
LANGUAGE plpgsql
|
|
190
|
+
STABLE
|
|
191
|
+
SET search_path = public
|
|
192
|
+
AS $$
|
|
193
|
+
BEGIN
|
|
194
|
+
IF usage_text IS NULL OR to_regclass('model_pricing') IS NULL THEN
|
|
195
|
+
RETURN NULL;
|
|
196
|
+
END IF;
|
|
197
|
+
RETURN melchizedek_usage_cost(usage_text::jsonb);
|
|
198
|
+
EXCEPTION WHEN others THEN
|
|
199
|
+
RETURN NULL;
|
|
200
|
+
END;
|
|
201
|
+
$$;
|
|
202
|
+
|
|
203
|
+
CREATE OR REPLACE FUNCTION melchizedek_turns_cost()
|
|
204
|
+
RETURNS trigger
|
|
205
|
+
LANGUAGE plpgsql
|
|
206
|
+
SECURITY DEFINER
|
|
207
|
+
SET search_path = public
|
|
208
|
+
AS $$
|
|
209
|
+
BEGIN
|
|
210
|
+
IF NEW.cost_usd IS NULL THEN
|
|
211
|
+
NEW.cost_usd := melchizedek_usage_text_cost(NEW.attributes->>'syndicate.usage');
|
|
212
|
+
END IF;
|
|
213
|
+
RETURN NEW;
|
|
214
|
+
END;
|
|
215
|
+
$$;
|
|
216
|
+
|
|
217
|
+
CREATE OR REPLACE FUNCTION melchizedek_telemetry_cost()
|
|
218
|
+
RETURNS trigger
|
|
219
|
+
LANGUAGE plpgsql
|
|
220
|
+
SECURITY DEFINER
|
|
221
|
+
SET search_path = public
|
|
222
|
+
AS $$
|
|
223
|
+
BEGIN
|
|
224
|
+
IF NEW.cost_usd IS NOT NULL OR to_regclass('model_pricing') IS NULL THEN
|
|
225
|
+
RETURN NEW;
|
|
226
|
+
END IF;
|
|
227
|
+
IF NEW.model IS NOT NULL THEN
|
|
228
|
+
NEW.cost_usd := melchizedek_cost_usd(NEW.model, NEW.input_tokens, NEW.output_tokens, NEW.thinking_tokens);
|
|
229
|
+
ELSE
|
|
230
|
+
NEW.cost_usd := melchizedek_usage_text_cost(NEW.span->'attributes'->>'syndicate.usage');
|
|
231
|
+
END IF;
|
|
232
|
+
RETURN NEW;
|
|
233
|
+
END;
|
|
234
|
+
$$;
|
|
235
|
+
|
|
236
|
+
DROP TRIGGER IF EXISTS melchizedek_turns_cost ON adk_turns;
|
|
237
|
+
CREATE TRIGGER melchizedek_turns_cost BEFORE INSERT ON adk_turns
|
|
238
|
+
FOR EACH ROW EXECUTE FUNCTION melchizedek_turns_cost();
|
|
239
|
+
DROP TRIGGER IF EXISTS melchizedek_telemetry_cost ON adk_telemetry;
|
|
240
|
+
CREATE TRIGGER melchizedek_telemetry_cost BEFORE INSERT ON adk_telemetry
|
|
241
|
+
FOR EACH ROW EXECUTE FUNCTION melchizedek_telemetry_cost();
|
|
242
|
+
|
|
166
243
|
-- ── adk_payloads: full prompt/response per model call, by policy, expiring ─
|
|
167
244
|
CREATE TABLE IF NOT EXISTS adk_payloads (
|
|
168
245
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
@@ -360,7 +437,8 @@ CREATE OR REPLACE VIEW adk_kpi_daily AS
|
|
|
360
437
|
sum(output_tokens) AS output_tokens,
|
|
361
438
|
sum(thinking_tokens) AS thinking_tokens,
|
|
362
439
|
avg(llm_calls) AS avg_llm_calls,
|
|
363
|
-
avg(tool_calls) AS avg_tool_calls
|
|
440
|
+
avg(tool_calls) AS avg_tool_calls,
|
|
441
|
+
sum(cost_usd) AS cost_usd
|
|
364
442
|
FROM adk_turns_production
|
|
365
443
|
GROUP BY 1, 2;
|
|
366
444
|
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -68,6 +68,7 @@ import { setOAuthHosts } from '../tools/oauthHosts.js';
|
|
|
68
68
|
import { setCredentialHosts } from '../tools/credentialHosts.js';
|
|
69
69
|
import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
|
|
70
70
|
import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
|
|
71
|
+
import { credentialValue } from '../models/credentialSource.js';
|
|
71
72
|
const SURFACE_HEADERS = [
|
|
72
73
|
['x-surface', 'name'],
|
|
73
74
|
['x-surface-guild', 'guild'],
|
|
@@ -335,7 +336,7 @@ export async function createA2AApp(options) {
|
|
|
335
336
|
// Embeddings and fact extraction use the SERVER's Gemini key: memory is
|
|
336
337
|
// operator infrastructure, like tools.
|
|
337
338
|
const services = await createSupabaseServices({
|
|
338
|
-
apiKey:
|
|
339
|
+
apiKey: credentialValue('GOOGLE_GENAI_API_KEY') || credentialValue('GEMINI_API_KEY') || '',
|
|
339
340
|
withMemory: true,
|
|
340
341
|
extractor: options.memory?.extractor,
|
|
341
342
|
embedder: options.memory?.embedder,
|
|
@@ -75,9 +75,10 @@ export interface SignInResult {
|
|
|
75
75
|
planUsage: boolean;
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* The browser sign-in. Stores the
|
|
79
|
-
*
|
|
80
|
-
* the
|
|
78
|
+
* The browser sign-in. Stores the host id before the browser opens when no
|
|
79
|
+
* file exists yet, the registration (host id, issued client id) before the
|
|
80
|
+
* code exchange, and the tokens after the ID token verifies and the plan
|
|
81
|
+
* scope is confirmed.
|
|
81
82
|
*/
|
|
82
83
|
export declare function signIn(opts: SignInOptions): Promise<SignInResult>;
|
|
83
84
|
export interface AccessTokenOptions extends OAuthOptions {
|
|
@@ -50,6 +50,14 @@ const UNUSABLE_REFRESH = new Set([
|
|
|
50
50
|
'refresh_token_invalidated',
|
|
51
51
|
'refresh_token_reused',
|
|
52
52
|
]);
|
|
53
|
+
/**
|
|
54
|
+
* Callback error codes that mean the consent page refused or the person
|
|
55
|
+
* stopped there: OpenAI's page for a plan or workspace without plan usage for
|
|
56
|
+
* apps offers no way on, so leaving it is the usual way these arrive.
|
|
57
|
+
*/
|
|
58
|
+
const REFUSED_AT_CONSENT = new Set(['access_denied', 'unauthorized_client', 'consent_required', 'interaction_required']);
|
|
59
|
+
/** One line for a sign-in OpenAI refused at the consent page or the code exchange. */
|
|
60
|
+
const signInRefused = (code) => `ChatGPT refused the sign-in (${code}); the most common cause is a ChatGPT plan or workspace that does not include plan usage for apps. Retry with \`npm run setup -- --chatgpt-signin\` (\`npx melchizedek-setup --chatgpt-signin\` from an installed package).`;
|
|
53
61
|
/** A sign-in failure: a code for the program, a message for the person, never a token. */
|
|
54
62
|
export class SignInError extends Error {
|
|
55
63
|
code;
|
|
@@ -236,7 +244,7 @@ async function listen(port, state, savedClientId, timeoutMs, signal) {
|
|
|
236
244
|
res.writeHead(200, PAGE_HEADERS).end(page('Sign-in cancelled', 'You can close this tab and return to the terminal.'));
|
|
237
245
|
const raw = url.searchParams.get('error') ?? '';
|
|
238
246
|
const code = /^[a-z0-9_.-]{1,64}$/i.test(raw) ? raw : 'authorization_failed';
|
|
239
|
-
settle(new SignInError(code, `ChatGPT did not authorize the sign-in (${code}).`));
|
|
247
|
+
settle(new SignInError(code, REFUSED_AT_CONSENT.has(code) ? signInRefused(code) : `ChatGPT did not authorize the sign-in (${code}).`));
|
|
240
248
|
return;
|
|
241
249
|
}
|
|
242
250
|
const codes = url.searchParams.getAll('code');
|
|
@@ -284,9 +292,10 @@ export async function openInBrowser(url) {
|
|
|
284
292
|
});
|
|
285
293
|
}
|
|
286
294
|
/**
|
|
287
|
-
* The browser sign-in. Stores the
|
|
288
|
-
*
|
|
289
|
-
* the
|
|
295
|
+
* The browser sign-in. Stores the host id before the browser opens when no
|
|
296
|
+
* file exists yet, the registration (host id, issued client id) before the
|
|
297
|
+
* code exchange, and the tokens after the ID token verifies and the plan
|
|
298
|
+
* scope is confirmed.
|
|
290
299
|
*/
|
|
291
300
|
export async function signIn(opts) {
|
|
292
301
|
const issuer = issuerOf(opts);
|
|
@@ -295,6 +304,11 @@ export async function signIn(opts) {
|
|
|
295
304
|
const sameIssuer = previous?.issuer === issuer;
|
|
296
305
|
const hostId = previous?.hostId ?? `urn:uuid:${randomUUID()}`;
|
|
297
306
|
const savedClientId = sameIssuer ? previous?.clientId : undefined;
|
|
307
|
+
// OpenAI asks for the host id to be chosen and persisted before the first
|
|
308
|
+
// sign-in, so a timed-out or cancelled attempt does not send a new one next
|
|
309
|
+
// time. Only written when nothing is stored: a stored registration is kept.
|
|
310
|
+
if (!previous)
|
|
311
|
+
writeStoredSignIn(opts.file, { version: 1, issuer, hostId });
|
|
298
312
|
const state = randomValue();
|
|
299
313
|
const nonce = randomValue();
|
|
300
314
|
const verifier = randomValue();
|
|
@@ -326,14 +340,24 @@ export async function signIn(opts) {
|
|
|
326
340
|
? { ...previous, clientId: cb.clientId, hostId }
|
|
327
341
|
: { version: 1, issuer, hostId, clientId: cb.clientId };
|
|
328
342
|
writeStoredSignIn(opts.file, registration);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
343
|
+
let data;
|
|
344
|
+
try {
|
|
345
|
+
data = await tokenRequest(d.token_endpoint, {
|
|
346
|
+
grant_type: 'authorization_code',
|
|
347
|
+
client_id: cb.clientId,
|
|
348
|
+
code: cb.code,
|
|
349
|
+
code_verifier: verifier,
|
|
350
|
+
redirect_uri: listener.redirectUri,
|
|
351
|
+
resource: OPENAI_API_RESOURCE,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
// OpenAI's consent page refuses a plan or workspace without plan usage
|
|
356
|
+
// for apps, and the exchange then answers invalid_grant.
|
|
357
|
+
if (err instanceof SignInError && err.code === 'invalid_grant')
|
|
358
|
+
throw new SignInError('invalid_grant', signInRefused('invalid_grant'));
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
337
361
|
const subject = await verifyIdToken(data.id_token, d, cb.clientId, nonce);
|
|
338
362
|
const fields = tokenFields(data);
|
|
339
363
|
const planUsage = fields.scopes.includes(PLAN_SCOPE);
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* times and the file's path.
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { chatGptSignInFile } from '../paths.js';
|
|
26
|
+
import { credentialEnv } from '../models/credentialSource.js';
|
|
26
27
|
/** Where the credential file lives, when not the default (a path; never a value). */
|
|
27
28
|
export const CHATGPT_SIGNIN_FILE_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN_FILE';
|
|
28
29
|
/** `off` ignores a stored sign-in (a served process on a machine that also signed in). */
|
|
@@ -31,8 +32,7 @@ export const CHATGPT_SIGNIN_ENV = 'MELCHIZEDEK_CHATGPT_SIGNIN';
|
|
|
31
32
|
export const PLAN_SCOPE = 'chatgpt.tokens.use.direct';
|
|
32
33
|
/** The credential file: MELCHIZEDEK_CHATGPT_SIGNIN_FILE, else ~/.melchizedek/chatgpt-signin.json. */
|
|
33
34
|
export function signInFile(env = process.env) {
|
|
34
|
-
|
|
35
|
-
return set ? resolve(set) : join(homedir(), '.melchizedek', 'chatgpt-signin.json');
|
|
35
|
+
return chatGptSignInFile(env);
|
|
36
36
|
}
|
|
37
37
|
/** The git work tree a path sits inside, if any (an ancestor holding `.git`). */
|
|
38
38
|
export function insideGitWorkTree(file) {
|
|
@@ -100,7 +100,7 @@ export function chatGptSignInStatus(env = process.env) {
|
|
|
100
100
|
* OPENAI_PLATFORM other than direct, no OPENAI_BASE_URL proxy, which must
|
|
101
101
|
* never receive the token).
|
|
102
102
|
*/
|
|
103
|
-
export function chatGptSignInRoutesOpenAi(env =
|
|
103
|
+
export function chatGptSignInRoutesOpenAi(env = credentialEnv()) {
|
|
104
104
|
if (env.OPENAI_API_KEY?.trim())
|
|
105
105
|
return false;
|
|
106
106
|
const platform = env.OPENAI_PLATFORM?.trim().toLowerCase();
|
package/dist/lib/compile.js
CHANGED
|
@@ -42,7 +42,7 @@ import { REASONING_OLDER_SPELLING } from './syndicateSchema.js';
|
|
|
42
42
|
import { reasoningConfig } from './models/reasoning.js';
|
|
43
43
|
import { instructionFor, toEngineAgent } from './agentDialect.js';
|
|
44
44
|
import { resolveTools as resolveNamedTools } from './toolRegistry.js';
|
|
45
|
-
import { createMcpTools } from './tools/mcpToolFactory.js';
|
|
45
|
+
import { createMcpTools, loadStdioMcpTools } from './tools/mcpToolFactory.js';
|
|
46
46
|
import { requireApproval, toolOf } from './tools/tool.js';
|
|
47
47
|
import { examplesInstructionTool } from './tools/examples.js';
|
|
48
48
|
import { capabilitySummary, describeCapabilities } from './models/capabilities.js';
|
|
@@ -145,6 +145,17 @@ export function examplesTool(examples) {
|
|
|
145
145
|
const tool = examplesInstructionTool(examples);
|
|
146
146
|
return tool ? [tool] : [];
|
|
147
147
|
}
|
|
148
|
+
/** A stdio server's tools (ADR 0132), narrowed to the names the entry lists. */
|
|
149
|
+
async function stdioOfferedBy(server, opts) {
|
|
150
|
+
const label = `${server.name} (stdio: ${server.command})`;
|
|
151
|
+
opts.log?.(`Loading MCP tools: ${label}`);
|
|
152
|
+
const offered = await loadStdioMcpTools(server.name, { command: server.command, args: server.args, env: server.env, pass_env: server.pass_env, cwd: server.cwd });
|
|
153
|
+
const allowed = new Set(server.tools);
|
|
154
|
+
const missing = server.tools.filter((n) => !offered.some((t) => t.name === n));
|
|
155
|
+
if (missing.length)
|
|
156
|
+
opts.log?.(`MCP ${label} does not offer ${missing.map((n) => `'${n}'`).join(', ')}`);
|
|
157
|
+
return offered.filter((t) => allowed.has(t.name));
|
|
158
|
+
}
|
|
148
159
|
async function resolveAgentTools(agent, opts) {
|
|
149
160
|
const tools = [...resolveNamedTools(agent.tools, opts.onUnknownTool), ...examplesTool(agent.examples)];
|
|
150
161
|
// OpenAPI operations become tools here, so require_approval can name them.
|
|
@@ -185,7 +196,9 @@ async function resolveAgentTools(agent, opts) {
|
|
|
185
196
|
// mcp_servers (ADR 0124): the loader refused a declared name twice; a
|
|
186
197
|
// server that answers with a name already taken is refused here too, so
|
|
187
198
|
// no server's tool can stand in for another's or the agent's own.
|
|
188
|
-
const servers = await Promise.all((agent.mcp_servers ?? []).map((server) =>
|
|
199
|
+
const servers = await Promise.all((agent.mcp_servers ?? []).map((server) => server.command !== undefined
|
|
200
|
+
? stdioOfferedBy(server, opts)
|
|
201
|
+
: offeredBy(`${server.name} (${server.url})`, server.url, server.tools, server.auth?.oauth2, server.transport)));
|
|
189
202
|
for (const [i, offered] of servers.entries()) {
|
|
190
203
|
const server = agent.mcp_servers[i];
|
|
191
204
|
for (const mcpTool of offered) {
|
|
@@ -215,6 +215,7 @@ export function workflowSubagentOf(workflow) {
|
|
|
215
215
|
...(run.memory ? { memory: run.memory } : {}),
|
|
216
216
|
...(run.log ? { log: run.log } : {}),
|
|
217
217
|
...(run.credentials ? { credentials: run.credentials } : {}),
|
|
218
|
+
...(run.consent ? { consent: run.consent } : {}),
|
|
218
219
|
}),
|
|
219
220
|
};
|
|
220
221
|
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type { MemoryEntry, MemoryIngestOptions, MemorySearchRequest, MemorySearc
|
|
|
24
24
|
export type { TurnEvent } from './runtime/events.ts';
|
|
25
25
|
export { approvalResponsePart, pendingApproval, APPROVAL_REQUEST } from './runtime/approvals.ts';
|
|
26
26
|
export type { PendingApproval } from './runtime/approvals.ts';
|
|
27
|
-
export type { DrainedRun, MessagePart, TurnUsage, RouteDecision, SyndicateTurnOptions, SyndicateTurnResult, TraceOptions, TurnEvents, TurnStage, } from './runtime/syndicateTurn.ts';
|
|
27
|
+
export type { AgentBranch, DrainedRun, MessagePart, TurnUsage, RouteDecision, SyndicateTurnOptions, SyndicateTurnResult, TraceOptions, TurnEvents, TurnStage, } from './runtime/syndicateTurn.ts';
|
|
28
28
|
export type { CompileOptions } from './compile.ts';
|
|
29
29
|
export { isWorkflowSyndicate, describeInput } from './workflow.ts';
|
|
30
30
|
export type { WorkflowConfig, WorkflowNodeYaml, EdgeElement, PendingInput } from './workflow.ts';
|
|
@@ -57,3 +57,5 @@ export type { MemoryStore } from './memory/store.ts';
|
|
|
57
57
|
export { validateSyndicateConfig, syndicateJsonSchema, SyndicateValidationError } from './syndicateSchema.ts';
|
|
58
58
|
export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logProviderStatuses, resolveModel, describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.ts';
|
|
59
59
|
export { loadEnv } from './loadEnv.ts';
|
|
60
|
+
export { setCredentialSource, credentialValue } from './models/credentialSource.ts';
|
|
61
|
+
export type { CredentialSource } from './models/credentialSource.ts';
|
package/dist/lib/index.js
CHANGED
|
@@ -48,3 +48,5 @@ export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logP
|
|
|
48
48
|
// subpath until 0.16.0.
|
|
49
49
|
describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayProblem, gatewayUsable, GATEWAYS, } from './models/registry.js';
|
|
50
50
|
export { loadEnv } from './loadEnv.js';
|
|
51
|
+
// Where credentials are read, by environment variable name (ADR 0133).
|
|
52
|
+
export { setCredentialSource, credentialValue } from './models/credentialSource.js';
|
|
@@ -261,6 +261,10 @@ function anchorSpecPaths(config, dir) {
|
|
|
261
261
|
for (const entry of agent?.openapi ?? [])
|
|
262
262
|
if (!path.isAbsolute(entry.spec))
|
|
263
263
|
entry.spec = path.resolve(dir, entry.spec);
|
|
264
|
+
// A stdio server's working directory, like a spec, is the YAML's (ADR 0132).
|
|
265
|
+
for (const entry of agent?.mcp_servers ?? [])
|
|
266
|
+
if (entry.cwd && !path.isAbsolute(entry.cwd))
|
|
267
|
+
entry.cwd = path.resolve(dir, entry.cwd);
|
|
264
268
|
};
|
|
265
269
|
anchor(config.orchestrator);
|
|
266
270
|
for (const sub of config.subagents ?? [])
|
|
@@ -37,6 +37,7 @@ import { providerForModel } from '../models/providerMap.js';
|
|
|
37
37
|
import { resolveModel } from '../models/registry.js';
|
|
38
38
|
import { startEngineSpan, traceLlmGeneration } from '../observability/tracer.js';
|
|
39
39
|
import { trimTrailingSlashes } from '../models/urls.js';
|
|
40
|
+
import { credentialEnv } from '../models/credentialSource.js';
|
|
40
41
|
/**
|
|
41
42
|
* Extraction on any model id. The key passed here reaches only a model of
|
|
42
43
|
* the matching provider (resolveModel's BYOK rule); other providers read
|
|
@@ -194,7 +195,7 @@ export const DEFAULT_EMBEDDING_MODELS = {
|
|
|
194
195
|
* already passes for memory; it is used only when the configured extraction
|
|
195
196
|
* model or embedder is Gemini.
|
|
196
197
|
*/
|
|
197
|
-
export function memoryProvidersFromEnv(env =
|
|
198
|
+
export function memoryProvidersFromEnv(env = credentialEnv(), geminiKey) {
|
|
198
199
|
const extractionModel = env.MEMORY_EXTRACTION_MODEL?.trim() || MEMORY_EXTRACTION_MODEL;
|
|
199
200
|
const extractor = modelExtractor({
|
|
200
201
|
model: extractionModel,
|
|
@@ -47,6 +47,7 @@ import { currentTurnStart, providerStateOf, withProviderState } from './provider
|
|
|
47
47
|
import { errorDecision, errorText } from './errorResponse.js';
|
|
48
48
|
import { adaptiveThinkingFor, claudeGeneration, claudeReasoningOf, claudeUrlImagesOn, THINKING_BINDING_BETA } from './claudeModels.js';
|
|
49
49
|
import { isAboveHigh } from './reasoning.js';
|
|
50
|
+
import { credentialValue } from './credentialSource.js';
|
|
50
51
|
/** The provider id this adapter reports and writes its state under (lib/models/providerMap.ts). */
|
|
51
52
|
export const ANTHROPIC_PROVIDER = 'anthropic';
|
|
52
53
|
/** The providerState kind this adapter writes: the signed blocks that preceded a part. */
|
|
@@ -217,7 +218,7 @@ export class ClaudeAdapter {
|
|
|
217
218
|
yield setupFailure('ENDPOINT_MISCONFIGURED', messageOf(err));
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
|
-
const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey ||
|
|
221
|
+
const spec = claudeClientSpec(endpoint, this.#apiKey || endpoint.apiKey || credentialValue('ANTHROPIC_API_KEY'));
|
|
221
222
|
if ('error' in spec) {
|
|
222
223
|
yield setupFailure(endpoint.platform === 'direct' ? 'MISSING_API_KEY' : 'ENDPOINT_MISCONFIGURED', spec.error);
|
|
223
224
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/credentialSource.ts — where the engine reads a credential by
|
|
3
|
+
* its environment variable name (ADR 0133).
|
|
4
|
+
*
|
|
5
|
+
* By default every provider key, gateway key and embedding key is read from
|
|
6
|
+
* `process.env`. An embedding application that holds keys elsewhere (an OS
|
|
7
|
+
* keychain, a secret manager) sets a source once:
|
|
8
|
+
*
|
|
9
|
+
* setCredentialSource((name) => keychain.get(name));
|
|
10
|
+
*
|
|
11
|
+
* and every read below asks the source first, by the same names the
|
|
12
|
+
* environment uses (ANTHROPIC_API_KEY, MODEL_GATEWAY_API_KEY, …), at the
|
|
13
|
+
* moment of use: nothing is cached, so a key the user removes stops working
|
|
14
|
+
* on the next call. A source that returns undefined for a name falls back to
|
|
15
|
+
* the environment. A dependency-free leaf, so providerMap.ts stays one.
|
|
16
|
+
*/
|
|
17
|
+
/** Returns the credential held under an environment variable name, or undefined. Must not throw. */
|
|
18
|
+
export type CredentialSource = (name: string) => string | undefined;
|
|
19
|
+
/** Sets (or, with undefined, clears) the process's credential source. */
|
|
20
|
+
export declare function setCredentialSource(next: CredentialSource | undefined): void;
|
|
21
|
+
/** The credential named `name`: the source's, else `env`'s. A source that throws counts as holding nothing. */
|
|
22
|
+
export declare function credentialValue(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* `env` as the engine's env-reading helpers see it: a read by name asks the
|
|
25
|
+
* source first. For functions that take an `env` parameter; enumeration and
|
|
26
|
+
* spreading see only `env`'s own variables.
|
|
27
|
+
*/
|
|
28
|
+
export declare function credentialEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|