ambitry 0.1.2 → 0.1.4

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 CHANGED
@@ -150,6 +150,8 @@ Point a provider at a different upstream — Azure, a gateway, a local model —
150
150
 
151
151
  Cost is computed from published per-token rates. Models with no known rate report `null` rather than a guess; a cost dashboard that quietly invents numbers is worse than one that admits ignorance.
152
152
 
153
+ **One request modification, and the only one.** OpenAI-compatible providers omit token usage from streamed responses unless `stream_options.include_usage` is set, and almost no agent code sets it — so without this, cost and spend caps would be blank for most streaming traffic. Ambitry adds that flag to outgoing OpenAI streaming requests when you haven't set it yourself. The visible effect is one extra terminal chunk carrying usage, which SDKs treat as a normal chunk with no choices. If you set `stream_options` yourself, it's left alone, and Anthropic requests are never modified.
154
+
153
155
  ## Development
154
156
 
155
157
  ```bash
package/dist/server.js CHANGED
@@ -33,9 +33,22 @@ export function createServer({ store, controls }) {
33
33
  examples: ['http://localhost:8787/anthropic', 'http://localhost:8787/openai/v1'],
34
34
  });
35
35
  }
36
- const body = await readBody(req);
36
+ let body = await readBody(req);
37
37
  const parsed = parseRequest(route.provider.id, body);
38
38
  const requestJson = safeJson(body);
39
+ // OpenAI-compatible providers omit usage from streamed responses unless
40
+ // stream_options.include_usage is set, and almost no agent code sets it.
41
+ // Without this the cost column is empty for most streaming traffic, which
42
+ // guts the spend caps. Anthropic always reports usage, so this is only
43
+ // needed here. The cost is one extra terminal chunk carrying usage, which
44
+ // SDKs treat as a normal chunk with no choices.
45
+ if (route.provider.id === 'openai' && parsed.stream && requestJson && typeof requestJson === 'object') {
46
+ const r = requestJson;
47
+ if (r.stream_options === undefined) {
48
+ r.stream_options = { include_usage: true };
49
+ body = Buffer.from(JSON.stringify(r));
50
+ }
51
+ }
39
52
  // Scanned on the way out, so a leak is caught on the request that carries
40
53
  // it rather than after the provider has already received it.
41
54
  const findings = scanRequest(requestJson);
package/dist/viewer.js CHANGED
@@ -103,13 +103,20 @@ function toolCells(t) {
103
103
  }).join('');
104
104
  }
105
105
 
106
- function meter(label, spent, cap) {
106
+ function meter(label, spent, cap, unpriced) {
107
107
  const pct = cap ? Math.min(100, (spent / cap) * 100) : 0;
108
+ // An unpriced model contributes nothing to the total, so the figure is a
109
+ // floor rather than the spend. Saying so beats implying a precise zero.
110
+ const note = unpriced
111
+ ? unpriced + ' request' + (unpriced === 1 ? '' : 's') + ' unpriced'
112
+ : (cap ? null : 'no cap set');
108
113
  const bar = cap
109
114
  ? '<div class="bar' + (pct > 80 ? ' hot' : '') + '"><i style="width:' + pct + '%"></i></div>'
110
- : '<div class="k" style="margin-top:8px">no cap set</div>';
111
- return '<div class="card"><div class="k">' + label + '</div><div class="v">' + money(spent) +
112
- (cap ? ' <span class="muted" style="font-size:13px">/ ' + money(cap) + '</span>' : '') + '</div>' + bar + '</div>';
115
+ : '';
116
+ return '<div class="card"><div class="k">' + label + '</div><div class="v">' +
117
+ (unpriced ? ' ' : '') + money(spent) +
118
+ (cap ? ' <span class="muted" style="font-size:13px">/ ' + money(cap) + '</span>' : '') + '</div>' +
119
+ bar + (note ? '<div class="k" style="margin-top:8px">' + note + '</div>' : '') + '</div>';
113
120
  }
114
121
 
115
122
  async function refresh() {
@@ -143,15 +150,27 @@ async function refresh() {
143
150
  // Counts denied tool calls as well as rejected requests. Tool denial
144
151
  // returns 200 with the call stripped, so counting status codes alone
145
152
  // reports zero blocks while enforcement is visibly working.
146
- const blocked = traces.reduce((n, t) =>
147
- n + (t.denied_count || 0) + (t.status === 403 ? 1 : 0), 0);
153
+ const byPolicy = traces.reduce((n, t) => n + (t.denied_count || 0), 0);
154
+ const stopped = traces.filter(t => t.status === 403).length;
155
+ const blocked = byPolicy + stopped;
156
+ // Naming the cause matters: a kill-switch stop is not a policy denial, and
157
+ // labelling everything "by policy" contradicts the card beside it that
158
+ // says the proxy is only logging.
159
+ const cause = byPolicy && stopped ? 'policy and kill switch'
160
+ : byPolicy ? 'tool calls denied'
161
+ : stopped ? 'requests stopped'
162
+ : 'nothing blocked';
163
+
164
+ // A request whose model has no published rate contributes nothing to spend.
165
+ const unpriced = traces.filter(t => t.cost_usd === null && t.status === 200).length;
166
+
148
167
  $('cards').innerHTML =
149
- meter('Spend · last hour', status.spend.lastHour, status.limits.perHourUsd) +
150
- meter('Spend · last day', status.spend.lastDay, status.limits.perDayUsd) +
168
+ meter('Spend · last hour', status.spend.lastHour, status.limits.perHourUsd, unpriced) +
169
+ meter('Spend · last day', status.spend.lastDay, status.limits.perDayUsd, unpriced) +
151
170
  '<div class="card"><div class="k">Requests</div><div class="v">' + traces.length + '</div>' +
152
171
  '<div class="k" style="margin-top:8px">' + (status.enforcing ? 'policy enforcing' : 'logging only') + '</div></div>' +
153
172
  '<div class="card"><div class="k">Blocked</div><div class="v">' + blocked + '</div>' +
154
- '<div class="k" style="margin-top:8px">by policy</div></div>';
173
+ '<div class="k" style="margin-top:8px">' + cause + '</div></div>';
155
174
 
156
175
  if (!traces.length) {
157
176
  $('list').innerHTML = '<div class="empty">No traffic yet. Point your SDK at ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ambitry",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Audit log and permission layer for AI agents. One-line integration, zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {