local-openai2anthropic 0.2.3__py3-none-any.whl → 0.3.6__py3-none-any.whl
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.
- local_openai2anthropic/__init__.py +1 -1
- local_openai2anthropic/config.py +132 -18
- local_openai2anthropic/converter.py +82 -60
- local_openai2anthropic/main.py +83 -12
- local_openai2anthropic/protocol.py +1 -1
- local_openai2anthropic/router.py +208 -576
- local_openai2anthropic/streaming/__init__.py +6 -0
- local_openai2anthropic/streaming/handler.py +444 -0
- local_openai2anthropic/tools/__init__.py +14 -0
- local_openai2anthropic/tools/handler.py +357 -0
- local_openai2anthropic/utils/__init__.py +18 -0
- local_openai2anthropic/utils/tokens.py +96 -0
- {local_openai2anthropic-0.2.3.dist-info → local_openai2anthropic-0.3.6.dist-info}/METADATA +51 -28
- local_openai2anthropic-0.3.6.dist-info/RECORD +25 -0
- local_openai2anthropic-0.2.3.dist-info/RECORD +0 -19
- {local_openai2anthropic-0.2.3.dist-info → local_openai2anthropic-0.3.6.dist-info}/WHEEL +0 -0
- {local_openai2anthropic-0.2.3.dist-info → local_openai2anthropic-0.3.6.dist-info}/entry_points.txt +0 -0
- {local_openai2anthropic-0.2.3.dist-info → local_openai2anthropic-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Streaming response handlers."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, AsyncGenerator
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
from local_openai2anthropic.protocol import AnthropicError, AnthropicErrorResponse
|
|
13
|
+
from local_openai2anthropic.utils.tokens import (
|
|
14
|
+
_chunk_text,
|
|
15
|
+
_count_tokens,
|
|
16
|
+
_estimate_input_tokens,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _stream_response(
|
|
23
|
+
client: httpx.AsyncClient,
|
|
24
|
+
url: str,
|
|
25
|
+
headers: dict,
|
|
26
|
+
json_data: dict,
|
|
27
|
+
model: str,
|
|
28
|
+
) -> AsyncGenerator[str, None]:
|
|
29
|
+
"""
|
|
30
|
+
Stream response from OpenAI and convert to Anthropic format.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
async with client.stream(
|
|
34
|
+
"POST", url, headers=headers, json=json_data
|
|
35
|
+
) as response:
|
|
36
|
+
if response.status_code != 200:
|
|
37
|
+
error_body = await response.aread()
|
|
38
|
+
error_text = error_body.decode("utf-8", errors="replace").strip()
|
|
39
|
+
try:
|
|
40
|
+
error_json = json.loads(error_text) if error_text else {}
|
|
41
|
+
error_msg = error_json.get("error", {}).get("message") or error_text
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
error_msg = error_text
|
|
44
|
+
if not error_msg:
|
|
45
|
+
error_msg = (
|
|
46
|
+
response.reason_phrase
|
|
47
|
+
or f"Upstream API error ({response.status_code})"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
error_event = AnthropicErrorResponse(
|
|
51
|
+
error=AnthropicError(type="api_error", message=error_msg)
|
|
52
|
+
)
|
|
53
|
+
yield f"event: error\ndata: {error_event.model_dump_json()}\n\n"
|
|
54
|
+
yield "data: [DONE]\n\n"
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Process SSE stream
|
|
58
|
+
first_chunk = True
|
|
59
|
+
content_block_started = False
|
|
60
|
+
content_block_index = 0
|
|
61
|
+
current_block_type = None # 'thinking', 'text', or 'tool_use'
|
|
62
|
+
current_tool_call_index = None
|
|
63
|
+
tool_call_buffers: dict[int, str] = {}
|
|
64
|
+
finish_reason = None
|
|
65
|
+
input_tokens = _estimate_input_tokens(json_data)
|
|
66
|
+
output_tokens = 0
|
|
67
|
+
message_id = None
|
|
68
|
+
sent_message_delta = False
|
|
69
|
+
pending_text_prefix = ""
|
|
70
|
+
|
|
71
|
+
async for line in response.aiter_lines():
|
|
72
|
+
if not line.startswith("data: "):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
data = line[6:]
|
|
76
|
+
if data == "[DONE]":
|
|
77
|
+
if not sent_message_delta:
|
|
78
|
+
stop_reason_map = {
|
|
79
|
+
"stop": "end_turn",
|
|
80
|
+
"length": "max_tokens",
|
|
81
|
+
"tool_calls": "tool_use",
|
|
82
|
+
}
|
|
83
|
+
delta_event = {
|
|
84
|
+
"type": "message_delta",
|
|
85
|
+
"delta": {
|
|
86
|
+
"stop_reason": stop_reason_map.get(
|
|
87
|
+
finish_reason or "stop", "end_turn"
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
"usage": {
|
|
91
|
+
"input_tokens": input_tokens,
|
|
92
|
+
"output_tokens": output_tokens,
|
|
93
|
+
"cache_creation_input_tokens": None,
|
|
94
|
+
"cache_read_input_tokens": None,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"[Anthropic Stream Event] message_delta: {json.dumps(delta_event, ensure_ascii=False)}"
|
|
99
|
+
)
|
|
100
|
+
yield f"event: message_delta\ndata: {json.dumps(delta_event)}\n\n"
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
chunk = json.loads(data)
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"[OpenAI Stream Chunk] {json.dumps(chunk, ensure_ascii=False)}"
|
|
107
|
+
)
|
|
108
|
+
except json.JSONDecodeError:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# First chunk: message_start
|
|
112
|
+
if first_chunk:
|
|
113
|
+
message_id = chunk.get("id", "")
|
|
114
|
+
usage = chunk.get("usage") or {}
|
|
115
|
+
input_tokens = usage.get("prompt_tokens", input_tokens)
|
|
116
|
+
|
|
117
|
+
start_event = {
|
|
118
|
+
"type": "message_start",
|
|
119
|
+
"message": {
|
|
120
|
+
"id": message_id,
|
|
121
|
+
"type": "message",
|
|
122
|
+
"role": "assistant",
|
|
123
|
+
"content": [],
|
|
124
|
+
"model": model,
|
|
125
|
+
"stop_reason": None,
|
|
126
|
+
"stop_sequence": None,
|
|
127
|
+
"usage": {
|
|
128
|
+
"input_tokens": input_tokens,
|
|
129
|
+
"output_tokens": 0,
|
|
130
|
+
"cache_creation_input_tokens": None,
|
|
131
|
+
"cache_read_input_tokens": None,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
logger.debug(
|
|
136
|
+
f"[Anthropic Stream Event] message_start: {json.dumps(start_event, ensure_ascii=False)}"
|
|
137
|
+
)
|
|
138
|
+
yield f"event: message_start\ndata: {json.dumps(start_event)}\n\n"
|
|
139
|
+
first_chunk = False
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
# Handle usage-only chunks
|
|
143
|
+
if not chunk.get("choices"):
|
|
144
|
+
usage = chunk.get("usage") or {}
|
|
145
|
+
if usage:
|
|
146
|
+
input_tokens = usage.get("prompt_tokens", input_tokens)
|
|
147
|
+
output_tokens = usage.get("completion_tokens", output_tokens)
|
|
148
|
+
if content_block_started:
|
|
149
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"
|
|
150
|
+
content_block_started = False
|
|
151
|
+
|
|
152
|
+
stop_reason_map = {
|
|
153
|
+
"stop": "end_turn",
|
|
154
|
+
"length": "max_tokens",
|
|
155
|
+
"tool_calls": "tool_use",
|
|
156
|
+
}
|
|
157
|
+
delta_event = {
|
|
158
|
+
"type": "message_delta",
|
|
159
|
+
"delta": {
|
|
160
|
+
"stop_reason": stop_reason_map.get(
|
|
161
|
+
finish_reason or "stop", "end_turn"
|
|
162
|
+
)
|
|
163
|
+
},
|
|
164
|
+
"usage": {
|
|
165
|
+
"input_tokens": usage.get(
|
|
166
|
+
"prompt_tokens", input_tokens
|
|
167
|
+
),
|
|
168
|
+
"output_tokens": usage.get("completion_tokens", 0),
|
|
169
|
+
"cache_creation_input_tokens": None,
|
|
170
|
+
"cache_read_input_tokens": None,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
logger.debug(
|
|
174
|
+
f"[Anthropic Stream Event] message_delta: {json.dumps(delta_event, ensure_ascii=False)}"
|
|
175
|
+
)
|
|
176
|
+
yield f"event: message_delta\ndata: {json.dumps(delta_event)}\n\n"
|
|
177
|
+
sent_message_delta = True
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
choice = chunk["choices"][0]
|
|
181
|
+
delta = choice.get("delta", {})
|
|
182
|
+
|
|
183
|
+
# Track finish reason (but don't skip - content may also be present)
|
|
184
|
+
if choice.get("finish_reason"):
|
|
185
|
+
finish_reason = choice["finish_reason"]
|
|
186
|
+
|
|
187
|
+
# Handle reasoning content (thinking)
|
|
188
|
+
if delta.get("reasoning_content"):
|
|
189
|
+
reasoning = delta["reasoning_content"]
|
|
190
|
+
pending_text_prefix = ""
|
|
191
|
+
# Start thinking content block if not already started
|
|
192
|
+
if not content_block_started or current_block_type != "thinking":
|
|
193
|
+
# Close previous block if exists
|
|
194
|
+
if content_block_started:
|
|
195
|
+
stop_block = {
|
|
196
|
+
"type": "content_block_stop",
|
|
197
|
+
"index": content_block_index,
|
|
198
|
+
}
|
|
199
|
+
logger.debug(
|
|
200
|
+
f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
201
|
+
)
|
|
202
|
+
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
203
|
+
content_block_index += 1
|
|
204
|
+
start_block = {
|
|
205
|
+
"type": "content_block_start",
|
|
206
|
+
"index": content_block_index,
|
|
207
|
+
"content_block": {
|
|
208
|
+
"type": "thinking",
|
|
209
|
+
"thinking": "",
|
|
210
|
+
"signature": "",
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
logger.debug(
|
|
214
|
+
f"[Anthropic Stream Event] content_block_start (thinking): {json.dumps(start_block, ensure_ascii=False)}"
|
|
215
|
+
)
|
|
216
|
+
yield f"event: content_block_start\ndata: {json.dumps(start_block)}\n\n"
|
|
217
|
+
content_block_started = True
|
|
218
|
+
current_block_type = "thinking"
|
|
219
|
+
|
|
220
|
+
for chunk in _chunk_text(reasoning):
|
|
221
|
+
delta_block = {
|
|
222
|
+
"type": "content_block_delta",
|
|
223
|
+
"index": content_block_index,
|
|
224
|
+
"delta": {"type": "thinking_delta", "thinking": chunk},
|
|
225
|
+
}
|
|
226
|
+
yield f"event: content_block_delta\ndata: {json.dumps(delta_block)}\n\n"
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Handle content
|
|
230
|
+
if isinstance(delta.get("content"), str):
|
|
231
|
+
content_text = delta.get("content", "")
|
|
232
|
+
if not content_text:
|
|
233
|
+
continue
|
|
234
|
+
if content_text.strip() == "(no content)":
|
|
235
|
+
continue
|
|
236
|
+
if not content_block_started or current_block_type != "text":
|
|
237
|
+
if not content_text.strip():
|
|
238
|
+
pending_text_prefix += content_text
|
|
239
|
+
continue
|
|
240
|
+
# Close previous block if exists
|
|
241
|
+
if content_block_started:
|
|
242
|
+
stop_block = {
|
|
243
|
+
"type": "content_block_stop",
|
|
244
|
+
"index": content_block_index,
|
|
245
|
+
}
|
|
246
|
+
logger.debug(
|
|
247
|
+
f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
248
|
+
)
|
|
249
|
+
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
250
|
+
content_block_index += 1
|
|
251
|
+
start_block = {
|
|
252
|
+
"type": "content_block_start",
|
|
253
|
+
"index": content_block_index,
|
|
254
|
+
"content_block": {"type": "text", "text": ""},
|
|
255
|
+
}
|
|
256
|
+
logger.debug(
|
|
257
|
+
f"[Anthropic Stream Event] content_block_start (text): {json.dumps(start_block, ensure_ascii=False)}"
|
|
258
|
+
)
|
|
259
|
+
yield f"event: content_block_start\ndata: {json.dumps(start_block)}\n\n"
|
|
260
|
+
content_block_started = True
|
|
261
|
+
current_block_type = "text"
|
|
262
|
+
|
|
263
|
+
if pending_text_prefix:
|
|
264
|
+
content_text = pending_text_prefix + content_text
|
|
265
|
+
pending_text_prefix = ""
|
|
266
|
+
|
|
267
|
+
output_tokens += _count_tokens(content_text)
|
|
268
|
+
delta_block = {
|
|
269
|
+
"type": "content_block_delta",
|
|
270
|
+
"index": content_block_index,
|
|
271
|
+
"delta": {"type": "text_delta", "text": content_text},
|
|
272
|
+
}
|
|
273
|
+
yield f"event: content_block_delta\ndata: {json.dumps(delta_block)}\n\n"
|
|
274
|
+
|
|
275
|
+
# Handle tool calls
|
|
276
|
+
if delta.get("tool_calls"):
|
|
277
|
+
pending_text_prefix = ""
|
|
278
|
+
for tool_call in delta["tool_calls"]:
|
|
279
|
+
tool_call_idx = tool_call.get("index", 0)
|
|
280
|
+
|
|
281
|
+
if tool_call.get("id"):
|
|
282
|
+
if content_block_started and (
|
|
283
|
+
current_block_type != "tool_use"
|
|
284
|
+
or current_tool_call_index != tool_call_idx
|
|
285
|
+
):
|
|
286
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"
|
|
287
|
+
content_block_started = False
|
|
288
|
+
content_block_index += 1
|
|
289
|
+
|
|
290
|
+
if not content_block_started:
|
|
291
|
+
func = tool_call.get("function") or {}
|
|
292
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': content_block_index, 'content_block': {'type': 'tool_use', 'id': tool_call['id'], 'name': func.get('name', ''), 'input': {}}})}\n\n"
|
|
293
|
+
content_block_started = True
|
|
294
|
+
current_block_type = "tool_use"
|
|
295
|
+
current_tool_call_index = tool_call_idx
|
|
296
|
+
tool_call_buffers.setdefault(tool_call_idx, "")
|
|
297
|
+
|
|
298
|
+
if (tool_call.get("function") or {}).get("arguments"):
|
|
299
|
+
args = (tool_call.get("function") or {}).get(
|
|
300
|
+
"arguments", ""
|
|
301
|
+
)
|
|
302
|
+
if (
|
|
303
|
+
not content_block_started
|
|
304
|
+
or current_block_type != "tool_use"
|
|
305
|
+
or current_tool_call_index != tool_call_idx
|
|
306
|
+
):
|
|
307
|
+
if content_block_started:
|
|
308
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"
|
|
309
|
+
content_block_index += 1
|
|
310
|
+
func = tool_call.get("function") or {}
|
|
311
|
+
tool_id = tool_call.get("id", "")
|
|
312
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': content_block_index, 'content_block': {'type': 'tool_use', 'id': tool_id, 'name': func.get('name', ''), 'input': {}}})}\n\n"
|
|
313
|
+
content_block_started = True
|
|
314
|
+
current_block_type = "tool_use"
|
|
315
|
+
current_tool_call_index = tool_call_idx
|
|
316
|
+
tool_call_buffers.setdefault(tool_call_idx, "")
|
|
317
|
+
tool_call_buffers[tool_call_idx] = (
|
|
318
|
+
tool_call_buffers.get(tool_call_idx, "") + args
|
|
319
|
+
)
|
|
320
|
+
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': content_block_index, 'delta': {'type': 'input_json_delta', 'partial_json': args}})}\n\n"
|
|
321
|
+
|
|
322
|
+
# Close final content block
|
|
323
|
+
if content_block_started:
|
|
324
|
+
stop_block = {
|
|
325
|
+
"type": "content_block_stop",
|
|
326
|
+
"index": content_block_index,
|
|
327
|
+
}
|
|
328
|
+
logger.debug(
|
|
329
|
+
f"[Anthropic Stream Event] content_block_stop (final): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
330
|
+
)
|
|
331
|
+
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
332
|
+
|
|
333
|
+
# Message stop
|
|
334
|
+
stop_event = {"type": "message_stop"}
|
|
335
|
+
logger.debug(
|
|
336
|
+
f"[Anthropic Stream Event] message_stop: {json.dumps(stop_event, ensure_ascii=False)}"
|
|
337
|
+
)
|
|
338
|
+
yield f"event: message_stop\ndata: {json.dumps(stop_event)}\n\n"
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
import traceback
|
|
342
|
+
|
|
343
|
+
error_msg = f"{str(e)}\n{traceback.format_exc()}"
|
|
344
|
+
logger.error(f"Stream error: {error_msg}")
|
|
345
|
+
error_event = AnthropicErrorResponse(
|
|
346
|
+
error=AnthropicError(type="internal_error", message=str(e))
|
|
347
|
+
)
|
|
348
|
+
yield f"event: error\ndata: {error_event.model_dump_json()}\n\n"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
async def _convert_result_to_stream(
|
|
352
|
+
result: JSONResponse,
|
|
353
|
+
model: str,
|
|
354
|
+
) -> AsyncGenerator[str, None]:
|
|
355
|
+
"""Convert a JSONResponse to streaming SSE format."""
|
|
356
|
+
body = json.loads(bytes(result.body).decode("utf-8"))
|
|
357
|
+
message_id = body.get("id", f"msg_{int(time.time() * 1000)}")
|
|
358
|
+
content = body.get("content", [])
|
|
359
|
+
usage = body.get("usage", {})
|
|
360
|
+
stop_reason = body.get("stop_reason", "end_turn")
|
|
361
|
+
|
|
362
|
+
# Map stop_reason
|
|
363
|
+
stop_reason_map = {
|
|
364
|
+
"end_turn": "stop",
|
|
365
|
+
"max_tokens": "length",
|
|
366
|
+
"tool_use": "tool_calls",
|
|
367
|
+
}
|
|
368
|
+
openai_stop_reason = stop_reason_map.get(stop_reason, "stop")
|
|
369
|
+
|
|
370
|
+
# 1. message_start event
|
|
371
|
+
start_event = {
|
|
372
|
+
"type": "message_start",
|
|
373
|
+
"message": {
|
|
374
|
+
"id": message_id,
|
|
375
|
+
"type": "message",
|
|
376
|
+
"role": "assistant",
|
|
377
|
+
"content": [],
|
|
378
|
+
"model": model,
|
|
379
|
+
"stop_reason": None,
|
|
380
|
+
"stop_sequence": None,
|
|
381
|
+
"usage": {
|
|
382
|
+
"input_tokens": usage.get("input_tokens", 0),
|
|
383
|
+
"output_tokens": 0,
|
|
384
|
+
"cache_creation_input_tokens": usage.get("cache_creation_input_tokens"),
|
|
385
|
+
"cache_read_input_tokens": usage.get("cache_read_input_tokens"),
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
yield f"event: message_start\ndata: {json.dumps(start_event)}\n\n"
|
|
390
|
+
|
|
391
|
+
# 2. Process content blocks
|
|
392
|
+
for i, block in enumerate(content):
|
|
393
|
+
block_type = block.get("type")
|
|
394
|
+
|
|
395
|
+
if block_type == "text":
|
|
396
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': i, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
|
|
397
|
+
text = block.get("text", "")
|
|
398
|
+
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': i, 'delta': {'type': 'text_delta', 'text': text}})}\n\n"
|
|
399
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
400
|
+
|
|
401
|
+
elif block_type == "tool_use":
|
|
402
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': i, 'content_block': {'type': 'tool_use', 'id': block.get('id', ''), 'name': block.get('name', ''), 'input': block.get('input', {})}})}\n\n"
|
|
403
|
+
tool_input = block.get("input", {})
|
|
404
|
+
if tool_input:
|
|
405
|
+
input_json = json.dumps(tool_input, ensure_ascii=False)
|
|
406
|
+
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': i, 'delta': {'type': 'input_json_delta', 'partial_json': input_json}})}\n\n"
|
|
407
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
408
|
+
|
|
409
|
+
elif block_type == "server_tool_use":
|
|
410
|
+
# Preserve official Anthropic block type so clients can count server tool uses.
|
|
411
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': i, 'content_block': {'type': 'server_tool_use', 'id': block.get('id', ''), 'name': block.get('name', ''), 'input': block.get('input', {})}})}\n\n"
|
|
412
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
413
|
+
|
|
414
|
+
elif block_type == "web_search_tool_result":
|
|
415
|
+
# Stream the tool result as its own content block.
|
|
416
|
+
tool_result_block = dict(block)
|
|
417
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': i, 'content_block': tool_result_block})}\n\n"
|
|
418
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
419
|
+
|
|
420
|
+
elif block_type == "thinking":
|
|
421
|
+
# Handle thinking blocks (BetaThinkingBlock)
|
|
422
|
+
signature = block.get("signature", "")
|
|
423
|
+
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': i, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': signature}})}\n\n"
|
|
424
|
+
thinking_text = block.get("thinking", "")
|
|
425
|
+
if thinking_text:
|
|
426
|
+
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': i, 'delta': {'type': 'thinking_delta', 'thinking': thinking_text}})}\n\n"
|
|
427
|
+
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
428
|
+
|
|
429
|
+
# 3. message_delta with final usage
|
|
430
|
+
delta_event = {
|
|
431
|
+
"type": "message_delta",
|
|
432
|
+
"delta": {"stop_reason": stop_reason},
|
|
433
|
+
"usage": {
|
|
434
|
+
"input_tokens": usage.get("input_tokens", 0),
|
|
435
|
+
"output_tokens": usage.get("output_tokens", 0),
|
|
436
|
+
"cache_creation_input_tokens": usage.get("cache_creation_input_tokens"),
|
|
437
|
+
"cache_read_input_tokens": usage.get("cache_read_input_tokens"),
|
|
438
|
+
"server_tool_use": usage.get("server_tool_use"),
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
yield f"event: message_delta\ndata: {json.dumps(delta_event)}\n\n"
|
|
442
|
+
|
|
443
|
+
# 4. message_stop
|
|
444
|
+
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Server tool handling for local_openai2anthropic."""
|
|
3
|
+
|
|
4
|
+
from .handler import (
|
|
5
|
+
ServerToolHandler,
|
|
6
|
+
_add_tool_results_to_messages,
|
|
7
|
+
_handle_with_server_tools,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ServerToolHandler",
|
|
12
|
+
"_handle_with_server_tools",
|
|
13
|
+
"_add_tool_results_to_messages",
|
|
14
|
+
]
|