@rynfar/meridian 1.40.0 → 1.41.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/dist/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-3azh7s3k.js";
4
+ } from "./cli-swjr844z.js";
5
5
  import"./cli-vdp9s10c.js";
6
6
  import"./cli-sry5aqdj.js";
7
7
  import"./cli-4rqtm83g.js";
8
8
  import"./cli-340h1chz.js";
9
9
  import"./cli-rtab0qa6.js";
10
- import"./cli-m9pfb7h9.js";
10
+ import"./cli-0eky480v.js";
11
11
  import {
12
12
  __require
13
13
  } from "./cli-p9swy5t3.js";
@@ -89,7 +89,7 @@ Restart OpenCode for the plugin to take effect.`);
89
89
  process.exit(0);
90
90
  }
91
91
  if (args[0] === "refresh-token") {
92
- const { refreshOAuthToken } = await import("./tokenRefresh-y7d1qvb3.js");
92
+ const { refreshOAuthToken } = await import("./tokenRefresh-3kh1e8q8.js");
93
93
  const success = await refreshOAuthToken();
94
94
  if (success) {
95
95
  console.log("Token refreshed successfully");
@@ -9,6 +9,19 @@ import"./cli-p9swy5t3.js";
9
9
 
10
10
  // src/telemetry/profilePage.ts
11
11
  init_profileBar();
12
+
13
+ // src/telemetry/profileUsage.ts
14
+ var WINDOW_LABELS = {
15
+ five_hour: "5h",
16
+ seven_day: "7d",
17
+ seven_day_opus: "7d Opus",
18
+ seven_day_sonnet: "7d Sonnet",
19
+ seven_day_oauth_apps: "7d Apps",
20
+ seven_day_cowork: "7d Cowork",
21
+ seven_day_omelette: "7d Omelette"
22
+ };
23
+
24
+ // src/telemetry/profilePage.ts
12
25
  var profilePageHtml = `<!DOCTYPE html>
13
26
  <html lang="en">
14
27
  <head>
@@ -92,6 +105,48 @@ var profilePageHtml = `<!DOCTYPE html>
92
105
  }
93
106
  .copy-btn:hover { border-color: var(--accent); color: var(--accent); }
94
107
  .copy-btn.copied { color: var(--green); border-color: var(--green); }
108
+
109
+ /* OAuth usage panel — one block per profile, mirrors pylon's quota strip. */
110
+ .usage-section { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
111
+ .usage-section-title {
112
+ font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px;
113
+ margin-bottom: 10px; display: flex; align-items: center; gap: 8px;
114
+ }
115
+ .usage-as-of { font-size: 10px; color: var(--muted); text-transform: none; letter-spacing: 0; opacity: 0.7; }
116
+ .usage-grid {
117
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
118
+ gap: 8px;
119
+ }
120
+ .usage-card {
121
+ background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
122
+ padding: 8px 10px; min-width: 0;
123
+ }
124
+ .usage-row {
125
+ display: flex; justify-content: space-between; align-items: baseline;
126
+ font-size: 11px; gap: 8px; margin-bottom: 6px;
127
+ }
128
+ .usage-label { color: var(--muted); font-weight: 500; white-space: nowrap; }
129
+ .usage-pct { font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; font-weight: 600; font-size: 12px; }
130
+ .usage-bar {
131
+ height: 4px; background: rgba(127,127,127,0.18); border-radius: 2px; overflow: hidden;
132
+ margin-bottom: 4px;
133
+ }
134
+ .usage-fill { height: 100%; transition: width 0.4s ease; background: var(--green); }
135
+ .usage-card.status-warn .usage-fill,
136
+ .usage-card.status-warn .usage-pct { color: var(--yellow); }
137
+ .usage-card.status-warn .usage-fill { background: var(--yellow); }
138
+ .usage-card.status-high .usage-fill,
139
+ .usage-card.status-high .usage-pct { color: var(--red); }
140
+ .usage-card.status-high .usage-fill { background: var(--red); }
141
+ .usage-reset { font-size: 10px; color: var(--muted); white-space: nowrap; }
142
+ .usage-extra {
143
+ margin-top: 8px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border);
144
+ border-radius: 6px; font-size: 11px;
145
+ }
146
+ .usage-extra-row { display: flex; justify-content: space-between; gap: 8px; }
147
+ .usage-empty {
148
+ font-size: 11px; color: var(--muted); padding: 6px 0; font-style: italic;
149
+ }
95
150
  ` + profileBarCss + `
96
151
  </style>
97
152
  </head>
@@ -145,11 +200,73 @@ var profilePageHtml = `<!DOCTYPE html>
145
200
  </div>
146
201
 
147
202
  <script>
203
+ // Inlined from src/telemetry/profileUsage.ts. The TS source is unit-tested
204
+ // (see profile-usage.test.ts) and the labels object is interpolated here so
205
+ // the browser script and TS module share their data.
206
+ var WINDOW_LABELS = ${JSON.stringify(WINDOW_LABELS)};
207
+
208
+ function labelForWindow(type) {
209
+ if (WINDOW_LABELS[type]) return WINDOW_LABELS[type];
210
+ return String(type || '').split('_').map(function (p) {
211
+ return p.length > 0 ? p[0].toUpperCase() + p.slice(1) : p;
212
+ }).join(' ');
213
+ }
214
+
215
+ function classifyUtilization(u) {
216
+ if (u == null || !isFinite(u)) return 'ok';
217
+ if (u >= 0.85) return 'high';
218
+ if (u >= 0.6) return 'warn';
219
+ return 'ok';
220
+ }
221
+
222
+ function formatResetCountdown(resetsAt) {
223
+ if (resetsAt == null || !isFinite(resetsAt)) return '';
224
+ var ms = resetsAt - Date.now();
225
+ if (ms <= 0) return 'resetting…';
226
+ var minutes = Math.floor(ms / 60000);
227
+ if (minutes < 60) return 'in ' + Math.max(1, minutes) + 'm';
228
+ var hours = Math.floor(minutes / 60);
229
+ var remMin = minutes % 60;
230
+ if (hours < 24) return remMin > 0 ? 'in ' + hours + 'h ' + remMin + 'm' : 'in ' + hours + 'h';
231
+ var days = Math.floor(hours / 24);
232
+ var remHr = hours % 24;
233
+ return remHr > 0 ? 'in ' + days + 'd ' + remHr + 'h' : 'in ' + days + 'd';
234
+ }
235
+
236
+ function formatExtraUsage(eu) {
237
+ if (!eu || !eu.isEnabled) return null;
238
+ var monthlyLimit = isFinite(eu.monthlyLimit) ? eu.monthlyLimit : 0;
239
+ if (monthlyLimit <= 0) return null;
240
+ var used = isFinite(eu.usedCredits) ? eu.usedCredits : 0;
241
+ var utilization = (eu.utilization != null && isFinite(eu.utilization))
242
+ ? Math.max(0, Math.min(1, eu.utilization))
243
+ : (monthlyLimit > 0 ? Math.max(0, Math.min(1, used / monthlyLimit)) : 0);
244
+ var currency = eu.currency || '';
245
+ return {
246
+ used: (currency + used.toFixed(2)).trim(),
247
+ limit: (currency + monthlyLimit.toFixed(2)).trim(),
248
+ utilizationPct: Math.round(utilization * 100),
249
+ status: classifyUtilization(utilization),
250
+ };
251
+ }
252
+
253
+ // Cache the last seen quota response so the /profiles/list refresh can
254
+ // keep showing usage even if a single /v1/usage/quota/all call fails.
255
+ var lastQuota = null;
256
+
148
257
  async function refresh() {
149
258
  try {
150
- const res = await fetch('/profiles/list');
151
- const data = await res.json();
152
- render(data);
259
+ var [profilesRes, quotaRes] = await Promise.all([
260
+ fetch('/profiles/list'),
261
+ fetch('/v1/usage/quota/all').catch(function () { return null; }),
262
+ ]);
263
+ var profiles = await profilesRes.json();
264
+ var quota = null;
265
+ if (quotaRes && quotaRes.ok) {
266
+ try { quota = await quotaRes.json(); } catch (_) { quota = null; }
267
+ }
268
+ if (quota) lastQuota = quota;
269
+ render(profiles, lastQuota);
153
270
  } catch {
154
271
  document.getElementById('content').innerHTML = '<div class="empty-state"><h2>Could not load profiles</h2><p>Is Meridian running?</p></div>';
155
272
  }
@@ -157,9 +274,82 @@ async function refresh() {
157
274
 
158
275
  function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
159
276
 
160
- function render(data) {
277
+ function renderUsageSection(profileQuota) {
278
+ // No quota data for this profile yet (cold start or fetch failed) — hide
279
+ // entirely so we don't render an empty box.
280
+ if (!profileQuota) return '';
281
+ // API-key profiles cannot use OAuth usage — silently omit.
282
+ if (profileQuota.error === 'not_oauth') return '';
283
+
284
+ var windows = (profileQuota.windows || []).filter(function (w) {
285
+ return typeof w.utilization === 'number';
286
+ });
287
+ var extra = formatExtraUsage(profileQuota.extraUsage);
288
+
289
+ if (windows.length === 0 && !extra) {
290
+ if (profileQuota.error === 'no_token') {
291
+ return '<div class="usage-section">'
292
+ + '<div class="usage-section-title">Usage</div>'
293
+ + '<div class="usage-empty">Run <code style="background:var(--bg);padding:1px 5px;border-radius:3px">claude login</code> to see usage.</div>'
294
+ + '</div>';
295
+ }
296
+ return ''; // nothing fetched yet
297
+ }
298
+
299
+ var asOf = profileQuota.fetchedAt
300
+ ? '<span class="usage-as-of">updated ' + timeAgo(profileQuota.fetchedAt) + '</span>'
301
+ : '';
302
+
303
+ var cards = windows.map(function (w) {
304
+ var pct = Math.max(0, Math.min(1, w.utilization));
305
+ var pctRound = Math.round(pct * 100);
306
+ var status = classifyUtilization(pct);
307
+ var label = labelForWindow(w.type);
308
+ var reset = formatResetCountdown(w.resetsAt);
309
+ var tip = label + ' — ' + pctRound + '%' + (reset ? ' (resets ' + reset + ')' : '');
310
+ return '<div class="usage-card status-' + esc(status) + '" title="' + esc(tip) + '">'
311
+ + '<div class="usage-row">'
312
+ + '<span class="usage-label">' + esc(label) + '</span>'
313
+ + '<span class="usage-pct">' + pctRound + '%</span>'
314
+ + '</div>'
315
+ + '<div class="usage-bar"><div class="usage-fill" style="width:' + (pct * 100).toFixed(1) + '%"></div></div>'
316
+ + (reset ? '<div class="usage-reset">' + esc(reset) + '</div>' : '')
317
+ + '</div>';
318
+ }).join('');
319
+
320
+ var extraBlock = '';
321
+ if (extra) {
322
+ extraBlock = '<div class="usage-extra status-' + esc(extra.status) + '">'
323
+ + '<div class="usage-extra-row">'
324
+ + '<span class="usage-label">Extra usage</span>'
325
+ + '<span class="usage-pct">' + extra.utilizationPct + '%</span>'
326
+ + '</div>'
327
+ + '<div class="usage-bar"><div class="usage-fill" style="width:' + extra.utilizationPct + '%"></div></div>'
328
+ + '<div class="usage-extra-row" style="margin-top:4px">'
329
+ + '<span class="usage-reset">' + esc(extra.used) + ' / ' + esc(extra.limit) + '</span>'
330
+ + '</div>'
331
+ + '</div>';
332
+ }
333
+
334
+ return '<div class="usage-section">'
335
+ + '<div class="usage-section-title">Usage' + asOf + '</div>'
336
+ + (cards ? '<div class="usage-grid">' + cards + '</div>' : '')
337
+ + extraBlock
338
+ + '</div>';
339
+ }
340
+
341
+ function render(data, quotaData) {
161
342
  const profiles = data.profiles || [];
162
343
  const active = data.activeProfile;
344
+ // Build quick lookup: profileId -> per-profile quota entry from
345
+ // /v1/usage/quota/all. Endpoint may be unavailable (older Meridian)
346
+ // or have errored — in that case quotaById is empty and the per-card
347
+ // renderer simply hides its usage section.
348
+ const quotaProfiles = (quotaData && Array.isArray(quotaData.profiles)) ? quotaData.profiles : [];
349
+ const quotaById = {};
350
+ for (var qi = 0; qi < quotaProfiles.length; qi++) {
351
+ quotaById[quotaProfiles[qi].id] = quotaProfiles[qi];
352
+ }
163
353
 
164
354
  if (profiles.length === 0) {
165
355
  document.getElementById('content').innerHTML = '<div class="empty-state">'
@@ -218,6 +408,8 @@ function render(data) {
218
408
  html += '</button>';
219
409
  html += '</div>';
220
410
 
411
+ html += renderUsageSection(quotaById[p.id]);
412
+
221
413
  if (!isActive) {
222
414
  html += '<button class="switch-btn" onclick="switchProfile(&quot;'+esc(p.id)+'&quot;)">Switch to ' + esc(p.id) + '</button>';
223
415
  } else {
@@ -50,4 +50,6 @@ export declare function mapModelTier(model?: string): "sonnet" | "opus" | "opus[
50
50
  * @param mcpToolNames - Optional list of MCP tool names to make available to agents
51
51
  */
52
52
  export declare function buildAgentDefinitions(taskDescription: string, mcpToolNames?: string[]): Record<string, AgentDefinition>;
53
+ export declare function parseAgentNamesFromSchema(taskTool: unknown): string[];
54
+ export declare function buildAgentDefinitionsFromTool(taskTool: unknown, mcpToolNames?: string[]): Record<string, AgentDefinition>;
53
55
  //# sourceMappingURL=agentDefs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agentDefs.d.ts","sourceRoot":"","sources":["../../src/proxy/agentDefs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAc5C,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IAC/C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAcnF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAOjG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAsBjC"}
1
+ {"version":3,"file":"agentDefs.d.ts","sourceRoot":"","sources":["../../src/proxy/agentDefs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAc5C,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IAC/C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAcnF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAOjG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAsBjC;AA4ED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAIrE;AAED,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,OAAO,EACjB,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAuBjC"}
@@ -38,4 +38,46 @@ export declare function isRateLimitError(errMsg: string): boolean;
38
38
  * sonnet[1m] or opus[1m]. The fix is to fall back to the base model.
39
39
  */
40
40
  export declare function isExtraUsageRequiredError(errMsg: string): boolean;
41
+ /**
42
+ * Structured SDK-termination metadata extracted from raw error text.
43
+ * Used by diagnosticLog to surface why the SDK subprocess ended (max_turns,
44
+ * exit, abort) plus the captured stderr tail — info that classifyError
45
+ * collapses into a generic api_error.
46
+ */
47
+ export interface SdkTermination {
48
+ reason: "max_turns" | "process_exit" | "aborted" | "unknown";
49
+ /** Turn count when reason=max_turns and parseable. */
50
+ turns?: number;
51
+ /** Exit code when reason=process_exit and parseable. */
52
+ exitCode?: number;
53
+ /** Captured "Subprocess stderr: …" tail (truncated). */
54
+ stderrTail?: string;
55
+ /** Truncated raw error message — set only when reason="unknown" so the log
56
+ * line stays self-contained for unrecognized SDK errors (e.g. so we can
57
+ * add a new pattern next time). */
58
+ rawTail?: string;
59
+ }
60
+ /**
61
+ * Parse the raw error message thrown by the Claude Agent SDK into structured
62
+ * termination metadata. Pure function — no I/O.
63
+ *
64
+ * Returns reason="unknown" when the message doesn't match any recognized
65
+ * pattern; callers can still log it with whatever surrounding context they have.
66
+ */
67
+ export declare function extractSdkTermination(errMsg: string): SdkTermination;
68
+ /**
69
+ * Render an SdkTermination plus request context as a single greppable log line.
70
+ * Matches the key=value style used by token-health diagnostic messages so all
71
+ * /telemetry/logs entries are uniform.
72
+ *
73
+ * Session IDs are truncated to 8 chars to keep lines short — full IDs are
74
+ * already on the parent telemetry record.
75
+ */
76
+ export declare function formatSdkTermination(t: SdkTermination, ctx: {
77
+ model?: string;
78
+ requestSource?: string;
79
+ isResume?: boolean;
80
+ hasDeferredTools?: boolean;
81
+ sdkSessionId?: string;
82
+ }): string;
41
83
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA+G7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA+G7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAO3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,CAAA;IAC5D,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAuCpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACA,MAAM,CAYR"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Continuous OAuth usage fetching from Anthropic's private OAuth endpoint.
3
+ *
4
+ * Anthropic exposes `GET https://api.anthropic.com/api/oauth/usage` for OAuth
5
+ * (Claude Max) subscribers. Unlike the SDK's `rate_limit_event` (which only
6
+ * populates `utilization` near `allowed_warning` / `rejected`), this endpoint
7
+ * always returns continuous percentage values for every active rate-limit
8
+ * window — exactly what claude.ai's own UI uses.
9
+ *
10
+ * Headers required:
11
+ * Authorization: Bearer <oauth-access-token>
12
+ * anthropic-beta: oauth-2025-04-20
13
+ *
14
+ * We reuse `tokenRefresh.ts`'s cross-platform credential store (macOS Keychain
15
+ * or `~/.claude/.credentials.json`) to read the access token, and trigger a
16
+ * background refresh on 401.
17
+ *
18
+ * Per-profile caching: each profile has its own 30s TTL cache so multi-account
19
+ * setups can be queried independently without cross-contamination. Concurrent
20
+ * callers for the same profile share a single in-flight request.
21
+ */
22
+ import { type CredentialStore } from "./tokenRefresh";
23
+ export interface OAuthUsageWindow {
24
+ type: string;
25
+ utilization: number | null;
26
+ resetsAt: number | null;
27
+ }
28
+ export interface OAuthExtraUsageInfo {
29
+ isEnabled: boolean;
30
+ monthlyLimit: number;
31
+ usedCredits: number;
32
+ utilization: number | null;
33
+ currency: string;
34
+ }
35
+ export interface OAuthUsageSnapshot {
36
+ windows: OAuthUsageWindow[];
37
+ extraUsage: OAuthExtraUsageInfo | null;
38
+ fetchedAt: number;
39
+ }
40
+ /**
41
+ * Fetch latest OAuth usage for a specific profile (or the default OAuth
42
+ * account if none specified). Returns null if no OAuth token is available
43
+ * or the upstream call fails (after one refresh attempt).
44
+ *
45
+ * Per-profile in-process cache (30s TTL by default) prevents hammering
46
+ * Anthropic's endpoint when many clients poll concurrently. Concurrent
47
+ * callers for the same profile share a single in-flight request.
48
+ *
49
+ * @param ttlMs Override the cache TTL (default 30s).
50
+ * @param force Bypass the cache and fetch fresh.
51
+ * @param store Override the credential store (for testing).
52
+ * @param profileId Logical profile identifier used as the cache key.
53
+ * Pass null/undefined for the default OAuth account.
54
+ * @param claudeConfigDir When provided, reads credentials from this dir's
55
+ * keychain entry (macOS) or `.credentials.json`
56
+ * (Linux) instead of the platform default.
57
+ */
58
+ export declare function fetchOAuthUsage(opts?: {
59
+ ttlMs?: number;
60
+ force?: boolean;
61
+ store?: CredentialStore;
62
+ profileId?: string | null;
63
+ claudeConfigDir?: string;
64
+ }): Promise<OAuthUsageSnapshot | null>;
65
+ /** Test-only / shutdown helper — clears all cached snapshots and pending fetches. */
66
+ export declare function resetOAuthUsageCache(): void;
67
+ //# sourceMappingURL=oauthUsage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthUsage.d.ts","sourceRoot":"","sources":["../../src/proxy/oauthUsage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAoD,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAgCvG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AA0ED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,eAAe,CAAC,IAAI,CAAC,EAAE;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAgDrC;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
@@ -15,7 +15,7 @@
15
15
  * This is intentional — OpenAI-format clients replay full history themselves
16
16
  * and don't benefit from Meridian's session resumption.
17
17
  */
18
- export type OpenAiRole = "system" | "user" | "assistant";
18
+ export type OpenAiRole = "system" | "user" | "assistant" | "tool";
19
19
  export interface OpenAiTextPart {
20
20
  type: "text";
21
21
  text?: string;
@@ -35,8 +35,23 @@ export interface OpenAiContentPart {
35
35
  }
36
36
  export interface OpenAiMessage {
37
37
  role: OpenAiRole;
38
+ tool_call_id?: string;
38
39
  content: string | OpenAiContentPart[];
40
+ tool_calls?: OpenAiCompletionToolCall[];
39
41
  }
42
+ export interface OpenAiChatToolFunction {
43
+ type: "function";
44
+ function: {
45
+ name: string;
46
+ description?: string;
47
+ parameters: unknown;
48
+ strict?: boolean;
49
+ };
50
+ }
51
+ export interface OpenAiChatToolCustom {
52
+ type: "custom";
53
+ }
54
+ export type OpenAiChatTool = OpenAiChatToolFunction | OpenAiChatToolCustom;
40
55
  export interface OpenAiChatRequest {
41
56
  model?: string;
42
57
  messages?: OpenAiMessage[];
@@ -45,6 +60,7 @@ export interface OpenAiChatRequest {
45
60
  max_completion_tokens?: number;
46
61
  temperature?: number;
47
62
  top_p?: number;
63
+ tools?: OpenAiChatTool[];
48
64
  }
49
65
  export interface AnthropicTextBlock {
50
66
  type: "text";
@@ -58,10 +74,15 @@ export interface AnthropicImageBlock {
58
74
  data: string;
59
75
  };
60
76
  }
61
- export type AnthropicInputContentBlock = AnthropicTextBlock | AnthropicImageBlock;
62
77
  export interface AnthropicMessage {
63
78
  role: "user" | "assistant";
64
- content: string | AnthropicInputContentBlock[];
79
+ content: string | AnthropicContentBlock[];
80
+ }
81
+ export interface AnthropicTool {
82
+ name: string;
83
+ description: string;
84
+ input_schema: unknown;
85
+ strict?: boolean;
65
86
  }
66
87
  export interface AnthropicRequestBody {
67
88
  model: string;
@@ -71,20 +92,56 @@ export interface AnthropicRequestBody {
71
92
  system?: string;
72
93
  temperature?: number;
73
94
  top_p?: number;
95
+ tools?: AnthropicTool[];
74
96
  }
75
97
  export interface AnthropicUsage {
76
98
  input_tokens?: number;
77
99
  output_tokens?: number;
78
100
  }
79
- export interface AnthropicContentBlock {
80
- type: string;
101
+ export interface AnthropicContentBlockText {
102
+ type: "text";
81
103
  text?: string;
82
104
  }
105
+ export interface AnthropicToolUseBlock {
106
+ type: "tool_use";
107
+ id: string;
108
+ name: string;
109
+ input: Record<string, unknown>;
110
+ }
111
+ export interface AnthropicToolResultBlock {
112
+ type: "tool_result";
113
+ tool_use_id: string;
114
+ content: string | AnthropicContentBlock[];
115
+ }
116
+ export interface AnthropicThinkingBlock {
117
+ type: "thinking";
118
+ thinking: string;
119
+ }
120
+ export type AnthropicContentBlock = AnthropicTextBlock | AnthropicImageBlock | AnthropicThinkingBlock | AnthropicToolResultBlock | AnthropicToolUseBlock;
83
121
  export interface AnthropicResponse {
84
122
  content?: AnthropicContentBlock[];
85
123
  stop_reason?: string;
86
124
  usage?: AnthropicUsage;
87
125
  }
126
+ /**
127
+ * Streaming tool-call delta as emitted in chat.completion.chunk events.
128
+ *
129
+ * The OpenAI streaming protocol splits a single tool call across multiple
130
+ * chunks: a "start" chunk announces the call (id + function name), and
131
+ * subsequent "args" chunks append `function.arguments` fragments. `index`
132
+ * correlates fragments back to their parent call. Fields are optional rather
133
+ * than `DeepPartial<OpenAiCompletionToolCall>` so the type can't represent
134
+ * nonsense like `{ function: { arguments: undefined } }`.
135
+ */
136
+ export interface OpenAiStreamingToolCallDelta {
137
+ index: number;
138
+ type?: "function";
139
+ id?: string;
140
+ function?: {
141
+ name?: string;
142
+ arguments?: string;
143
+ };
144
+ }
88
145
  export interface OpenAiStreamChunk {
89
146
  id: string;
90
147
  object: "chat.completion.chunk";
@@ -95,10 +152,25 @@ export interface OpenAiStreamChunk {
95
152
  delta: {
96
153
  role?: "assistant";
97
154
  content?: string;
155
+ tool_calls?: OpenAiStreamingToolCallDelta[];
156
+ reasoning_content?: string;
98
157
  };
99
- finish_reason: "stop" | "length" | null;
158
+ finish_reason: "stop" | "length" | "tool_calls" | null;
100
159
  }>;
101
160
  }
161
+ export interface OpenAiCompletionFunctionToolCall {
162
+ type: "function";
163
+ index?: number;
164
+ id: string;
165
+ function: {
166
+ name: string;
167
+ arguments: string;
168
+ };
169
+ }
170
+ export interface OpenAiCompletionCustomToolCall {
171
+ type: "custom";
172
+ }
173
+ export type OpenAiCompletionToolCall = OpenAiCompletionFunctionToolCall | OpenAiCompletionCustomToolCall;
102
174
  export interface OpenAiCompletion {
103
175
  id: string;
104
176
  object: "chat.completion";
@@ -108,9 +180,11 @@ export interface OpenAiCompletion {
108
180
  index: 0;
109
181
  message: {
110
182
  role: "assistant";
111
- content: string;
183
+ content: string | null;
184
+ reasoning_content?: string;
185
+ tool_calls?: OpenAiCompletionToolCall[];
112
186
  };
113
- finish_reason: "stop" | "length";
187
+ finish_reason: "stop" | "length" | "tool_calls";
114
188
  }>;
115
189
  usage: {
116
190
  prompt_tokens: number;
@@ -142,29 +216,81 @@ export declare function extractOpenAiContent(content: string | OpenAiContentPart
142
216
  export declare function translateOpenAiToAnthropic(body: OpenAiChatRequest): AnthropicRequestBody | null;
143
217
  /**
144
218
  * Translate a complete Anthropic /v1/messages response to OpenAI format.
145
- * Thinking blocks are filtered out only text blocks are included.
219
+ * Currently supports only text, thinking and function call blocks.
220
+ *
221
+ * When `thinkingPassthrough` is false, thinking blocks are not
222
+ * mapped to `reasoning_content` (stripped from the response).
146
223
  */
147
- export declare function translateAnthropicToOpenAi(response: AnthropicResponse, completionId: string, model: string, created: number): OpenAiCompletion;
148
- interface AnthropicSseEvent {
224
+ export declare function translateAnthropicToOpenAi(response: AnthropicResponse, completionId: string, model: string, created: number, options?: {
225
+ thinkingPassthrough?: boolean;
226
+ }): OpenAiCompletion;
227
+ /**
228
+ * Wire-format SSE event from Anthropic's `/v1/messages` streaming API.
229
+ *
230
+ * `content_block` may describe a text block, a tool_use block, or a thinking
231
+ * block depending on the stream position — only `type` is guaranteed.
232
+ */
233
+ export interface AnthropicSseEvent {
149
234
  type: string;
235
+ index?: number;
150
236
  delta?: {
151
237
  type?: string;
152
238
  text?: string;
153
239
  stop_reason?: string;
240
+ partial_json?: string;
241
+ thinking?: string;
154
242
  };
243
+ content_block?: {
244
+ type: "text";
245
+ text?: string;
246
+ } | {
247
+ type: "thinking";
248
+ thinking?: string;
249
+ } | AnthropicToolUseBlock;
155
250
  message?: {
156
251
  id?: string;
157
252
  };
158
253
  }
254
+ export interface SseTranslator {
255
+ (event: AnthropicSseEvent): OpenAiStreamChunk | null;
256
+ }
257
+ export interface SseTranslatorContext {
258
+ completionId: string;
259
+ model: string;
260
+ created: number;
261
+ /** When false, thinking blocks are stripped from the response */
262
+ thinkingPassthrough?: boolean;
263
+ }
264
+ /**
265
+ * A stateful translator for one OpenAI streaming response.
266
+ *
267
+ * Each completion stream gets its own translator instance to keep state out
268
+ * of server.ts. Internally tracks the current tool-call index so that
269
+ * `content_block_start` (tool_use) events are emitted as OpenAI tool_call
270
+ * deltas with monotonically increasing `index` values, matching how
271
+ * `function.arguments` fragments must correlate back to their parent call.
272
+ *
273
+ * Anthropic's wire format signals start/end of each content block; OpenAI's
274
+ * does not, so we manufacture an index per stream.
275
+ */
276
+ export declare function createSseTranslator(ctx: SseTranslatorContext): SseTranslator;
159
277
  /**
160
278
  * Translate one parsed Anthropic SSE event into an OpenAI stream chunk.
161
- * Returns null for events that should be skipped (pings, block starts, etc).
279
+ * Returns null for events that should be skipped (pings, message_stop,
280
+ * content_block_stop, text-block content_block_start, etc).
281
+ *
282
+ * `toolCallNum` is the OpenAI `tool_calls[].index` value to emit on tool-call
283
+ * chunks. Callers tracking multiple tools per stream must increment it on
284
+ * each `content_block_start` with `type: "tool_use"` *before* calling this
285
+ * function. Use `createSseTranslator` to handle this automatically.
286
+ *
287
+ * When `thinkingPassthrough` is false, thinking_delta events are skipped
288
+ * so the client does not receive reasoning_content.
162
289
  */
163
- export declare function translateAnthropicSseEvent(event: AnthropicSseEvent, completionId: string, model: string, created: number): OpenAiStreamChunk | null;
290
+ export declare function translateAnthropicSseEvent(event: AnthropicSseEvent, completionId: string, model: string, created: number, toolCallNum: number, thinkingPassthrough?: boolean): OpenAiStreamChunk | null;
164
291
  /**
165
292
  * Return the static list of available Claude models in OpenAI format.
166
293
  * Context windows reflect subscription capabilities.
167
294
  */
168
295
  export declare function buildModelList(isMaxSubscription: boolean, now?: number): OpenAiModel[];
169
- export {};
170
296
  //# sourceMappingURL=openai.d.ts.map