amalgm 0.0.0 → 0.0.32
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/README.md +41 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1132 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +476 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +30 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +315 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +136 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +140 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
- package/runtime/scripts/chat-core/adapters/claude.js +165 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +328 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +253 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +131 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +854 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { calculateCost } = require('../chat-server/model-catalog');
|
|
4
|
+
const {
|
|
5
|
+
PROXY_BASE_URL,
|
|
6
|
+
PROXY_TOKEN,
|
|
7
|
+
ensureFreshProxyToken,
|
|
8
|
+
readProxyToken,
|
|
9
|
+
} = require('../chat-server/config');
|
|
10
|
+
|
|
11
|
+
function n(value) {
|
|
12
|
+
const parsed = Number(value || 0);
|
|
13
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeTokenList(input = {}) {
|
|
17
|
+
const tokenList = input.tokenList && typeof input.tokenList === 'object' ? input.tokenList : {};
|
|
18
|
+
const inputTokens = n(tokenList.input ?? tokenList.inputTokens ?? input.inputTokens);
|
|
19
|
+
const outputTokens = n(tokenList.output ?? tokenList.outputTokens ?? input.outputTokens);
|
|
20
|
+
const cacheReadTokens = n(tokenList.cacheRead ?? tokenList.cacheReadTokens ?? input.cacheReadTokens);
|
|
21
|
+
const cacheWriteTokens = n(tokenList.cacheWrite ?? tokenList.cacheWriteTokens ?? input.cacheWriteTokens);
|
|
22
|
+
const thoughtTokens = n(tokenList.thought ?? tokenList.thoughtTokens ?? input.thoughtTokens);
|
|
23
|
+
const computedTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
24
|
+
const total = nonNegativeOrNull(tokenList.total ?? tokenList.totalTokens ?? input.totalTokens)
|
|
25
|
+
?? computedTotal;
|
|
26
|
+
return {
|
|
27
|
+
input: inputTokens,
|
|
28
|
+
output: outputTokens,
|
|
29
|
+
cacheRead: cacheReadTokens,
|
|
30
|
+
cacheWrite: cacheWriteTokens,
|
|
31
|
+
thought: thoughtTokens,
|
|
32
|
+
total,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasTokenListBuckets(tokenList = {}) {
|
|
37
|
+
return ['input', 'output', 'cacheRead', 'cacheWrite', 'thought', 'total']
|
|
38
|
+
.some((key) => Number.isFinite(Number(tokenList[key])));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function zeroTokenList() {
|
|
42
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thought: 0, total: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addTokenLists(a = {}, b = {}) {
|
|
46
|
+
return normalizeTokenList({
|
|
47
|
+
inputTokens: n(a.input) + n(b.input),
|
|
48
|
+
outputTokens: n(a.output) + n(b.output),
|
|
49
|
+
cacheReadTokens: n(a.cacheRead) + n(b.cacheRead),
|
|
50
|
+
cacheWriteTokens: n(a.cacheWrite) + n(b.cacheWrite),
|
|
51
|
+
thoughtTokens: n(a.thought) + n(b.thought),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function subtractTokenLists(after = {}, before = {}) {
|
|
56
|
+
return normalizeTokenList({
|
|
57
|
+
inputTokens: Math.max(0, n(after.input) - n(before.input)),
|
|
58
|
+
outputTokens: Math.max(0, n(after.output) - n(before.output)),
|
|
59
|
+
cacheReadTokens: Math.max(0, n(after.cacheRead) - n(before.cacheRead)),
|
|
60
|
+
cacheWriteTokens: Math.max(0, n(after.cacheWrite) - n(before.cacheWrite)),
|
|
61
|
+
thoughtTokens: Math.max(0, n(after.thought) - n(before.thought)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeUsage(input = {}) {
|
|
66
|
+
const cacheRead = n(input.cacheReadTokens ?? input.cachedInputTokens ?? input.cache_read_input_tokens);
|
|
67
|
+
const cacheWrite = n(input.cacheWriteTokens ?? input.cacheCreationInputTokens ?? input.cache_creation_input_tokens);
|
|
68
|
+
const output = n(input.outputTokens ?? input.completionTokens ?? input.output_tokens);
|
|
69
|
+
const thought = n(input.thoughtTokens ?? input.reasoningTokens);
|
|
70
|
+
const totalInput = input.totalInputTokens ?? input.promptTokens ?? (input.inputTokensAreTotal ? input.inputTokens : null);
|
|
71
|
+
const nonCachedInput = totalInput == null
|
|
72
|
+
? n(input.inputTokens ?? input.input_tokens ?? input.nonCachedInputTokens)
|
|
73
|
+
: Math.max(0, n(totalInput) - cacheRead - cacheWrite);
|
|
74
|
+
const tokenList = normalizeTokenList({
|
|
75
|
+
tokenList: input.tokenList,
|
|
76
|
+
inputTokens: nonCachedInput,
|
|
77
|
+
outputTokens: output,
|
|
78
|
+
cacheReadTokens: cacheRead,
|
|
79
|
+
cacheWriteTokens: cacheWrite,
|
|
80
|
+
thoughtTokens: thought,
|
|
81
|
+
});
|
|
82
|
+
const inputContextSize = nonNegativeOrNull(input.inputContextSize ?? input.input_context_size)
|
|
83
|
+
?? tokenList.total;
|
|
84
|
+
return {
|
|
85
|
+
tokenList,
|
|
86
|
+
cumulativeTokenList: input.cumulativeTokenList && typeof input.cumulativeTokenList === 'object'
|
|
87
|
+
? normalizeTokenList({ tokenList: input.cumulativeTokenList })
|
|
88
|
+
: null,
|
|
89
|
+
previousCumulativeTokenList: input.previousCumulativeTokenList && typeof input.previousCumulativeTokenList === 'object'
|
|
90
|
+
? normalizeTokenList({ tokenList: input.previousCumulativeTokenList })
|
|
91
|
+
: null,
|
|
92
|
+
inputTokens: nonCachedInput,
|
|
93
|
+
outputTokens: output,
|
|
94
|
+
cacheReadTokens: cacheRead,
|
|
95
|
+
cacheWriteTokens: cacheWrite,
|
|
96
|
+
thoughtTokens: thought,
|
|
97
|
+
inputContextSize,
|
|
98
|
+
contextUsedTokens: positiveOrNull(input.contextUsedTokens ?? input.usedTokens ?? inputContextSize),
|
|
99
|
+
contextLimit: positiveOrNull(input.contextLimit ?? input.maxTokens ?? input.modelContextWindow),
|
|
100
|
+
usedTokens: positiveOrNull(input.usedTokens ?? input.contextUsedTokens ?? inputContextSize),
|
|
101
|
+
lastUsedTokens: positiveOrNull(input.lastUsedTokens ?? input.contextUsedTokens ?? input.usedTokens ?? inputContextSize),
|
|
102
|
+
maxTokens: positiveOrNull(input.maxTokens ?? input.contextLimit ?? input.modelContextWindow),
|
|
103
|
+
totalProcessedTokens: positiveOrNull(input.totalProcessedTokens),
|
|
104
|
+
costUsd: input.costUsd ?? input.costUSD ?? input.cost ?? null,
|
|
105
|
+
marketCostUsd: input.marketCostUsd ?? null,
|
|
106
|
+
operation: String(input.operation || input.usageOperation || 'chat_step'),
|
|
107
|
+
billable: input.billable !== false,
|
|
108
|
+
source: input.source || 'runtime',
|
|
109
|
+
exact: input.exact === true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function positiveOrNull(value) {
|
|
114
|
+
const parsed = Number(value);
|
|
115
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function nonNegativeOrNull(value) {
|
|
119
|
+
const parsed = Number(value);
|
|
120
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function totalUsageTokens(usage = {}) {
|
|
124
|
+
return n(usage.tokenList?.total)
|
|
125
|
+
|| n(usage.inputContextSize)
|
|
126
|
+
|| n(usage.inputTokens) + n(usage.outputTokens) + n(usage.cacheReadTokens) + n(usage.cacheWriteTokens);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function currentProxyToken(options = {}) {
|
|
130
|
+
if (typeof ensureFreshProxyToken === 'function') {
|
|
131
|
+
try {
|
|
132
|
+
const fresh = await ensureFreshProxyToken(options);
|
|
133
|
+
if (fresh) return fresh;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn('[ChatCore:Billing] Proxy token refresh failed:', err.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (typeof readProxyToken === 'function') {
|
|
139
|
+
const stored = readProxyToken();
|
|
140
|
+
if (stored) return stored;
|
|
141
|
+
}
|
|
142
|
+
return PROXY_TOKEN || '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function beginProxyTurn(contract) {
|
|
146
|
+
if (contract.authMethod !== 'amalgm') {
|
|
147
|
+
return { skipped: true, reason: 'not_amalgm' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const baseUrl = String(PROXY_BASE_URL || '').replace(/\/$/, '');
|
|
151
|
+
let token = await currentProxyToken();
|
|
152
|
+
if (!baseUrl || !token) {
|
|
153
|
+
throw Object.assign(new Error('Amalgm proxy billing is not configured'), {
|
|
154
|
+
status: 503,
|
|
155
|
+
code: 'AMALGM_PROXY_NOT_CONFIGURED',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bodyPayload = {
|
|
160
|
+
sessionId: contract.sessionId,
|
|
161
|
+
messageId: contract.assistantMessageId,
|
|
162
|
+
agentId: contract.agentId,
|
|
163
|
+
harnessId: contract.harness,
|
|
164
|
+
authMethod: contract.authMethod,
|
|
165
|
+
usageOwner: contract.usageOwner || 'platform_proxy',
|
|
166
|
+
modelId: contract.usageModelId || contract.modelId,
|
|
167
|
+
internalModelId: contract.modelId,
|
|
168
|
+
cliModel: contract.cliModel,
|
|
169
|
+
contextWindow: contract.contextWindow ?? null,
|
|
170
|
+
maxTokens: contract.maxTokens ?? null,
|
|
171
|
+
cwd: contract.cwd,
|
|
172
|
+
mcpServers: contract.mcpServers,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const requestTurnStart = (authToken) => fetch(`${baseUrl}/internal/turn-start`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
Authorization: `Bearer ${authToken}`,
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify(bodyPayload),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let response = await requestTurnStart(token);
|
|
185
|
+
if (response.status === 401) {
|
|
186
|
+
console.warn('[ChatCore:Billing] turn-start got 401; refreshing proxy token and retrying once');
|
|
187
|
+
token = await currentProxyToken({ force: true });
|
|
188
|
+
if (token) response = await requestTurnStart(token);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const body = await response.json().catch(() => null);
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const message = body?.message || body?.error || `Amalgm turn start failed (${response.status})`;
|
|
194
|
+
throw Object.assign(new Error(message), {
|
|
195
|
+
status: response.status,
|
|
196
|
+
code: body?.error || 'AMALGM_TURN_START_FAILED',
|
|
197
|
+
balanceUsd: body?.balanceUsd,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return body || { ok: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function providerFromModel(modelId) {
|
|
205
|
+
return typeof modelId === 'string' && modelId.includes('/') ? modelId.split('/')[0] : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function usageFromTokenList(tokenList, base = {}) {
|
|
209
|
+
const normalized = normalizeTokenList({ tokenList });
|
|
210
|
+
return normalizeUsage({
|
|
211
|
+
...base,
|
|
212
|
+
tokenList: normalized,
|
|
213
|
+
inputTokens: normalized.input,
|
|
214
|
+
outputTokens: normalized.output,
|
|
215
|
+
cacheReadTokens: normalized.cacheRead,
|
|
216
|
+
cacheWriteTokens: normalized.cacheWrite,
|
|
217
|
+
thoughtTokens: normalized.thought,
|
|
218
|
+
inputContextSize: base.inputContextSize ?? normalized.total,
|
|
219
|
+
operation: 'chat_turn',
|
|
220
|
+
billable: true,
|
|
221
|
+
exact: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function summedTurnUsage(records, base = {}) {
|
|
226
|
+
const sum = records.reduce((acc, entry) => addTokenLists(acc, entry.tokenList), zeroTokenList());
|
|
227
|
+
if (!hasTokenListBuckets(sum) || sum.total <= 0) return null;
|
|
228
|
+
return usageFromTokenList(sum, {
|
|
229
|
+
...base,
|
|
230
|
+
source: base.source || 'turn_sum',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function codexTurnUsage(records, base = {}) {
|
|
235
|
+
const cumulativeRecords = records.filter((entry) => (
|
|
236
|
+
entry?.cumulativeTokenList
|
|
237
|
+
&& hasTokenListBuckets(entry.cumulativeTokenList)
|
|
238
|
+
&& entry.cumulativeTokenList.total > 0
|
|
239
|
+
));
|
|
240
|
+
if (cumulativeRecords.length > 0) {
|
|
241
|
+
const first = cumulativeRecords[0];
|
|
242
|
+
const last = cumulativeRecords[cumulativeRecords.length - 1];
|
|
243
|
+
const baseline = first.previousCumulativeTokenList && hasTokenListBuckets(first.previousCumulativeTokenList)
|
|
244
|
+
? first.previousCumulativeTokenList
|
|
245
|
+
: zeroTokenList();
|
|
246
|
+
const delta = subtractTokenLists(last.cumulativeTokenList, baseline);
|
|
247
|
+
if (delta.total > 0) {
|
|
248
|
+
return usageFromTokenList(delta, {
|
|
249
|
+
...base,
|
|
250
|
+
inputContextSize: last.inputContextSize,
|
|
251
|
+
contextLimit: last.contextLimit,
|
|
252
|
+
totalProcessedTokens: last.cumulativeTokenList.total,
|
|
253
|
+
source: 'codex_app_server_turn_delta',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return summedTurnUsage(records.filter((entry) => entry.exact === true), {
|
|
258
|
+
...base,
|
|
259
|
+
source: 'codex_app_server_step_sum',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function finalizeTurnUsage({ contract, usage, usageRecords } = {}) {
|
|
264
|
+
const records = Array.isArray(usageRecords) && usageRecords.length > 0
|
|
265
|
+
? usageRecords
|
|
266
|
+
: usage
|
|
267
|
+
? [usage]
|
|
268
|
+
: [];
|
|
269
|
+
|
|
270
|
+
if (contract?.harness === 'codex') {
|
|
271
|
+
const codexUsage = codexTurnUsage(records, {
|
|
272
|
+
contextLimit: usage?.contextLimit,
|
|
273
|
+
inputContextSize: usage?.inputContextSize,
|
|
274
|
+
costUsd: null,
|
|
275
|
+
marketCostUsd: null,
|
|
276
|
+
});
|
|
277
|
+
if (codexUsage) return codexUsage;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const billableRecords = records.filter((entry) => entry && entry.billable !== false);
|
|
281
|
+
if (billableRecords.length === 0) return null;
|
|
282
|
+
|
|
283
|
+
const base = {
|
|
284
|
+
contextLimit: usage?.contextLimit,
|
|
285
|
+
inputContextSize: usage?.inputContextSize,
|
|
286
|
+
costUsd: null,
|
|
287
|
+
marketCostUsd: null,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const exactTurnRecord = billableRecords.find((entry) => (
|
|
291
|
+
entry.exact === true && entry.operation === 'chat_turn' && entry.tokenList?.total > 0
|
|
292
|
+
));
|
|
293
|
+
if (exactTurnRecord) {
|
|
294
|
+
return usageFromTokenList(exactTurnRecord.tokenList, {
|
|
295
|
+
...base,
|
|
296
|
+
inputContextSize: exactTurnRecord.inputContextSize,
|
|
297
|
+
contextLimit: exactTurnRecord.contextLimit,
|
|
298
|
+
totalProcessedTokens: exactTurnRecord.totalProcessedTokens,
|
|
299
|
+
source: exactTurnRecord.source || 'native_chat_turn',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return summedTurnUsage(billableRecords.filter((entry) => entry.exact === true), base);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function logTurnUsage({ db, contract, messageId, usage, usageRecords }) {
|
|
306
|
+
if (contract.authMethod === 'amalgm') {
|
|
307
|
+
return { skipped: true, source: 'proxy_owned_provider_usage', reason: 'proxy_persists_usage_immediately' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const turnUsage = finalizeTurnUsage({ contract, usage, usageRecords });
|
|
311
|
+
if (!turnUsage) return { skipped: true, reason: 'no_turn_usage' };
|
|
312
|
+
const marketCostUsd = turnUsage.marketCostUsd ?? calculateCost(
|
|
313
|
+
contract.usageModelId || contract.modelId,
|
|
314
|
+
turnUsage.inputTokens,
|
|
315
|
+
turnUsage.outputTokens,
|
|
316
|
+
turnUsage.cacheReadTokens,
|
|
317
|
+
turnUsage.cacheWriteTokens,
|
|
318
|
+
);
|
|
319
|
+
await db.logUsage({
|
|
320
|
+
userId: contract.userId,
|
|
321
|
+
sessionId: contract.sessionId,
|
|
322
|
+
messageId,
|
|
323
|
+
agentId: contract.agentId,
|
|
324
|
+
harnessId: contract.harness,
|
|
325
|
+
authMethod: contract.authMethod,
|
|
326
|
+
modelId: contract.usageModelId || contract.modelId,
|
|
327
|
+
usage: turnUsage,
|
|
328
|
+
marketCostUsd,
|
|
329
|
+
});
|
|
330
|
+
return { skipped: false, source: 'local_turn_exact', count: 1 };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
addTokenLists,
|
|
335
|
+
beginProxyTurn,
|
|
336
|
+
finalizeTurnUsage,
|
|
337
|
+
logTurnUsage,
|
|
338
|
+
normalizeTokenList,
|
|
339
|
+
normalizeUsage,
|
|
340
|
+
providerFromModel,
|
|
341
|
+
subtractTokenLists,
|
|
342
|
+
totalUsageTokens,
|
|
343
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration — env vars, constants, proxy URLs.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all chat-server configuration.
|
|
5
|
+
* No business logic here — just read env, compute derived values, export.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const {
|
|
12
|
+
DEFAULT_PROXY_BASE_URL,
|
|
13
|
+
ensureFreshProxyToken,
|
|
14
|
+
proxyBaseUrl,
|
|
15
|
+
readProxyToken,
|
|
16
|
+
} = require('../proxy-token-store');
|
|
17
|
+
|
|
18
|
+
// ── Paths ──────────────────────────────────────────────────────────────────
|
|
19
|
+
// Set by the orchestrator (entrypoint.sh or electron/main.ts).
|
|
20
|
+
// The code never decides these — it just reads them.
|
|
21
|
+
|
|
22
|
+
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
23
|
+
const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || '/workspace';
|
|
24
|
+
|
|
25
|
+
// ── Server ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const PORT = parseInt(process.env.CHAT_SERVER_PORT || '8084', 10);
|
|
28
|
+
const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
|
|
29
|
+
|
|
30
|
+
// ── API keys / proxy ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const RAW_OPENAI_API_KEY = process.env.OPENAI_API_KEY || '';
|
|
33
|
+
const OPENAI_API_KEY_TITLE = /^sk-/.test(RAW_OPENAI_API_KEY) ? RAW_OPENAI_API_KEY : '';
|
|
34
|
+
const AI_GATEWAY_API_KEY_TITLE = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_AI_GATEWAY_API_KEY || '';
|
|
35
|
+
const AI_GATEWAY_URL_TITLE = process.env.AI_GATEWAY_URL || 'https://ai-gateway.vercel.sh';
|
|
36
|
+
|
|
37
|
+
// All sensitive keys live on the API proxy (Fly.io). Containers get an HMAC proxy token.
|
|
38
|
+
const PROXY_TOKEN = readProxyToken()
|
|
39
|
+
|| (process.env.AMALGM_RUNTIME_SOURCE === 'npm' ? '' : process.env.ANTHROPIC_API_KEY || '');
|
|
40
|
+
const PROXY_BASE_URL = proxyBaseUrl() || (PROXY_TOKEN ? DEFAULT_PROXY_BASE_URL : '');
|
|
41
|
+
const OPENAI_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/openai` : '';
|
|
42
|
+
const AI_GATEWAY_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/ai_gateway/v1` : '';
|
|
43
|
+
const SUPABASE_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/supabase` : '';
|
|
44
|
+
|
|
45
|
+
// ── Supabase ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const SUPABASE_URL_DIRECT = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || '';
|
|
48
|
+
const SUPABASE_KEY_DIRECT = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
|
|
49
|
+
|
|
50
|
+
// Prefer direct Supabase when service-role creds are available (local dev / tests).
|
|
51
|
+
// The proxy is only needed when SUPABASE_KEY_DIRECT is unavailable (in-container runs).
|
|
52
|
+
const SUPABASE_USES_DIRECT = !!(SUPABASE_URL_DIRECT && SUPABASE_KEY_DIRECT);
|
|
53
|
+
const SUPABASE_URL = SUPABASE_USES_DIRECT ? SUPABASE_URL_DIRECT : (SUPABASE_PROXY_URL || SUPABASE_URL_DIRECT);
|
|
54
|
+
const SUPABASE_AUTH_HEADERS = SUPABASE_USES_DIRECT
|
|
55
|
+
? { apikey: SUPABASE_KEY_DIRECT, Authorization: `Bearer ${SUPABASE_KEY_DIRECT}` }
|
|
56
|
+
: (SUPABASE_PROXY_URL
|
|
57
|
+
? { Authorization: `Bearer ${PROXY_TOKEN}` }
|
|
58
|
+
: { apikey: SUPABASE_KEY_DIRECT, Authorization: `Bearer ${SUPABASE_KEY_DIRECT}` });
|
|
59
|
+
|
|
60
|
+
// ── Platform context (injected into every prompt) ───────────────────────────
|
|
61
|
+
|
|
62
|
+
let PLATFORM_CONTEXT = '';
|
|
63
|
+
try {
|
|
64
|
+
const ctxPath = path.join(__dirname, '..', 'platform-context.txt');
|
|
65
|
+
if (fs.existsSync(ctxPath)) {
|
|
66
|
+
PLATFORM_CONTEXT = fs.readFileSync(ctxPath, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Fall through — will use empty string
|
|
70
|
+
}
|
|
71
|
+
// Wrap in XML tags if loaded from file and not already wrapped
|
|
72
|
+
if (PLATFORM_CONTEXT && !PLATFORM_CONTEXT.startsWith('<amalgm-platform>')) {
|
|
73
|
+
PLATFORM_CONTEXT = `<amalgm-platform>\n${PLATFORM_CONTEXT}\n</amalgm-platform>\n\n`;
|
|
74
|
+
} else if (PLATFORM_CONTEXT) {
|
|
75
|
+
PLATFORM_CONTEXT = PLATFORM_CONTEXT + '\n\n';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Startup log ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
console.log('[ChatServer] Config:', {
|
|
81
|
+
port: PORT,
|
|
82
|
+
bindHost: BIND_HOST,
|
|
83
|
+
proxyConfigured: !!PROXY_BASE_URL,
|
|
84
|
+
proxyBase: PROXY_BASE_URL || '(none)',
|
|
85
|
+
supabaseVia: SUPABASE_USES_DIRECT ? 'direct' : (SUPABASE_PROXY_URL ? 'proxy' : 'direct'),
|
|
86
|
+
titleVia: AI_GATEWAY_API_KEY_TITLE ? 'vercel_ai_sdk_gateway' : (AI_GATEWAY_PROXY_URL && PROXY_TOKEN ? 'ai_gateway_proxy' : (OPENAI_API_KEY_TITLE ? 'direct_openai' : 'disabled')),
|
|
87
|
+
amalgmDir: AMALGM_DIR,
|
|
88
|
+
defaultCwd: DEFAULT_CWD,
|
|
89
|
+
hasDirectOpenaiKey: !!OPENAI_API_KEY_TITLE,
|
|
90
|
+
hasPlatformContext: PLATFORM_CONTEXT.length > 0,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
PORT,
|
|
95
|
+
BIND_HOST,
|
|
96
|
+
OPENAI_API_KEY_TITLE,
|
|
97
|
+
AI_GATEWAY_API_KEY_TITLE,
|
|
98
|
+
AI_GATEWAY_URL_TITLE,
|
|
99
|
+
PROXY_TOKEN,
|
|
100
|
+
ensureFreshProxyToken,
|
|
101
|
+
readProxyToken,
|
|
102
|
+
PROXY_BASE_URL,
|
|
103
|
+
OPENAI_PROXY_URL,
|
|
104
|
+
AI_GATEWAY_PROXY_URL,
|
|
105
|
+
SUPABASE_URL,
|
|
106
|
+
SUPABASE_AUTH_HEADERS,
|
|
107
|
+
AMALGM_DIR,
|
|
108
|
+
DEFAULT_CWD,
|
|
109
|
+
PLATFORM_CONTEXT,
|
|
110
|
+
};
|