openzoo 0.48.66 → 0.48.68
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/lib/anthropic.js +44 -2
- package/package.json +1 -1
package/lib/anthropic.js
CHANGED
|
@@ -96,7 +96,21 @@ export function anthropicToOpenAI(body) {
|
|
|
96
96
|
}));
|
|
97
97
|
if (!out.tools.length) delete out.tools;
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
// TOOL_CHOICE MAY NOT OUTLIVE TOOLS.
|
|
100
|
+
//
|
|
101
|
+
// The block above DROPS any tool without a name+input_schema — which is every
|
|
102
|
+
// server-side tool (web_search, computer, bash), because those carry no schema
|
|
103
|
+
// in the Anthropic shape. When that empties the list, `out.tools` is deleted.
|
|
104
|
+
// But tool_choice was set unconditionally, so the body went upstream with a
|
|
105
|
+
// choice and nothing to choose from.
|
|
106
|
+
//
|
|
107
|
+
// OBSERVED on grok-4.6: a Claude Code Web Search 400'd with "A tool_choice was
|
|
108
|
+
// set on the request but no tools were specified." Every provider rejects this
|
|
109
|
+
// shape; it just words it differently. Gate the choice on tools surviving.
|
|
110
|
+
const hasTools = Array.isArray(out.tools) && out.tools.length > 0;
|
|
111
|
+
if (!hasTools) {
|
|
112
|
+
delete out.tool_choice;
|
|
113
|
+
} else if (body.tool_choice?.type === 'auto') out.tool_choice = 'auto';
|
|
100
114
|
else if (body.tool_choice?.type === 'any') out.tool_choice = 'required';
|
|
101
115
|
else if (body.tool_choice?.type === 'tool') {
|
|
102
116
|
out.tool_choice = { type: 'function', function: { name: body.tool_choice.name } };
|
|
@@ -115,6 +129,12 @@ export function openAIToAnthropic(data, requestedModel) {
|
|
|
115
129
|
const msg = choice.message ?? {};
|
|
116
130
|
const content = [];
|
|
117
131
|
if (msg.content) content.push({ type: 'text', text: msg.content });
|
|
132
|
+
// Same reason as the streaming path: a refusal arrives with `content: null`
|
|
133
|
+
// and the sentence in `refusal`, so dropping it yields an empty message that
|
|
134
|
+
// reads as a broken model rather than as the answer it is.
|
|
135
|
+
else if (typeof msg.refusal === 'string' && msg.refusal.length) {
|
|
136
|
+
content.push({ type: 'text', text: msg.refusal });
|
|
137
|
+
}
|
|
118
138
|
for (const t of msg.tool_calls ?? []) {
|
|
119
139
|
let input = {};
|
|
120
140
|
try { input = JSON.parse(t.function?.arguments || '{}'); } catch { input = {}; }
|
|
@@ -257,9 +277,31 @@ export function streamOpenAIToAnthropic(res, upstream, requestedModel, onReceipt
|
|
|
257
277
|
// `ping` is in Anthropic's own SSE grammar and carries no content, so a
|
|
258
278
|
// client either ignores it or treats it as liveness — never as text.
|
|
259
279
|
if (!(typeof delta.content === 'string' && delta.content.length)
|
|
260
|
-
&& !(delta.tool_calls ?? []).length
|
|
280
|
+
&& !(delta.tool_calls ?? []).length
|
|
281
|
+
&& !(typeof delta.refusal === 'string' && delta.refusal.length)) {
|
|
261
282
|
ev('ping', {});
|
|
262
283
|
}
|
|
284
|
+
// A REFUSAL IS A RESPONSE. SHOW IT.
|
|
285
|
+
//
|
|
286
|
+
// Upstream returns refusals out-of-band: `finish_reason: "content_filter"`,
|
|
287
|
+
// `content: null`, and the actual sentence in `refusal`. We only ever read
|
|
288
|
+
// `delta.content`, so the whole turn translated to a VALID, EMPTY stream —
|
|
289
|
+
// message_start, message_stop, output_tokens 0 — and rendered as silence.
|
|
290
|
+
//
|
|
291
|
+
// OBSERVED: a security audit of the user's OWN exploited protocol returning
|
|
292
|
+
// nothing at all, repeatedly, and reading as "fable is broken" when the
|
|
293
|
+
// provider had in fact answered: "This request triggered restrictions on
|
|
294
|
+
// violative cyber content and was blocked under Anthropic's Usage Policy."
|
|
295
|
+
// Being told you were refused is actionable — you can rephrase, or route to
|
|
296
|
+
// another model. Being shown an empty box is not.
|
|
297
|
+
if (typeof delta.refusal === 'string' && delta.refusal.length) {
|
|
298
|
+
if (textIndex < 0) {
|
|
299
|
+
textIndex = nextIndex++;
|
|
300
|
+
ev('content_block_start', { index: textIndex, content_block: { type: 'text', text: '' } });
|
|
301
|
+
open.add(textIndex);
|
|
302
|
+
}
|
|
303
|
+
ev('content_block_delta', { index: textIndex, delta: { type: 'text_delta', text: delta.refusal } });
|
|
304
|
+
}
|
|
263
305
|
if (typeof delta.content === 'string' && delta.content.length) {
|
|
264
306
|
if (textIndex < 0) {
|
|
265
307
|
textIndex = nextIndex++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.68",
|
|
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",
|