openzoo 0.48.66 → 0.48.67

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.
Files changed (2) hide show
  1. package/lib/anthropic.js +29 -1
  2. package/package.json +1 -1
package/lib/anthropic.js CHANGED
@@ -115,6 +115,12 @@ export function openAIToAnthropic(data, requestedModel) {
115
115
  const msg = choice.message ?? {};
116
116
  const content = [];
117
117
  if (msg.content) content.push({ type: 'text', text: msg.content });
118
+ // Same reason as the streaming path: a refusal arrives with `content: null`
119
+ // and the sentence in `refusal`, so dropping it yields an empty message that
120
+ // reads as a broken model rather than as the answer it is.
121
+ else if (typeof msg.refusal === 'string' && msg.refusal.length) {
122
+ content.push({ type: 'text', text: msg.refusal });
123
+ }
118
124
  for (const t of msg.tool_calls ?? []) {
119
125
  let input = {};
120
126
  try { input = JSON.parse(t.function?.arguments || '{}'); } catch { input = {}; }
@@ -257,9 +263,31 @@ export function streamOpenAIToAnthropic(res, upstream, requestedModel, onReceipt
257
263
  // `ping` is in Anthropic's own SSE grammar and carries no content, so a
258
264
  // client either ignores it or treats it as liveness — never as text.
259
265
  if (!(typeof delta.content === 'string' && delta.content.length)
260
- && !(delta.tool_calls ?? []).length) {
266
+ && !(delta.tool_calls ?? []).length
267
+ && !(typeof delta.refusal === 'string' && delta.refusal.length)) {
261
268
  ev('ping', {});
262
269
  }
270
+ // A REFUSAL IS A RESPONSE. SHOW IT.
271
+ //
272
+ // Upstream returns refusals out-of-band: `finish_reason: "content_filter"`,
273
+ // `content: null`, and the actual sentence in `refusal`. We only ever read
274
+ // `delta.content`, so the whole turn translated to a VALID, EMPTY stream —
275
+ // message_start, message_stop, output_tokens 0 — and rendered as silence.
276
+ //
277
+ // OBSERVED: a security audit of the user's OWN exploited protocol returning
278
+ // nothing at all, repeatedly, and reading as "fable is broken" when the
279
+ // provider had in fact answered: "This request triggered restrictions on
280
+ // violative cyber content and was blocked under Anthropic's Usage Policy."
281
+ // Being told you were refused is actionable — you can rephrase, or route to
282
+ // another model. Being shown an empty box is not.
283
+ if (typeof delta.refusal === 'string' && delta.refusal.length) {
284
+ if (textIndex < 0) {
285
+ textIndex = nextIndex++;
286
+ ev('content_block_start', { index: textIndex, content_block: { type: 'text', text: '' } });
287
+ open.add(textIndex);
288
+ }
289
+ ev('content_block_delta', { index: textIndex, delta: { type: 'text_delta', text: delta.refusal } });
290
+ }
263
291
  if (typeof delta.content === 'string' && delta.content.length) {
264
292
  if (textIndex < 0) {
265
293
  textIndex = nextIndex++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.66",
3
+ "version": "0.48.67",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",