mixdog 0.7.16 → 0.7.18
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/UNINSTALL.md +7 -4
- package/bin/statusline-lib.mjs +29 -6
- package/bin/statusline-route.mjs +273 -0
- package/bin/statusline.mjs +31 -8
- package/commands/model.md +61 -0
- package/hooks/session-start.cjs +15 -1
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/rules/lead/01-general.md +3 -0
- package/scripts/gateway-model.mjs +596 -0
- package/scripts/lib/gateway-inventory.mjs +178 -0
- package/scripts/lib/gateway-settings.mjs +78 -0
- package/scripts/run-mcp.mjs +26 -1
- package/scripts/statusline-launcher-smoke.mjs +155 -2
- package/scripts/uninstall.mjs +44 -7
- package/server-main.mjs +252 -11
- package/setup/setup.html +1 -1
- package/src/agent/index.mjs +96 -5
- package/src/agent/orchestrator/bridge-trace.mjs +18 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +4 -1
- package/src/agent/orchestrator/providers/anthropic.mjs +6 -2
- package/src/agent/orchestrator/session/loop.mjs +145 -4
- package/src/agent/orchestrator/session/trim.mjs +1 -1
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +62 -6
- package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
- package/src/agent/tool-defs.mjs +10 -3
- package/src/channels/lib/runtime-paths.mjs +39 -4
- package/src/gateway/claude-current.mjs +255 -0
- package/src/gateway/oauth-usage.mjs +598 -0
- package/src/gateway/route-meta.mjs +629 -0
- package/src/gateway/server.mjs +713 -0
- package/src/shared/llm/cost.mjs +1 -1
- package/src/shared/seed.mjs +25 -0
- package/tools.json +21 -3
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { resolvePluginData } from '../shared/plugin-paths.mjs';
|
|
4
|
+
import { readSection } from '../shared/config.mjs';
|
|
5
|
+
import { updateJsonAtomicSync } from '../shared/atomic-file.mjs';
|
|
6
|
+
import { computeCostUsd, isInclusiveProvider } from '../shared/llm/cost.mjs';
|
|
7
|
+
import { getModelMetadataSync } from '../agent/orchestrator/providers/model-catalog.mjs';
|
|
8
|
+
import {
|
|
9
|
+
estimateMessagesTokens,
|
|
10
|
+
estimateRequestReserveTokens,
|
|
11
|
+
trimMessages,
|
|
12
|
+
} from '../agent/orchestrator/session/trim.mjs';
|
|
13
|
+
import { CLAUDE_CURRENT_MODE } from './claude-current.mjs';
|
|
14
|
+
|
|
15
|
+
const GATEWAY_USAGE_FILE = 'gateway-usage.local.json';
|
|
16
|
+
const MAX_USAGE_EVENTS = 1000;
|
|
17
|
+
const USAGE_EVENT_TTL_MS = 35 * 24 * 60 * 60_000;
|
|
18
|
+
const OPENCODE_GO_WINDOWS = Object.freeze([
|
|
19
|
+
Object.freeze({ label: '5H', limitUsd: 12, durationMs: 5 * 60 * 60_000, source: 'opencode-go-local' }),
|
|
20
|
+
Object.freeze({ label: '7D', limitUsd: 30, durationMs: 7 * 24 * 60 * 60_000, source: 'opencode-go-local' }),
|
|
21
|
+
Object.freeze({ label: 'M', limitUsd: 60, durationMs: 30 * 24 * 60 * 60_000, source: 'opencode-go-local' }),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
let routeSectionKey = null;
|
|
25
|
+
let routeSectionStartedAt = 0;
|
|
26
|
+
|
|
27
|
+
function num(value, fallback = 0) {
|
|
28
|
+
if (value === null || value === undefined || value === '') return fallback;
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
return Number.isFinite(n) ? n : fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function round(value, digits = 6) {
|
|
34
|
+
const n = Number(value);
|
|
35
|
+
if (!Number.isFinite(n)) return null;
|
|
36
|
+
const scale = 10 ** digits;
|
|
37
|
+
return Math.round(n * scale) / scale;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasOwn(obj, key) {
|
|
41
|
+
return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJsonFile(file) {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(file)) return null;
|
|
47
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function agentConfigSection() {
|
|
54
|
+
const raw = readSection('agent') || {};
|
|
55
|
+
return raw?.agent?.providers ? raw.agent : raw;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function gatewaySection() {
|
|
59
|
+
return readSection('gateway') || {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function providerSection(provider) {
|
|
63
|
+
const agent = agentConfigSection();
|
|
64
|
+
return agent?.providers?.[provider] && typeof agent.providers[provider] === 'object'
|
|
65
|
+
? agent.providers[provider]
|
|
66
|
+
: {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function gatewayPresets() {
|
|
70
|
+
const agent = agentConfigSection();
|
|
71
|
+
return Array.isArray(agent?.presets) ? agent.presets : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cleanString(value) {
|
|
75
|
+
const s = typeof value === 'string' ? value.trim() : '';
|
|
76
|
+
return s || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cleanBool(value) {
|
|
80
|
+
if (value === true) return true;
|
|
81
|
+
if (value === false) return false;
|
|
82
|
+
if (typeof value === 'string') {
|
|
83
|
+
const s = value.trim().toLowerCase();
|
|
84
|
+
if (['1', 'true', 'yes', 'on', 'fast', 'priority'].includes(s)) return true;
|
|
85
|
+
if (['0', 'false', 'no', 'off', 'none'].includes(s)) return false;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function providerKind(provider) {
|
|
91
|
+
const p = String(provider || '').toLowerCase();
|
|
92
|
+
if (!p) return 'unknown';
|
|
93
|
+
if (p === 'opencode-go') return 'quota-api';
|
|
94
|
+
if (p.includes('oauth')) return 'oauth';
|
|
95
|
+
if (p === 'ollama' || p === 'lmstudio') return 'local';
|
|
96
|
+
return 'api';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function routeIdentityKey(routeInfo = {}) {
|
|
100
|
+
return [
|
|
101
|
+
routeInfo.provider || '',
|
|
102
|
+
routeInfo.model || '',
|
|
103
|
+
routeInfo.effort || '',
|
|
104
|
+
routeInfo.fast === true ? 'fast' : '',
|
|
105
|
+
].join('\u0001');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ensureRouteSection(routeInfo = {}) {
|
|
109
|
+
const key = routeIdentityKey(routeInfo);
|
|
110
|
+
if (!key.trim()) return null;
|
|
111
|
+
if (routeSectionKey !== key) {
|
|
112
|
+
routeSectionKey = key;
|
|
113
|
+
routeSectionStartedAt = Date.now();
|
|
114
|
+
}
|
|
115
|
+
return routeSectionStartedAt;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shouldTrackDollarSpend(routeInfo = {}) {
|
|
119
|
+
const kind = routeInfo.providerKind || providerKind(routeInfo.provider);
|
|
120
|
+
return kind === 'api' || kind === 'quota-api';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function presetLabel(p) {
|
|
124
|
+
return cleanString(p?.name) || cleanString(p?.id) || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findPresetForRoute(provider, model, gateway) {
|
|
128
|
+
const presets = gatewayPresets().filter(p => p?.provider === provider && p?.model === model);
|
|
129
|
+
if (!presets.length) return null;
|
|
130
|
+
if (cleanString(gateway?.mode) === CLAUDE_CURRENT_MODE) {
|
|
131
|
+
const effort = cleanString(gateway?.effort ?? gateway?.displayEffort);
|
|
132
|
+
const fast = cleanBool(gateway?.fast);
|
|
133
|
+
if (effort || fast !== null) {
|
|
134
|
+
const exact = presets.find(p =>
|
|
135
|
+
(!effort || cleanString(p.effort) === effort) &&
|
|
136
|
+
(fast === null || cleanBool(p.fast) === fast)
|
|
137
|
+
);
|
|
138
|
+
if (exact) return exact;
|
|
139
|
+
}
|
|
140
|
+
return presets.length === 1 ? presets[0] : null;
|
|
141
|
+
}
|
|
142
|
+
const presetId = cleanString(gateway?.presetId);
|
|
143
|
+
const presetName = cleanString(gateway?.presetName);
|
|
144
|
+
if (presetId || presetName) {
|
|
145
|
+
const exact = presets.find(p =>
|
|
146
|
+
(presetId && p.id === presetId) ||
|
|
147
|
+
(presetName && (p.name === presetName || p.id === presetName))
|
|
148
|
+
);
|
|
149
|
+
if (exact) return exact;
|
|
150
|
+
}
|
|
151
|
+
const effort = cleanString(gateway?.effort ?? gateway?.displayEffort);
|
|
152
|
+
const fast = cleanBool(gateway?.fast);
|
|
153
|
+
if (effort || fast !== null) {
|
|
154
|
+
const exact = presets.find(p =>
|
|
155
|
+
(!effort || cleanString(p.effort) === effort) &&
|
|
156
|
+
(fast === null || cleanBool(p.fast) === fast)
|
|
157
|
+
);
|
|
158
|
+
if (exact) return exact;
|
|
159
|
+
}
|
|
160
|
+
return presets.length === 1 ? presets[0] : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadCachedModel(provider, model) {
|
|
164
|
+
const files = [
|
|
165
|
+
`${provider}-models.json`,
|
|
166
|
+
provider === 'openai-oauth' ? 'openai-oauth-models.json' : null,
|
|
167
|
+
provider === 'anthropic-oauth' ? 'anthropic-oauth-models.json' : null,
|
|
168
|
+
provider === 'grok-oauth' ? 'grok-oauth-models.json' : null,
|
|
169
|
+
].filter(Boolean);
|
|
170
|
+
const dataDir = resolvePluginData();
|
|
171
|
+
for (const name of files) {
|
|
172
|
+
const raw = readJsonFile(join(dataDir, name));
|
|
173
|
+
const models = Array.isArray(raw?.models) ? raw.models : Array.isArray(raw) ? raw : [];
|
|
174
|
+
const found = models.find(m => (m?.id || m?.name || m?.slug) === model);
|
|
175
|
+
if (found) return found;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function modelInfoFromProvider(providerObj, model) {
|
|
181
|
+
try {
|
|
182
|
+
const info = providerObj?.getCachedModelInfo?.(model);
|
|
183
|
+
return info && typeof info === 'object' ? info : null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function displayForModel(provider, model, info) {
|
|
190
|
+
const display = cleanString(info?.display) || cleanString(info?.displayName) || cleanString(info?.name);
|
|
191
|
+
if (display) return display;
|
|
192
|
+
if (provider === 'anthropic-oauth') {
|
|
193
|
+
const m = String(model || '').match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/i);
|
|
194
|
+
if (m) return `${m[1][0].toUpperCase()}${m[1].slice(1).toLowerCase()} ${m[2]}.${m[3]}`;
|
|
195
|
+
}
|
|
196
|
+
return model || '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function mergeModelInfo(provider, model, providerObj) {
|
|
200
|
+
const fromProvider = modelInfoFromProvider(providerObj, model);
|
|
201
|
+
const fromCache = loadCachedModel(provider, model);
|
|
202
|
+
const fromCatalog = getModelMetadataSync(model, provider);
|
|
203
|
+
return {
|
|
204
|
+
...(fromCatalog || {}),
|
|
205
|
+
...(fromCache || {}),
|
|
206
|
+
...(fromProvider || {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function readGatewayRouteInfo(seed = {}, providerObj = null) {
|
|
211
|
+
let gateway = {};
|
|
212
|
+
try { gateway = gatewaySection(); } catch { gateway = {}; }
|
|
213
|
+
const mode = cleanString(seed.mode) || cleanString(gateway.mode);
|
|
214
|
+
const inheritClaudeCurrent = mode === CLAUDE_CURRENT_MODE;
|
|
215
|
+
const provider = cleanString(process.env.MIXDOG_GATEWAY_PROVIDER)
|
|
216
|
+
|| (inheritClaudeCurrent ? cleanString(seed.provider) || cleanString(gateway.defaultProvider) : cleanString(gateway.defaultProvider) || cleanString(seed.provider));
|
|
217
|
+
const model = cleanString(process.env.MIXDOG_GATEWAY_MODEL)
|
|
218
|
+
|| (inheritClaudeCurrent ? cleanString(seed.model) || cleanString(gateway.defaultModel) : cleanString(gateway.defaultModel) || cleanString(seed.model));
|
|
219
|
+
if (!provider || !model) return { provider, model, mode };
|
|
220
|
+
|
|
221
|
+
const seedGateway = inheritClaudeCurrent
|
|
222
|
+
? {
|
|
223
|
+
mode,
|
|
224
|
+
effort: seed.effort ?? seed.displayEffort ?? gateway.effort ?? gateway.displayEffort,
|
|
225
|
+
fast: seed.fast ?? gateway.fast,
|
|
226
|
+
}
|
|
227
|
+
: gateway;
|
|
228
|
+
const preset = findPresetForRoute(provider, model, seedGateway);
|
|
229
|
+
const effort = cleanString(process.env.MIXDOG_GATEWAY_EFFORT)
|
|
230
|
+
|| (inheritClaudeCurrent ? cleanString(seed.effort ?? seed.displayEffort) : null)
|
|
231
|
+
|| cleanString(gateway.effort ?? gateway.displayEffort)
|
|
232
|
+
|| cleanString(preset?.effort);
|
|
233
|
+
const fast = cleanBool(process.env.MIXDOG_GATEWAY_FAST)
|
|
234
|
+
?? (inheritClaudeCurrent ? cleanBool(seed.fast) : null)
|
|
235
|
+
?? cleanBool(gateway.fast)
|
|
236
|
+
?? cleanBool(preset?.fast)
|
|
237
|
+
?? false;
|
|
238
|
+
const info = mergeModelInfo(provider, model, providerObj);
|
|
239
|
+
const contextWindow = num(seed.contextWindow, 0) || num(info?.contextWindow ?? info?.maxContextWindow ?? info?.max_input_tokens, 0);
|
|
240
|
+
const outputTokens = num(info?.outputTokens ?? info?.maxOutputTokens ?? info?.max_output_tokens, 0);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
mode,
|
|
244
|
+
provider,
|
|
245
|
+
model,
|
|
246
|
+
requestedModel: cleanString(seed.requestedModel) || null,
|
|
247
|
+
providerKind: providerKind(provider),
|
|
248
|
+
modelDisplay: cleanString(seed.modelDisplay) || displayForModel(provider, model, info),
|
|
249
|
+
presetId: inheritClaudeCurrent ? cleanString(preset?.id) : cleanString(gateway.presetId) || cleanString(preset?.id),
|
|
250
|
+
presetName: inheritClaudeCurrent ? presetLabel(preset) : cleanString(gateway.presetName) || presetLabel(preset),
|
|
251
|
+
effort,
|
|
252
|
+
fast,
|
|
253
|
+
thinkingBudgetTokens: num(seed.thinkingBudgetTokens, null),
|
|
254
|
+
contextWindow: contextWindow > 0 ? contextWindow : null,
|
|
255
|
+
outputTokens: outputTokens > 0 ? outputTokens : null,
|
|
256
|
+
inputCostPerM: info?.inputCostPerM ?? null,
|
|
257
|
+
outputCostPerM: info?.outputCostPerM ?? null,
|
|
258
|
+
cacheReadCostPerM: info?.cacheReadCostPerM ?? null,
|
|
259
|
+
cacheWriteCostPerM: info?.cacheWriteCostPerM ?? null,
|
|
260
|
+
serviceTier: cleanString(info?.defaultServiceTier) || null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function gatewaySendOptions(routeInfo) {
|
|
265
|
+
const opts = {};
|
|
266
|
+
if (routeInfo?.thinkingBudgetTokens) opts.thinkingBudgetTokens = routeInfo.thinkingBudgetTokens;
|
|
267
|
+
if (routeInfo?.effort) opts.effort = routeInfo.effort;
|
|
268
|
+
if (routeInfo?.fast === true) opts.fast = true;
|
|
269
|
+
return opts;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function gatewayRouteIdentityMessage(routeInfo) {
|
|
273
|
+
if (!routeInfo?.provider || !routeInfo?.model) return null;
|
|
274
|
+
const suffix = [
|
|
275
|
+
routeInfo.effort ? `effort=${routeInfo.effort}` : '',
|
|
276
|
+
routeInfo.thinkingBudgetTokens ? `thinking_budget=${routeInfo.thinkingBudgetTokens}` : '',
|
|
277
|
+
routeInfo.fast === true ? 'fast=true' : '',
|
|
278
|
+
].filter(Boolean).join(' ');
|
|
279
|
+
return {
|
|
280
|
+
role: 'system',
|
|
281
|
+
content:
|
|
282
|
+
`Runtime route: this request is served through the local mixdog gateway by ` +
|
|
283
|
+
`${routeInfo.provider}/${routeInfo.model}${suffix ? ` (${suffix})` : ''}. ` +
|
|
284
|
+
`If asked about the active provider or model identity, report this runtime route; ` +
|
|
285
|
+
`the Anthropic-compatible client model name may differ.`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function prepareGatewayMessages(messages, tools, routeInfo, log = () => {}) {
|
|
290
|
+
const identity = gatewayRouteIdentityMessage(routeInfo);
|
|
291
|
+
const withIdentity = identity ? [identity, ...messages] : messages;
|
|
292
|
+
const contextWindow = num(routeInfo?.contextWindow, 0);
|
|
293
|
+
const reserveTokens = estimateRequestReserveTokens(tools);
|
|
294
|
+
const beforeTokens = estimateMessagesTokens(withIdentity) + reserveTokens;
|
|
295
|
+
if (!(contextWindow > 0)) {
|
|
296
|
+
return {
|
|
297
|
+
messages: withIdentity,
|
|
298
|
+
trim: {
|
|
299
|
+
contextWindow: null,
|
|
300
|
+
beforeTokens,
|
|
301
|
+
afterTokens: beforeTokens,
|
|
302
|
+
reserveTokens,
|
|
303
|
+
trimmed: false,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const budgetTokens = Math.max(1, Math.floor(contextWindow * 0.92));
|
|
308
|
+
try {
|
|
309
|
+
const trimmed = trimMessages(withIdentity, budgetTokens, { reserveTokens });
|
|
310
|
+
const afterTokens = estimateMessagesTokens(trimmed) + reserveTokens;
|
|
311
|
+
return {
|
|
312
|
+
messages: trimmed,
|
|
313
|
+
trim: {
|
|
314
|
+
contextWindow,
|
|
315
|
+
budgetTokens,
|
|
316
|
+
beforeTokens,
|
|
317
|
+
afterTokens,
|
|
318
|
+
reserveTokens,
|
|
319
|
+
trimmed: trimmed.length !== withIdentity.length || afterTokens < beforeTokens,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
} catch (e) {
|
|
323
|
+
log(`gateway trim skipped: ${e?.message || e}`);
|
|
324
|
+
return {
|
|
325
|
+
messages: withIdentity,
|
|
326
|
+
trim: {
|
|
327
|
+
contextWindow,
|
|
328
|
+
budgetTokens,
|
|
329
|
+
beforeTokens,
|
|
330
|
+
afterTokens: beforeTokens,
|
|
331
|
+
reserveTokens,
|
|
332
|
+
trimmed: false,
|
|
333
|
+
error: String(e?.message || e),
|
|
334
|
+
overflow: contextWindow > 0 && beforeTokens > budgetTokens,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function promptFootprintTokens(provider, usage) {
|
|
341
|
+
const input = num(usage?.inputTokens, 0);
|
|
342
|
+
const cacheRead = num(usage?.cachedTokens ?? usage?.cacheReadTokens, 0);
|
|
343
|
+
const cacheWrite = num(usage?.cacheWriteTokens, 0);
|
|
344
|
+
if (num(usage?.promptTokens, 0) > 0) return num(usage.promptTokens, 0);
|
|
345
|
+
return isInclusiveProvider(provider) ? input : input + cacheRead + cacheWrite;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function usageCostUsd(routeInfo, usage) {
|
|
349
|
+
if (Number.isFinite(Number(usage?.costUsd))) return round(Number(usage.costUsd), 6);
|
|
350
|
+
return computeCostUsd({
|
|
351
|
+
provider: routeInfo?.provider,
|
|
352
|
+
model: routeInfo?.model,
|
|
353
|
+
inputTokens: num(usage?.inputTokens, 0),
|
|
354
|
+
outputTokens: num(usage?.outputTokens, 0),
|
|
355
|
+
cacheReadTokens: num(usage?.cachedTokens ?? usage?.cacheReadTokens, 0),
|
|
356
|
+
cacheWriteTokens: num(usage?.cacheWriteTokens, 0),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function summarizeGatewayUsage(routeInfo, providerOut, trim = null, durationMs = null) {
|
|
361
|
+
const u = providerOut?.usage || {};
|
|
362
|
+
const routeSectionStartedAt = ensureRouteSection(routeInfo);
|
|
363
|
+
const promptTokens = promptFootprintTokens(routeInfo?.provider, u);
|
|
364
|
+
const contextWindow = num(routeInfo?.contextWindow, 0);
|
|
365
|
+
const contextUsedPct = contextWindow > 0 ? round(promptTokens * 100 / contextWindow, 2) : null;
|
|
366
|
+
const costUsd = usageCostUsd(routeInfo, u);
|
|
367
|
+
return {
|
|
368
|
+
at: Date.now(),
|
|
369
|
+
provider: routeInfo?.provider,
|
|
370
|
+
model: routeInfo?.model,
|
|
371
|
+
providerKind: routeInfo?.providerKind || providerKind(routeInfo?.provider),
|
|
372
|
+
routeKey: routeIdentityKey(routeInfo),
|
|
373
|
+
routeSectionStartedAt,
|
|
374
|
+
responseModel: cleanString(providerOut?.model) || routeInfo?.model || null,
|
|
375
|
+
serviceTier: cleanString(providerOut?.serviceTier) || cleanString(u?.raw?.service_tier) || null,
|
|
376
|
+
inputTokens: num(u.inputTokens, 0),
|
|
377
|
+
outputTokens: num(u.outputTokens, 0),
|
|
378
|
+
cacheReadTokens: num(u.cachedTokens ?? u.cacheReadTokens, 0),
|
|
379
|
+
cacheWriteTokens: num(u.cacheWriteTokens, 0),
|
|
380
|
+
promptTokens,
|
|
381
|
+
costUsd,
|
|
382
|
+
costSource: Number.isFinite(Number(u?.costUsd)) ? 'provider' : costUsd > 0 ? 'catalog' : 'none',
|
|
383
|
+
contextUsedPct,
|
|
384
|
+
trim,
|
|
385
|
+
durationMs: Number.isFinite(durationMs) ? Math.max(0, Math.round(durationMs)) : null,
|
|
386
|
+
rawUsageKeys: u?.raw && typeof u.raw === 'object' ? Object.keys(u.raw).sort() : [],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function usageStorePath() {
|
|
391
|
+
return join(resolvePluginData(), GATEWAY_USAGE_FILE);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function recordGatewayUsageEvent(summary) {
|
|
395
|
+
const event = {
|
|
396
|
+
ts: summary?.at || Date.now(),
|
|
397
|
+
provider: summary?.provider || null,
|
|
398
|
+
model: summary?.model || null,
|
|
399
|
+
providerKind: summary?.providerKind || providerKind(summary?.provider),
|
|
400
|
+
routeKey: summary?.routeKey || null,
|
|
401
|
+
routeSectionStartedAt: num(summary?.routeSectionStartedAt, 0) || null,
|
|
402
|
+
inputTokens: num(summary?.inputTokens, 0),
|
|
403
|
+
outputTokens: num(summary?.outputTokens, 0),
|
|
404
|
+
promptTokens: num(summary?.promptTokens, 0),
|
|
405
|
+
costUsd: round(summary?.costUsd || 0, 6) || 0,
|
|
406
|
+
};
|
|
407
|
+
const cutoff = Date.now() - USAGE_EVENT_TTL_MS;
|
|
408
|
+
try {
|
|
409
|
+
updateJsonAtomicSync(usageStorePath(), (curRaw) => {
|
|
410
|
+
const cur = curRaw && typeof curRaw === 'object' ? curRaw : {};
|
|
411
|
+
const events = Array.isArray(cur.events) ? cur.events : [];
|
|
412
|
+
const kept = events
|
|
413
|
+
.filter(e => num(e?.ts, 0) >= cutoff)
|
|
414
|
+
.slice(-MAX_USAGE_EVENTS + 1);
|
|
415
|
+
kept.push(event);
|
|
416
|
+
return { version: 1, updatedAt: Date.now(), events: kept };
|
|
417
|
+
}, { compact: true, fsyncDir: true });
|
|
418
|
+
} catch {
|
|
419
|
+
// Local telemetry must never affect the routed model call.
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function loadUsageEvents() {
|
|
424
|
+
const raw = readJsonFile(usageStorePath());
|
|
425
|
+
const events = Array.isArray(raw?.events) ? raw.events : [];
|
|
426
|
+
const cutoff = Date.now() - USAGE_EVENT_TTL_MS;
|
|
427
|
+
return events.filter(e => num(e?.ts, 0) >= cutoff);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function labelForWindow(key, entry = {}) {
|
|
431
|
+
const explicit = cleanString(entry.label);
|
|
432
|
+
if (explicit) return explicit.toUpperCase();
|
|
433
|
+
const s = String(key || '').toLowerCase().replace(/[-\s]+/g, '_');
|
|
434
|
+
if (['five_hour', 'five_hours', '5h', '5_hour'].includes(s)) return '5H';
|
|
435
|
+
if (['seven_day', 'seven_days', 'weekly', 'week', '7d', '7_day'].includes(s)) return '7D';
|
|
436
|
+
if (['monthly', 'month', '30d', '30_day'].includes(s)) return 'M';
|
|
437
|
+
if (['daily', 'day', '24h', '24_hour'].includes(s)) return '24H';
|
|
438
|
+
return String(key || 'USE').toUpperCase();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function durationMsForWindow(key, entry = {}) {
|
|
442
|
+
const direct = num(entry.durationMs ?? entry.windowMs, 0);
|
|
443
|
+
if (direct > 0) return direct;
|
|
444
|
+
const hours = num(entry.hours ?? entry.durationHours ?? entry.windowHours, 0);
|
|
445
|
+
if (hours > 0) return hours * 60 * 60_000;
|
|
446
|
+
const days = num(entry.days ?? entry.durationDays ?? entry.windowDays, 0);
|
|
447
|
+
if (days > 0) return days * 24 * 60 * 60_000;
|
|
448
|
+
const label = labelForWindow(key, entry);
|
|
449
|
+
if (label === '5H') return 5 * 60 * 60_000;
|
|
450
|
+
if (label === '7D') return 7 * 24 * 60 * 60_000;
|
|
451
|
+
if (label === '24H') return 24 * 60 * 60_000;
|
|
452
|
+
if (label === 'M') return 30 * 24 * 60 * 60_000;
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resetAtForWindow(key, entry = {}) {
|
|
457
|
+
const raw = entry.resetAt ?? entry.resetsAt ?? entry.reset_at ?? entry.resets_at;
|
|
458
|
+
const n = num(raw, 0);
|
|
459
|
+
if (n > 0) return n < 10_000_000_000 ? n * 1000 : n;
|
|
460
|
+
const duration = durationMsForWindow(key, entry);
|
|
461
|
+
if (!(duration > 0)) return null;
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const start = Math.floor(now / duration) * duration;
|
|
464
|
+
return start + duration;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function windowFromRaw(key, value) {
|
|
468
|
+
const entry = value && typeof value === 'object' ? value : { used_percentage: value };
|
|
469
|
+
const usedPct = num(entry.usedPct ?? entry.used_percentage ?? entry.percent ?? entry.used_percent ?? entry.percentage, NaN);
|
|
470
|
+
const limitUsd = num(entry.limitUsd ?? entry.limit_usd ?? entry.budgetUsd ?? entry.budget_usd ?? entry.limit_usd_cents / 100, NaN);
|
|
471
|
+
const usedUsd = num(entry.usedUsd ?? entry.used_usd ?? entry.spendUsd ?? entry.spend_usd ?? entry.costUsd ?? entry.cost_usd ?? entry.used_usd_cents / 100, NaN);
|
|
472
|
+
const remainingUsd = num(entry.remainingUsd ?? entry.remaining_usd ?? entry.leftUsd ?? entry.left_usd ?? entry.balanceUsd ?? entry.balance_usd ?? entry.remaining_usd_cents / 100, NaN);
|
|
473
|
+
const out = {
|
|
474
|
+
label: labelForWindow(key, entry),
|
|
475
|
+
source: cleanString(entry.source) || 'provider',
|
|
476
|
+
};
|
|
477
|
+
if (Number.isFinite(usedPct)) out.usedPct = round(usedPct, 2);
|
|
478
|
+
if (Number.isFinite(limitUsd)) out.limitUsd = round(limitUsd, 4);
|
|
479
|
+
if (Number.isFinite(usedUsd)) out.usedUsd = round(usedUsd, 4);
|
|
480
|
+
if (Number.isFinite(remainingUsd)) out.remainingUsd = round(remainingUsd, 4);
|
|
481
|
+
const resetAt = resetAtForWindow(key, entry);
|
|
482
|
+
if (resetAt) out.resetAt = resetAt;
|
|
483
|
+
return Object.keys(out).length > 2 ? out : null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normaliseWindowsFromObject(obj) {
|
|
487
|
+
if (!obj || typeof obj !== 'object') return [];
|
|
488
|
+
if (Array.isArray(obj)) {
|
|
489
|
+
return obj.map((entry, idx) => windowFromRaw(entry?.id || entry?.name || entry?.label || idx, entry)).filter(Boolean);
|
|
490
|
+
}
|
|
491
|
+
return Object.entries(obj).map(([key, value]) => windowFromRaw(key, value)).filter(Boolean);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function rawUsageWindows(rawUsage) {
|
|
495
|
+
if (!rawUsage || typeof rawUsage !== 'object') return [];
|
|
496
|
+
const direct = rawUsage.quotaWindows || rawUsage.quota_windows || rawUsage.usageWindows || rawUsage.usage_windows;
|
|
497
|
+
const rate = rawUsage.rate_limits || rawUsage.rateLimits;
|
|
498
|
+
const quota = rawUsage.quota || rawUsage.quotas || rawUsage.limits || rawUsage.budgets;
|
|
499
|
+
return [
|
|
500
|
+
...normaliseWindowsFromObject(direct),
|
|
501
|
+
...normaliseWindowsFromObject(rate),
|
|
502
|
+
...normaliseWindowsFromObject(quota),
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function providerDefaultWindows(routeInfo) {
|
|
507
|
+
if (routeInfo?.provider === 'opencode-go') {
|
|
508
|
+
return OPENCODE_GO_WINDOWS.map(w => ({ ...w }));
|
|
509
|
+
}
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function configuredWindows(routeInfo) {
|
|
514
|
+
let gateway = {};
|
|
515
|
+
try { gateway = gatewaySection(); } catch { gateway = {}; }
|
|
516
|
+
const providerCfg = routeInfo?.provider ? providerSection(routeInfo.provider) : {};
|
|
517
|
+
return [
|
|
518
|
+
...normaliseWindowsFromObject(gateway.quotaWindows || gateway.usageWindows),
|
|
519
|
+
...normaliseWindowsFromObject(providerCfg.quotaWindows || providerCfg.usageWindows),
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function applyLocalSpend(windows, routeInfo) {
|
|
524
|
+
if (!windows.length) return [];
|
|
525
|
+
const events = loadUsageEvents()
|
|
526
|
+
.filter(e => (!routeInfo?.provider || e.provider === routeInfo.provider) && (!routeInfo?.model || e.model === routeInfo.model));
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
return windows.map(w => {
|
|
529
|
+
const duration = durationMsForWindow(w.label, w);
|
|
530
|
+
const since = duration > 0 ? now - duration : 0;
|
|
531
|
+
const usedUsd = events
|
|
532
|
+
.filter(e => num(e.ts, 0) >= since)
|
|
533
|
+
.reduce((sum, e) => sum + num(e.costUsd, 0), 0);
|
|
534
|
+
const next = { ...w };
|
|
535
|
+
if (num(w.limitUsd, 0) > 0) {
|
|
536
|
+
next.usedUsd = round(usedUsd, 4);
|
|
537
|
+
next.remainingUsd = round(Math.max(0, num(w.limitUsd, 0) - usedUsd), 4);
|
|
538
|
+
next.usedPct = round(Math.min(100, usedUsd * 100 / num(w.limitUsd, 1)), 2);
|
|
539
|
+
next.source = w.source || 'local-budget';
|
|
540
|
+
if (!next.resetAt) next.resetAt = resetAtForWindow(w.label, w);
|
|
541
|
+
}
|
|
542
|
+
return next;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function configuredBalance(routeInfo) {
|
|
547
|
+
let gateway = {};
|
|
548
|
+
try { gateway = gatewaySection(); } catch { gateway = {}; }
|
|
549
|
+
const providerCfg = routeInfo?.provider ? providerSection(routeInfo.provider) : {};
|
|
550
|
+
const raw = gateway.balance || gateway.budget || providerCfg.balance || providerCfg.budget || null;
|
|
551
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
552
|
+
const budgetUsd = num(raw.budgetUsd ?? raw.limitUsd ?? raw.monthlyUsd ?? raw.amountUsd, NaN);
|
|
553
|
+
if (!Number.isFinite(budgetUsd)) return null;
|
|
554
|
+
const events = loadUsageEvents().filter(e => !routeInfo?.provider || e.provider === routeInfo.provider);
|
|
555
|
+
const period = cleanString(raw.period) || '30d';
|
|
556
|
+
const duration = durationMsForWindow(period, raw) || 30 * 24 * 60 * 60_000;
|
|
557
|
+
const since = Date.now() - duration;
|
|
558
|
+
const spentUsd = events
|
|
559
|
+
.filter(e => num(e.ts, 0) >= since)
|
|
560
|
+
.reduce((sum, e) => sum + num(e.costUsd, 0), 0);
|
|
561
|
+
return {
|
|
562
|
+
source: 'local-budget',
|
|
563
|
+
period,
|
|
564
|
+
budgetUsd: round(budgetUsd, 4),
|
|
565
|
+
spentUsd: round(spentUsd, 4),
|
|
566
|
+
remainingUsd: round(Math.max(0, budgetUsd - spentUsd), 4),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function localRouteSpend(routeInfo) {
|
|
571
|
+
if (!shouldTrackDollarSpend(routeInfo)) return null;
|
|
572
|
+
const startedAt = ensureRouteSection(routeInfo);
|
|
573
|
+
if (!startedAt) return null;
|
|
574
|
+
const key = routeIdentityKey(routeInfo);
|
|
575
|
+
const events = loadUsageEvents().filter(e =>
|
|
576
|
+
(!routeInfo?.provider || e.provider === routeInfo.provider) &&
|
|
577
|
+
(!routeInfo?.model || e.model === routeInfo.model) &&
|
|
578
|
+
(e.routeKey ? e.routeKey === key : true) &&
|
|
579
|
+
num(e.ts, 0) >= startedAt
|
|
580
|
+
);
|
|
581
|
+
const costUsd = events.reduce((sum, e) => sum + num(e.costUsd, 0), 0);
|
|
582
|
+
if (!(costUsd > 0)) return null;
|
|
583
|
+
return {
|
|
584
|
+
label: 'SESS',
|
|
585
|
+
source: 'local-route',
|
|
586
|
+
period: 'session',
|
|
587
|
+
startedAt,
|
|
588
|
+
costUsd: round(costUsd, 6),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function buildGatewayLimits(routeInfo, providerOut = null, usageSnapshot = null) {
|
|
593
|
+
const providerWindows = rawUsageWindows(providerOut?.usage?.raw);
|
|
594
|
+
const snapshotWindows = Array.isArray(usageSnapshot?.quotaWindows) ? usageSnapshot.quotaWindows : [];
|
|
595
|
+
const cfgWindows = configuredWindows(routeInfo);
|
|
596
|
+
const defaultWindows = cfgWindows.length ? [] : providerDefaultWindows(routeInfo);
|
|
597
|
+
const localWindows = applyLocalSpend([...cfgWindows, ...defaultWindows], routeInfo);
|
|
598
|
+
return {
|
|
599
|
+
quotaWindows: providerWindows.length ? providerWindows : snapshotWindows.length ? snapshotWindows : localWindows,
|
|
600
|
+
balance: usageSnapshot?.balance || configuredBalance(routeInfo),
|
|
601
|
+
routeSpend: localRouteSpend(routeInfo),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function gatewayAdvertFields(routeInfo, usageSummary = null, limits = null) {
|
|
606
|
+
ensureRouteSection(routeInfo);
|
|
607
|
+
const fields = {
|
|
608
|
+
gateway_route_mode: routeInfo?.mode || null,
|
|
609
|
+
gateway_provider: routeInfo?.provider || null,
|
|
610
|
+
gateway_provider_kind: routeInfo?.providerKind || providerKind(routeInfo?.provider),
|
|
611
|
+
gateway_model: routeInfo?.model || null,
|
|
612
|
+
gateway_model_display: routeInfo?.modelDisplay || routeInfo?.model || null,
|
|
613
|
+
gateway_context_window: routeInfo?.contextWindow || null,
|
|
614
|
+
gateway_output_tokens: routeInfo?.outputTokens || null,
|
|
615
|
+
gateway_preset_id: routeInfo?.presetId || null,
|
|
616
|
+
gateway_preset_name: routeInfo?.presetName || null,
|
|
617
|
+
gateway_effort: routeInfo?.effort || null,
|
|
618
|
+
gateway_fast: routeInfo?.fast === true,
|
|
619
|
+
gateway_updated_at: Date.now(),
|
|
620
|
+
};
|
|
621
|
+
if (usageSummary) {
|
|
622
|
+
fields.gateway_context_used_pct = usageSummary.contextUsedPct;
|
|
623
|
+
fields.gateway_last_usage = usageSummary;
|
|
624
|
+
}
|
|
625
|
+
if (limits?.quotaWindows?.length) fields.gateway_quota_windows = limits.quotaWindows;
|
|
626
|
+
if (limits?.balance) fields.gateway_balance = limits.balance;
|
|
627
|
+
if (limits?.routeSpend) fields.gateway_route_spend = limits.routeSpend;
|
|
628
|
+
return fields;
|
|
629
|
+
}
|