local-openai2anthropic 0.2.7__py3-none-any.whl → 0.2.9__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/converter.py +76 -54
- local_openai2anthropic/router.py +253 -106
- {local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/METADATA +1 -1
- {local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/RECORD +7 -7
- {local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/WHEEL +0 -0
- {local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/entry_points.txt +0 -0
- {local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -92,7 +92,9 @@ def convert_anthropic_to_openai(
|
|
|
92
92
|
converted_messages = _convert_anthropic_message_to_openai(msg)
|
|
93
93
|
openai_messages.extend(converted_messages)
|
|
94
94
|
msg_count += 1
|
|
95
|
-
logger.debug(
|
|
95
|
+
logger.debug(
|
|
96
|
+
f"Converted {msg_count} messages, total OpenAI messages: {len(openai_messages)}"
|
|
97
|
+
)
|
|
96
98
|
|
|
97
99
|
# Build OpenAI params
|
|
98
100
|
params: dict[str, Any] = {
|
|
@@ -139,17 +141,21 @@ def convert_anthropic_to_openai(
|
|
|
139
141
|
openai_tools.append(openai_tool)
|
|
140
142
|
|
|
141
143
|
# Add server tools as OpenAI function tools
|
|
142
|
-
for tool_class in
|
|
144
|
+
for tool_class in enabled_server_tools or []:
|
|
143
145
|
if tool_class.tool_type in server_tools_config:
|
|
144
146
|
config = server_tools_config[tool_class.tool_type]
|
|
145
147
|
openai_tools.append(tool_class.to_openai_tool(config))
|
|
146
148
|
|
|
147
149
|
if openai_tools:
|
|
148
150
|
params["tools"] = openai_tools
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
# Convert tool_choice
|
|
151
153
|
if tool_choice:
|
|
152
|
-
tc =
|
|
154
|
+
tc = (
|
|
155
|
+
tool_choice
|
|
156
|
+
if isinstance(tool_choice, dict)
|
|
157
|
+
else tool_choice.model_dump()
|
|
158
|
+
)
|
|
153
159
|
tc_type = tc.get("type")
|
|
154
160
|
if tc_type == "auto":
|
|
155
161
|
params["tool_choice"] = "auto"
|
|
@@ -162,7 +168,7 @@ def convert_anthropic_to_openai(
|
|
|
162
168
|
}
|
|
163
169
|
else:
|
|
164
170
|
params["tool_choice"] = "auto"
|
|
165
|
-
|
|
171
|
+
|
|
166
172
|
# Handle thinking parameter
|
|
167
173
|
# vLLM/SGLang use chat_template_kwargs.thinking to toggle thinking mode
|
|
168
174
|
# Some models use "thinking", others use "enable_thinking", so we include both
|
|
@@ -181,7 +187,7 @@ def convert_anthropic_to_openai(
|
|
|
181
187
|
logger.debug(
|
|
182
188
|
"thinking.budget_tokens (%s) is accepted but not supported by "
|
|
183
189
|
"vLLM/SGLang. Using default thinking configuration.",
|
|
184
|
-
budget_tokens
|
|
190
|
+
budget_tokens,
|
|
185
191
|
)
|
|
186
192
|
else:
|
|
187
193
|
# Default to disabled thinking mode if not explicitly enabled
|
|
@@ -208,32 +214,32 @@ def _convert_anthropic_message_to_openai(
|
|
|
208
214
|
) -> list[dict[str, Any]]:
|
|
209
215
|
"""
|
|
210
216
|
Convert a single Anthropic message to OpenAI format.
|
|
211
|
-
|
|
212
|
-
Returns a list of messages because tool_results need to be
|
|
217
|
+
|
|
218
|
+
Returns a list of messages because tool_results need to be
|
|
213
219
|
separate tool messages in OpenAI format.
|
|
214
220
|
"""
|
|
215
221
|
role = msg.get("role", "user")
|
|
216
222
|
content = msg.get("content", "")
|
|
217
|
-
|
|
223
|
+
|
|
218
224
|
if isinstance(content, str):
|
|
219
225
|
return [{"role": role, "content": content}]
|
|
220
|
-
|
|
226
|
+
|
|
221
227
|
# Handle list of content blocks
|
|
222
228
|
openai_content: list[dict[str, Any]] = []
|
|
223
229
|
tool_calls: list[dict[str, Any]] = []
|
|
224
230
|
tool_call_results: list[dict[str, Any]] = []
|
|
225
|
-
|
|
231
|
+
|
|
226
232
|
for block in content:
|
|
227
233
|
if isinstance(block, str):
|
|
228
234
|
openai_content.append({"type": "text", "text": block})
|
|
229
235
|
continue
|
|
230
|
-
|
|
236
|
+
|
|
231
237
|
block_type = block.get("type") if isinstance(block, dict) else block.type
|
|
232
|
-
|
|
238
|
+
|
|
233
239
|
if block_type == "text":
|
|
234
240
|
text = block.get("text") if isinstance(block, dict) else block.text
|
|
235
241
|
openai_content.append({"type": "text", "text": text})
|
|
236
|
-
|
|
242
|
+
|
|
237
243
|
elif block_type == "image":
|
|
238
244
|
# Convert image to image_url format
|
|
239
245
|
source = block.get("source") if isinstance(block, dict) else block.source
|
|
@@ -246,11 +252,13 @@ def _convert_anthropic_message_to_openai(
|
|
|
246
252
|
data = source.data
|
|
247
253
|
# Build data URL
|
|
248
254
|
url = f"data:{media_type};base64,{data}"
|
|
249
|
-
openai_content.append(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
openai_content.append(
|
|
256
|
+
{
|
|
257
|
+
"type": "image_url",
|
|
258
|
+
"image_url": {"url": url},
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
|
|
254
262
|
elif block_type == "tool_use":
|
|
255
263
|
# Convert to function call
|
|
256
264
|
if isinstance(block, dict):
|
|
@@ -261,16 +269,20 @@ def _convert_anthropic_message_to_openai(
|
|
|
261
269
|
tool_id = block.id
|
|
262
270
|
name = block.name
|
|
263
271
|
input_data = block.input
|
|
264
|
-
|
|
265
|
-
tool_calls.append(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
|
|
273
|
+
tool_calls.append(
|
|
274
|
+
{
|
|
275
|
+
"id": tool_id,
|
|
276
|
+
"type": "function",
|
|
277
|
+
"function": {
|
|
278
|
+
"name": name,
|
|
279
|
+
"arguments": json.dumps(input_data)
|
|
280
|
+
if isinstance(input_data, dict)
|
|
281
|
+
else str(input_data),
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
274
286
|
elif block_type == "tool_result":
|
|
275
287
|
# Tool results need to be separate tool messages
|
|
276
288
|
if isinstance(block, dict):
|
|
@@ -281,7 +293,7 @@ def _convert_anthropic_message_to_openai(
|
|
|
281
293
|
tool_use_id = block.tool_use_id
|
|
282
294
|
result_content = block.content
|
|
283
295
|
is_error = getattr(block, "is_error", False)
|
|
284
|
-
|
|
296
|
+
|
|
285
297
|
# Handle content that might be a list or string
|
|
286
298
|
if isinstance(result_content, list):
|
|
287
299
|
# Extract text from content blocks
|
|
@@ -298,7 +310,7 @@ def _convert_anthropic_message_to_openai(
|
|
|
298
310
|
result_text = "\n".join(text_parts)
|
|
299
311
|
else:
|
|
300
312
|
result_text = str(result_content)
|
|
301
|
-
|
|
313
|
+
|
|
302
314
|
tool_msg: dict[str, Any] = {
|
|
303
315
|
"role": "tool",
|
|
304
316
|
"tool_call_id": tool_use_id,
|
|
@@ -306,28 +318,28 @@ def _convert_anthropic_message_to_openai(
|
|
|
306
318
|
}
|
|
307
319
|
# Note: is_error is not directly supported in OpenAI API
|
|
308
320
|
# but we could add it to content if needed
|
|
309
|
-
|
|
321
|
+
|
|
310
322
|
tool_call_results.append(tool_msg)
|
|
311
|
-
|
|
323
|
+
|
|
312
324
|
# Build primary message
|
|
313
325
|
messages: list[dict[str, Any]] = []
|
|
314
326
|
# SGLang requires content field to be present, default to empty string
|
|
315
327
|
primary_msg: dict[str, Any] = {"role": role, "content": ""}
|
|
316
|
-
|
|
328
|
+
|
|
317
329
|
if openai_content:
|
|
318
330
|
if len(openai_content) == 1 and openai_content[0]["type"] == "text":
|
|
319
331
|
primary_msg["content"] = openai_content[0]["text"]
|
|
320
332
|
else:
|
|
321
333
|
primary_msg["content"] = openai_content
|
|
322
|
-
|
|
334
|
+
|
|
323
335
|
if tool_calls:
|
|
324
336
|
primary_msg["tool_calls"] = tool_calls
|
|
325
|
-
|
|
337
|
+
|
|
326
338
|
messages.append(primary_msg)
|
|
327
|
-
|
|
339
|
+
|
|
328
340
|
# Add tool result messages separately
|
|
329
341
|
messages.extend(tool_call_results)
|
|
330
|
-
|
|
342
|
+
|
|
331
343
|
return messages
|
|
332
344
|
|
|
333
345
|
|
|
@@ -353,24 +365,24 @@ def convert_openai_to_anthropic(
|
|
|
353
365
|
) -> Message:
|
|
354
366
|
"""
|
|
355
367
|
Convert OpenAI ChatCompletion to Anthropic Message.
|
|
356
|
-
|
|
368
|
+
|
|
357
369
|
Args:
|
|
358
370
|
completion: OpenAI chat completion response
|
|
359
371
|
model: Model name
|
|
360
|
-
|
|
372
|
+
|
|
361
373
|
Returns:
|
|
362
374
|
Anthropic Message response
|
|
363
375
|
"""
|
|
364
376
|
from anthropic.types.beta import BetaThinkingBlock
|
|
365
|
-
|
|
377
|
+
|
|
366
378
|
choice = completion.choices[0]
|
|
367
379
|
message = choice.message
|
|
368
|
-
|
|
380
|
+
|
|
369
381
|
# Convert content blocks
|
|
370
382
|
content: list[ContentBlock] = []
|
|
371
|
-
|
|
383
|
+
|
|
372
384
|
# Add reasoning content (thinking) first if present
|
|
373
|
-
reasoning_content = getattr(message,
|
|
385
|
+
reasoning_content = getattr(message, "reasoning_content", None)
|
|
374
386
|
if reasoning_content:
|
|
375
387
|
content.append(
|
|
376
388
|
BetaThinkingBlock(
|
|
@@ -379,7 +391,7 @@ def convert_openai_to_anthropic(
|
|
|
379
391
|
signature="", # Signature not available from OpenAI format
|
|
380
392
|
)
|
|
381
393
|
)
|
|
382
|
-
|
|
394
|
+
|
|
383
395
|
# Add text content if present
|
|
384
396
|
if message.content:
|
|
385
397
|
if isinstance(message.content, str):
|
|
@@ -388,16 +400,20 @@ def convert_openai_to_anthropic(
|
|
|
388
400
|
for part in message.content:
|
|
389
401
|
if part.type == "text":
|
|
390
402
|
content.append(TextBlock(type="text", text=part.text))
|
|
391
|
-
|
|
403
|
+
|
|
392
404
|
# Convert tool calls
|
|
393
405
|
if message.tool_calls:
|
|
394
406
|
for tc in message.tool_calls:
|
|
407
|
+
# Handle case where function might be None
|
|
408
|
+
if not tc.function:
|
|
409
|
+
continue
|
|
410
|
+
|
|
395
411
|
tool_input: dict[str, Any] = {}
|
|
396
412
|
try:
|
|
397
413
|
tool_input = json.loads(tc.function.arguments)
|
|
398
414
|
except json.JSONDecodeError:
|
|
399
415
|
tool_input = {"raw": tc.function.arguments}
|
|
400
|
-
|
|
416
|
+
|
|
401
417
|
content.append(
|
|
402
418
|
ToolUseBlock(
|
|
403
419
|
type="tool_use",
|
|
@@ -406,7 +422,7 @@ def convert_openai_to_anthropic(
|
|
|
406
422
|
input=tool_input,
|
|
407
423
|
)
|
|
408
424
|
)
|
|
409
|
-
|
|
425
|
+
|
|
410
426
|
# Determine stop reason
|
|
411
427
|
stop_reason_map = {
|
|
412
428
|
"stop": "end_turn",
|
|
@@ -414,18 +430,24 @@ def convert_openai_to_anthropic(
|
|
|
414
430
|
"tool_calls": "tool_use",
|
|
415
431
|
"content_filter": "end_turn",
|
|
416
432
|
}
|
|
417
|
-
anthropic_stop_reason = stop_reason_map.get(
|
|
418
|
-
|
|
433
|
+
anthropic_stop_reason = stop_reason_map.get(
|
|
434
|
+
choice.finish_reason or "stop", "end_turn"
|
|
435
|
+
)
|
|
436
|
+
|
|
419
437
|
# Build usage dict with cache support (if available from upstream)
|
|
420
438
|
usage_dict = None
|
|
421
439
|
if completion.usage:
|
|
422
440
|
usage_dict = {
|
|
423
441
|
"input_tokens": completion.usage.prompt_tokens,
|
|
424
442
|
"output_tokens": completion.usage.completion_tokens,
|
|
425
|
-
"cache_creation_input_tokens": getattr(
|
|
426
|
-
|
|
443
|
+
"cache_creation_input_tokens": getattr(
|
|
444
|
+
completion.usage, "cache_creation_input_tokens", None
|
|
445
|
+
),
|
|
446
|
+
"cache_read_input_tokens": getattr(
|
|
447
|
+
completion.usage, "cache_read_input_tokens", None
|
|
448
|
+
),
|
|
427
449
|
}
|
|
428
|
-
|
|
450
|
+
|
|
429
451
|
# Build message dict to avoid Pydantic validation issues
|
|
430
452
|
message_dict = {
|
|
431
453
|
"id": completion.id,
|
|
@@ -437,5 +459,5 @@ def convert_openai_to_anthropic(
|
|
|
437
459
|
"stop_sequence": None,
|
|
438
460
|
"usage": usage_dict,
|
|
439
461
|
}
|
|
440
|
-
|
|
462
|
+
|
|
441
463
|
return Message.model_validate(message_dict)
|
local_openai2anthropic/router.py
CHANGED
|
@@ -47,7 +47,7 @@ def _generate_server_tool_id() -> str:
|
|
|
47
47
|
"""Generate Anthropic-style server tool use ID (srvtoolu_...)."""
|
|
48
48
|
# Generate 24 random alphanumeric characters
|
|
49
49
|
chars = string.ascii_lowercase + string.digits
|
|
50
|
-
random_part =
|
|
50
|
+
random_part = "".join(secrets.choice(chars) for _ in range(24))
|
|
51
51
|
return f"srvtoolu_{random_part}"
|
|
52
52
|
|
|
53
53
|
|
|
@@ -62,12 +62,16 @@ async def _stream_response(
|
|
|
62
62
|
Stream response from OpenAI and convert to Anthropic format.
|
|
63
63
|
"""
|
|
64
64
|
try:
|
|
65
|
-
async with client.stream(
|
|
65
|
+
async with client.stream(
|
|
66
|
+
"POST", url, headers=headers, json=json_data
|
|
67
|
+
) as response:
|
|
66
68
|
if response.status_code != 200:
|
|
67
69
|
error_body = await response.aread()
|
|
68
70
|
try:
|
|
69
71
|
error_json = json.loads(error_body.decode())
|
|
70
|
-
error_msg = error_json.get("error", {}).get(
|
|
72
|
+
error_msg = error_json.get("error", {}).get(
|
|
73
|
+
"message", error_body.decode()
|
|
74
|
+
)
|
|
71
75
|
except json.JSONDecodeError:
|
|
72
76
|
error_msg = error_body.decode()
|
|
73
77
|
|
|
@@ -98,7 +102,9 @@ async def _stream_response(
|
|
|
98
102
|
|
|
99
103
|
try:
|
|
100
104
|
chunk = json.loads(data)
|
|
101
|
-
logger.debug(
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"[OpenAI Stream Chunk] {json.dumps(chunk, ensure_ascii=False)}"
|
|
107
|
+
)
|
|
102
108
|
except json.JSONDecodeError:
|
|
103
109
|
continue
|
|
104
110
|
|
|
@@ -126,7 +132,9 @@ async def _stream_response(
|
|
|
126
132
|
},
|
|
127
133
|
},
|
|
128
134
|
}
|
|
129
|
-
logger.debug(
|
|
135
|
+
logger.debug(
|
|
136
|
+
f"[Anthropic Stream Event] message_start: {json.dumps(start_event, ensure_ascii=False)}"
|
|
137
|
+
)
|
|
130
138
|
yield f"event: message_start\ndata: {json.dumps(start_event)}\n\n"
|
|
131
139
|
first_chunk = False
|
|
132
140
|
continue
|
|
@@ -139,9 +147,28 @@ async def _stream_response(
|
|
|
139
147
|
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"
|
|
140
148
|
content_block_started = False
|
|
141
149
|
|
|
142
|
-
stop_reason_map = {
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
stop_reason_map = {
|
|
151
|
+
"stop": "end_turn",
|
|
152
|
+
"length": "max_tokens",
|
|
153
|
+
"tool_calls": "tool_use",
|
|
154
|
+
}
|
|
155
|
+
delta_event = {
|
|
156
|
+
"type": "message_delta",
|
|
157
|
+
"delta": {
|
|
158
|
+
"stop_reason": stop_reason_map.get(
|
|
159
|
+
finish_reason or "stop", "end_turn"
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
"usage": {
|
|
163
|
+
"input_tokens": usage.get("prompt_tokens", 0),
|
|
164
|
+
"output_tokens": usage.get("completion_tokens", 0),
|
|
165
|
+
"cache_creation_input_tokens": None,
|
|
166
|
+
"cache_read_input_tokens": None,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"[Anthropic Stream Event] message_delta: {json.dumps(delta_event, ensure_ascii=False)}"
|
|
171
|
+
)
|
|
145
172
|
yield f"event: message_delta\ndata: {json.dumps(delta_event)}\n\n"
|
|
146
173
|
continue
|
|
147
174
|
|
|
@@ -152,77 +179,133 @@ async def _stream_response(
|
|
|
152
179
|
if choice.get("finish_reason"):
|
|
153
180
|
finish_reason = choice["finish_reason"]
|
|
154
181
|
|
|
182
|
+
# When finish_reason is tool_calls, we need to close the current block
|
|
183
|
+
# and prepare to send message_delta
|
|
184
|
+
if finish_reason == "tool_calls" and content_block_started:
|
|
185
|
+
stop_block = {
|
|
186
|
+
"type": "content_block_stop",
|
|
187
|
+
"index": content_block_index,
|
|
188
|
+
}
|
|
189
|
+
logger.debug(
|
|
190
|
+
f"[Anthropic Stream Event] content_block_stop (tool_calls): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
191
|
+
)
|
|
192
|
+
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
193
|
+
content_block_started = False
|
|
194
|
+
|
|
155
195
|
# Handle reasoning content (thinking)
|
|
156
196
|
if delta.get("reasoning_content"):
|
|
157
197
|
reasoning = delta["reasoning_content"]
|
|
158
198
|
# Start thinking content block if not already started
|
|
159
|
-
if not content_block_started or current_block_type !=
|
|
199
|
+
if not content_block_started or current_block_type != "thinking":
|
|
160
200
|
# Close previous block if exists
|
|
161
201
|
if content_block_started:
|
|
162
|
-
stop_block = {
|
|
163
|
-
|
|
202
|
+
stop_block = {
|
|
203
|
+
"type": "content_block_stop",
|
|
204
|
+
"index": content_block_index,
|
|
205
|
+
}
|
|
206
|
+
logger.debug(
|
|
207
|
+
f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
208
|
+
)
|
|
164
209
|
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
165
210
|
content_block_index += 1
|
|
166
|
-
start_block = {
|
|
167
|
-
|
|
211
|
+
start_block = {
|
|
212
|
+
"type": "content_block_start",
|
|
213
|
+
"index": content_block_index,
|
|
214
|
+
"content_block": {"type": "thinking", "thinking": ""},
|
|
215
|
+
}
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"[Anthropic Stream Event] content_block_start (thinking): {json.dumps(start_block, ensure_ascii=False)}"
|
|
218
|
+
)
|
|
168
219
|
yield f"event: content_block_start\ndata: {json.dumps(start_block)}\n\n"
|
|
169
220
|
content_block_started = True
|
|
170
|
-
current_block_type =
|
|
221
|
+
current_block_type = "thinking"
|
|
171
222
|
|
|
172
|
-
delta_block = {
|
|
223
|
+
delta_block = {
|
|
224
|
+
"type": "content_block_delta",
|
|
225
|
+
"index": content_block_index,
|
|
226
|
+
"delta": {"type": "thinking_delta", "thinking": reasoning},
|
|
227
|
+
}
|
|
173
228
|
yield f"event: content_block_delta\ndata: {json.dumps(delta_block)}\n\n"
|
|
174
229
|
continue
|
|
175
230
|
|
|
176
231
|
# Handle content
|
|
177
232
|
if delta.get("content"):
|
|
178
|
-
if not content_block_started or current_block_type !=
|
|
233
|
+
if not content_block_started or current_block_type != "text":
|
|
179
234
|
# Close previous block if exists
|
|
180
235
|
if content_block_started:
|
|
181
|
-
stop_block = {
|
|
182
|
-
|
|
236
|
+
stop_block = {
|
|
237
|
+
"type": "content_block_stop",
|
|
238
|
+
"index": content_block_index,
|
|
239
|
+
}
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
242
|
+
)
|
|
183
243
|
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
184
244
|
content_block_index += 1
|
|
185
|
-
start_block = {
|
|
186
|
-
|
|
245
|
+
start_block = {
|
|
246
|
+
"type": "content_block_start",
|
|
247
|
+
"index": content_block_index,
|
|
248
|
+
"content_block": {"type": "text", "text": ""},
|
|
249
|
+
}
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"[Anthropic Stream Event] content_block_start (text): {json.dumps(start_block, ensure_ascii=False)}"
|
|
252
|
+
)
|
|
187
253
|
yield f"event: content_block_start\ndata: {json.dumps(start_block)}\n\n"
|
|
188
254
|
content_block_started = True
|
|
189
|
-
current_block_type =
|
|
255
|
+
current_block_type = "text"
|
|
190
256
|
|
|
191
|
-
delta_block = {
|
|
257
|
+
delta_block = {
|
|
258
|
+
"type": "content_block_delta",
|
|
259
|
+
"index": content_block_index,
|
|
260
|
+
"delta": {"type": "text_delta", "text": delta["content"]},
|
|
261
|
+
}
|
|
192
262
|
yield f"event: content_block_delta\ndata: {json.dumps(delta_block)}\n\n"
|
|
193
263
|
|
|
194
264
|
# Handle tool calls
|
|
195
|
-
|
|
196
|
-
|
|
265
|
+
tool_calls = delta.get("tool_calls", [])
|
|
266
|
+
if tool_calls:
|
|
267
|
+
tool_call = tool_calls[0]
|
|
197
268
|
|
|
269
|
+
# Handle new tool call (with id) - use separate if, not elif
|
|
270
|
+
# because a chunk may have both id AND arguments
|
|
198
271
|
if tool_call.get("id"):
|
|
199
272
|
if content_block_started:
|
|
200
273
|
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"
|
|
201
274
|
content_block_started = False
|
|
202
275
|
content_block_index += 1
|
|
203
276
|
|
|
204
|
-
func = tool_call.get(
|
|
277
|
+
func = tool_call.get("function") or {}
|
|
205
278
|
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"
|
|
206
279
|
content_block_started = True
|
|
207
|
-
current_block_type =
|
|
280
|
+
current_block_type = "tool_use"
|
|
208
281
|
|
|
209
|
-
|
|
210
|
-
|
|
282
|
+
# Handle tool call arguments - always check separately
|
|
283
|
+
# Note: This is intentionally NOT elif, as a single chunk may contain both
|
|
284
|
+
if (tool_call.get("function") or {}).get("arguments"):
|
|
285
|
+
args = (tool_call.get("function") or {}).get("arguments", "")
|
|
211
286
|
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"
|
|
212
287
|
|
|
213
288
|
# Close final content block
|
|
214
289
|
if content_block_started:
|
|
215
|
-
stop_block = {
|
|
216
|
-
|
|
290
|
+
stop_block = {
|
|
291
|
+
"type": "content_block_stop",
|
|
292
|
+
"index": content_block_index,
|
|
293
|
+
}
|
|
294
|
+
logger.debug(
|
|
295
|
+
f"[Anthropic Stream Event] content_block_stop (final): {json.dumps(stop_block, ensure_ascii=False)}"
|
|
296
|
+
)
|
|
217
297
|
yield f"event: content_block_stop\ndata: {json.dumps(stop_block)}\n\n"
|
|
218
298
|
|
|
219
299
|
# Message stop
|
|
220
|
-
stop_event = {
|
|
221
|
-
logger.debug(
|
|
300
|
+
stop_event = {"type": "message_stop"}
|
|
301
|
+
logger.debug(
|
|
302
|
+
f"[Anthropic Stream Event] message_stop: {json.dumps(stop_event, ensure_ascii=False)}"
|
|
303
|
+
)
|
|
222
304
|
yield f"event: message_stop\ndata: {json.dumps(stop_event)}\n\n"
|
|
223
305
|
|
|
224
306
|
except Exception as e:
|
|
225
307
|
import traceback
|
|
308
|
+
|
|
226
309
|
error_msg = f"{str(e)}\n{traceback.format_exc()}"
|
|
227
310
|
logger.error(f"Stream error: {error_msg}")
|
|
228
311
|
error_event = AnthropicErrorResponse(
|
|
@@ -237,17 +320,21 @@ async def _convert_result_to_stream(
|
|
|
237
320
|
) -> AsyncGenerator[str, None]:
|
|
238
321
|
"""Convert a JSONResponse to streaming SSE format."""
|
|
239
322
|
import time
|
|
240
|
-
|
|
323
|
+
|
|
241
324
|
body = json.loads(result.body)
|
|
242
325
|
message_id = body.get("id", f"msg_{int(time.time() * 1000)}")
|
|
243
326
|
content = body.get("content", [])
|
|
244
327
|
usage = body.get("usage", {})
|
|
245
328
|
stop_reason = body.get("stop_reason", "end_turn")
|
|
246
|
-
|
|
329
|
+
|
|
247
330
|
# Map stop_reason
|
|
248
|
-
stop_reason_map = {
|
|
331
|
+
stop_reason_map = {
|
|
332
|
+
"end_turn": "stop",
|
|
333
|
+
"max_tokens": "length",
|
|
334
|
+
"tool_use": "tool_calls",
|
|
335
|
+
}
|
|
249
336
|
openai_stop_reason = stop_reason_map.get(stop_reason, "stop")
|
|
250
|
-
|
|
337
|
+
|
|
251
338
|
# 1. message_start event
|
|
252
339
|
start_event = {
|
|
253
340
|
"type": "message_start",
|
|
@@ -268,7 +355,7 @@ async def _convert_result_to_stream(
|
|
|
268
355
|
},
|
|
269
356
|
}
|
|
270
357
|
yield f"event: message_start\ndata: {json.dumps(start_event)}\n\n"
|
|
271
|
-
|
|
358
|
+
|
|
272
359
|
# 2. Process content blocks
|
|
273
360
|
for i, block in enumerate(content):
|
|
274
361
|
block_type = block.get("type")
|
|
@@ -305,7 +392,7 @@ async def _convert_result_to_stream(
|
|
|
305
392
|
if thinking_text:
|
|
306
393
|
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': i, 'delta': {'type': 'thinking_delta', 'thinking': thinking_text}})}\n\n"
|
|
307
394
|
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\n\n"
|
|
308
|
-
|
|
395
|
+
|
|
309
396
|
# 3. message_delta with final usage
|
|
310
397
|
delta_event = {
|
|
311
398
|
"type": "message_delta",
|
|
@@ -319,7 +406,7 @@ async def _convert_result_to_stream(
|
|
|
319
406
|
},
|
|
320
407
|
}
|
|
321
408
|
yield f"event: message_delta\ndata: {json.dumps(delta_event)}\n\n"
|
|
322
|
-
|
|
409
|
+
|
|
323
410
|
# 4. message_stop
|
|
324
411
|
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
|
|
325
412
|
|
|
@@ -406,12 +493,16 @@ async def _handle_with_server_tools(
|
|
|
406
493
|
async with httpx.AsyncClient(timeout=settings.request_timeout) as client:
|
|
407
494
|
try:
|
|
408
495
|
# Log full request for debugging
|
|
409
|
-
logger.debug(
|
|
410
|
-
|
|
496
|
+
logger.debug(
|
|
497
|
+
f"Request body: {json.dumps(params, indent=2, default=str)[:3000]}"
|
|
498
|
+
)
|
|
499
|
+
|
|
411
500
|
response = await client.post(url, headers=headers, json=params)
|
|
412
501
|
|
|
413
502
|
if response.status_code != 200:
|
|
414
|
-
logger.error(
|
|
503
|
+
logger.error(
|
|
504
|
+
f"OpenAI API error: {response.status_code} - {response.text}"
|
|
505
|
+
)
|
|
415
506
|
error_response = AnthropicErrorResponse(
|
|
416
507
|
error=AnthropicError(type="api_error", message=response.text)
|
|
417
508
|
)
|
|
@@ -421,36 +512,45 @@ async def _handle_with_server_tools(
|
|
|
421
512
|
)
|
|
422
513
|
|
|
423
514
|
completion_data = response.json()
|
|
424
|
-
logger.debug(
|
|
515
|
+
logger.debug(
|
|
516
|
+
f"OpenAI response: {json.dumps(completion_data, indent=2)[:500]}..."
|
|
517
|
+
)
|
|
425
518
|
from openai.types.chat import ChatCompletion
|
|
519
|
+
|
|
426
520
|
completion = ChatCompletion.model_validate(completion_data)
|
|
427
521
|
|
|
428
522
|
# Check for server tool calls
|
|
429
523
|
server_tool_calls = []
|
|
430
524
|
other_tool_calls = []
|
|
431
|
-
|
|
525
|
+
|
|
432
526
|
tool_calls = completion.choices[0].message.tool_calls
|
|
433
|
-
logger.info(
|
|
527
|
+
logger.info(
|
|
528
|
+
f"Model returned tool_calls: {len(tool_calls) if tool_calls else 0}"
|
|
529
|
+
)
|
|
434
530
|
|
|
435
531
|
if tool_calls:
|
|
436
532
|
for tc in tool_calls:
|
|
437
533
|
func_name = tc.function.name if tc.function else ""
|
|
438
534
|
logger.info(f" Tool call: {func_name}")
|
|
439
|
-
|
|
535
|
+
|
|
440
536
|
# Generate Anthropic-style ID for server tools
|
|
441
|
-
is_server = handler.is_server_tool_call(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
537
|
+
is_server = handler.is_server_tool_call(
|
|
538
|
+
{
|
|
539
|
+
"id": tc.id,
|
|
540
|
+
"function": {"name": func_name, "arguments": ""},
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
|
|
446
544
|
# Use Anthropic-style ID for server tools, original ID otherwise
|
|
447
545
|
tool_id = _generate_server_tool_id() if is_server else tc.id
|
|
448
|
-
|
|
546
|
+
|
|
449
547
|
tc_dict = {
|
|
450
548
|
"id": tool_id,
|
|
451
549
|
"function": {
|
|
452
550
|
"name": func_name,
|
|
453
|
-
"arguments": tc.function.arguments
|
|
551
|
+
"arguments": tc.function.arguments
|
|
552
|
+
if tc.function
|
|
553
|
+
else "{}",
|
|
454
554
|
},
|
|
455
555
|
}
|
|
456
556
|
logger.info(f" Is server tool: {is_server}, ID: {tool_id}")
|
|
@@ -460,19 +560,25 @@ async def _handle_with_server_tools(
|
|
|
460
560
|
other_tool_calls.append(tc)
|
|
461
561
|
|
|
462
562
|
# No server tool calls - we're done
|
|
463
|
-
logger.info(
|
|
563
|
+
logger.info(
|
|
564
|
+
f"Server tool calls: {len(server_tool_calls)}, Other: {len(other_tool_calls)}"
|
|
565
|
+
)
|
|
464
566
|
if not server_tool_calls:
|
|
465
567
|
message = convert_openai_to_anthropic(completion, model)
|
|
466
568
|
|
|
467
569
|
if accumulated_content:
|
|
468
570
|
message_dict = message.model_dump()
|
|
469
|
-
message_dict["content"] =
|
|
470
|
-
|
|
571
|
+
message_dict["content"] = (
|
|
572
|
+
accumulated_content + message_dict.get("content", [])
|
|
573
|
+
)
|
|
574
|
+
|
|
471
575
|
if message_dict.get("usage"):
|
|
472
576
|
message_dict["usage"]["server_tool_use"] = handler.usage
|
|
473
|
-
|
|
577
|
+
|
|
474
578
|
# Log full response for debugging
|
|
475
|
-
logger.info(
|
|
579
|
+
logger.info(
|
|
580
|
+
f"Response content blocks: {json.dumps(message_dict.get('content', []), ensure_ascii=False)[:1000]}"
|
|
581
|
+
)
|
|
476
582
|
logger.info(f"Response usage: {message_dict.get('usage')}")
|
|
477
583
|
logger.info(f"Server tool use count: {handler.usage}")
|
|
478
584
|
|
|
@@ -489,6 +595,7 @@ async def _handle_with_server_tools(
|
|
|
489
595
|
tool_class = handler.server_tools.get(func_name)
|
|
490
596
|
if tool_class:
|
|
491
597
|
from local_openai2anthropic.server_tools import ToolResult
|
|
598
|
+
|
|
492
599
|
error_result = ToolResult(
|
|
493
600
|
success=False,
|
|
494
601
|
content=[],
|
|
@@ -520,14 +627,16 @@ async def _handle_with_server_tools(
|
|
|
520
627
|
accumulated_content.extend(content_blocks)
|
|
521
628
|
|
|
522
629
|
# Track for assistant message
|
|
523
|
-
assistant_tool_calls.append(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
"
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
630
|
+
assistant_tool_calls.append(
|
|
631
|
+
{
|
|
632
|
+
"id": call["id"],
|
|
633
|
+
"type": "function",
|
|
634
|
+
"function": {
|
|
635
|
+
"name": call["function"]["name"],
|
|
636
|
+
"arguments": call["function"]["arguments"],
|
|
637
|
+
},
|
|
638
|
+
}
|
|
639
|
+
)
|
|
531
640
|
tool_results.append(tool_result)
|
|
532
641
|
|
|
533
642
|
# Add to messages for next iteration
|
|
@@ -538,7 +647,9 @@ async def _handle_with_server_tools(
|
|
|
538
647
|
|
|
539
648
|
except httpx.TimeoutException:
|
|
540
649
|
error_response = AnthropicErrorResponse(
|
|
541
|
-
error=AnthropicError(
|
|
650
|
+
error=AnthropicError(
|
|
651
|
+
type="timeout_error", message="Request timed out"
|
|
652
|
+
)
|
|
542
653
|
)
|
|
543
654
|
raise HTTPException(
|
|
544
655
|
status_code=HTTPStatus.GATEWAY_TIMEOUT,
|
|
@@ -576,14 +687,18 @@ def _add_tool_results_to_messages(
|
|
|
576
687
|
# Add tool results
|
|
577
688
|
if is_error:
|
|
578
689
|
for call in tool_calls:
|
|
579
|
-
messages.append(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
"
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
690
|
+
messages.append(
|
|
691
|
+
{
|
|
692
|
+
"role": "tool",
|
|
693
|
+
"tool_call_id": call["id"],
|
|
694
|
+
"content": json.dumps(
|
|
695
|
+
{
|
|
696
|
+
"error": "max_uses_exceeded",
|
|
697
|
+
"message": "Maximum tool uses exceeded.",
|
|
698
|
+
}
|
|
699
|
+
),
|
|
700
|
+
}
|
|
701
|
+
)
|
|
587
702
|
elif tool_results:
|
|
588
703
|
messages.extend(tool_results)
|
|
589
704
|
|
|
@@ -611,12 +726,16 @@ async def create_message(
|
|
|
611
726
|
try:
|
|
612
727
|
body_bytes = await request.body()
|
|
613
728
|
body_json = json.loads(body_bytes.decode("utf-8"))
|
|
614
|
-
logger.debug(
|
|
729
|
+
logger.debug(
|
|
730
|
+
f"[Anthropic Request] {json.dumps(body_json, ensure_ascii=False, indent=2)}"
|
|
731
|
+
)
|
|
615
732
|
anthropic_params = body_json
|
|
616
733
|
except json.JSONDecodeError as e:
|
|
617
734
|
logger.error(f"Invalid JSON in request body: {e}")
|
|
618
735
|
error_response = AnthropicErrorResponse(
|
|
619
|
-
error=AnthropicError(
|
|
736
|
+
error=AnthropicError(
|
|
737
|
+
type="invalid_request_error", message=f"Invalid JSON: {e}"
|
|
738
|
+
)
|
|
620
739
|
)
|
|
621
740
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
622
741
|
except Exception as e:
|
|
@@ -629,28 +748,38 @@ async def create_message(
|
|
|
629
748
|
# Validate request shape early (avoid making upstream calls for obviously invalid requests)
|
|
630
749
|
if not isinstance(anthropic_params, dict):
|
|
631
750
|
error_response = AnthropicErrorResponse(
|
|
632
|
-
error=AnthropicError(
|
|
751
|
+
error=AnthropicError(
|
|
752
|
+
type="invalid_request_error",
|
|
753
|
+
message="Request body must be a JSON object",
|
|
754
|
+
)
|
|
633
755
|
)
|
|
634
756
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
635
757
|
|
|
636
758
|
model_value = anthropic_params.get("model")
|
|
637
759
|
if not isinstance(model_value, str) or not model_value.strip():
|
|
638
760
|
error_response = AnthropicErrorResponse(
|
|
639
|
-
error=AnthropicError(
|
|
761
|
+
error=AnthropicError(
|
|
762
|
+
type="invalid_request_error", message="Model must be a non-empty string"
|
|
763
|
+
)
|
|
640
764
|
)
|
|
641
765
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
642
766
|
|
|
643
767
|
messages_value = anthropic_params.get("messages")
|
|
644
768
|
if not isinstance(messages_value, list) or len(messages_value) == 0:
|
|
645
769
|
error_response = AnthropicErrorResponse(
|
|
646
|
-
error=AnthropicError(
|
|
770
|
+
error=AnthropicError(
|
|
771
|
+
type="invalid_request_error",
|
|
772
|
+
message="Messages must be a non-empty list",
|
|
773
|
+
)
|
|
647
774
|
)
|
|
648
775
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
649
776
|
|
|
650
777
|
max_tokens_value = anthropic_params.get("max_tokens")
|
|
651
778
|
if not isinstance(max_tokens_value, int):
|
|
652
779
|
error_response = AnthropicErrorResponse(
|
|
653
|
-
error=AnthropicError(
|
|
780
|
+
error=AnthropicError(
|
|
781
|
+
type="invalid_request_error", message="max_tokens is required"
|
|
782
|
+
)
|
|
654
783
|
)
|
|
655
784
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
656
785
|
|
|
@@ -668,10 +797,12 @@ async def create_message(
|
|
|
668
797
|
enabled_server_tools=enabled_server_tools if has_server_tools else None,
|
|
669
798
|
)
|
|
670
799
|
openai_params: dict[str, Any] = dict(openai_params_obj) # type: ignore
|
|
671
|
-
|
|
800
|
+
|
|
672
801
|
# Log converted OpenAI request (remove internal fields)
|
|
673
|
-
log_params = {k: v for k, v in openai_params.items() if not k.startswith(
|
|
674
|
-
logger.debug(
|
|
802
|
+
log_params = {k: v for k, v in openai_params.items() if not k.startswith("_")}
|
|
803
|
+
logger.debug(
|
|
804
|
+
f"[OpenAI Request] {json.dumps(log_params, ensure_ascii=False, indent=2)}"
|
|
805
|
+
)
|
|
675
806
|
|
|
676
807
|
stream = openai_params.get("stream", False)
|
|
677
808
|
model = openai_params.get("model", "")
|
|
@@ -698,7 +829,7 @@ async def create_message(
|
|
|
698
829
|
result = await _handle_with_server_tools(
|
|
699
830
|
openai_params, url, headers, settings, tool_classes, model
|
|
700
831
|
)
|
|
701
|
-
|
|
832
|
+
|
|
702
833
|
# If original request was streaming, convert result to streaming format
|
|
703
834
|
if stream:
|
|
704
835
|
return StreamingResponse(
|
|
@@ -728,20 +859,27 @@ async def create_message(
|
|
|
728
859
|
)
|
|
729
860
|
|
|
730
861
|
openai_completion = response.json()
|
|
731
|
-
logger.debug(
|
|
732
|
-
|
|
862
|
+
logger.debug(
|
|
863
|
+
f"[OpenAI Response] {json.dumps(openai_completion, ensure_ascii=False, indent=2)}"
|
|
864
|
+
)
|
|
865
|
+
|
|
733
866
|
from openai.types.chat import ChatCompletion
|
|
867
|
+
|
|
734
868
|
completion = ChatCompletion.model_validate(openai_completion)
|
|
735
869
|
anthropic_message = convert_openai_to_anthropic(completion, model)
|
|
736
|
-
|
|
870
|
+
|
|
737
871
|
anthropic_response = anthropic_message.model_dump()
|
|
738
|
-
logger.debug(
|
|
872
|
+
logger.debug(
|
|
873
|
+
f"[Anthropic Response] {json.dumps(anthropic_response, ensure_ascii=False, indent=2)}"
|
|
874
|
+
)
|
|
739
875
|
|
|
740
876
|
return JSONResponse(content=anthropic_response)
|
|
741
877
|
|
|
742
878
|
except httpx.TimeoutException:
|
|
743
879
|
error_response = AnthropicErrorResponse(
|
|
744
|
-
error=AnthropicError(
|
|
880
|
+
error=AnthropicError(
|
|
881
|
+
type="timeout_error", message="Request timed out"
|
|
882
|
+
)
|
|
745
883
|
)
|
|
746
884
|
raise HTTPException(
|
|
747
885
|
status_code=HTTPStatus.GATEWAY_TIMEOUT,
|
|
@@ -798,10 +936,14 @@ async def count_tokens(
|
|
|
798
936
|
try:
|
|
799
937
|
body_bytes = await request.body()
|
|
800
938
|
body_json = json.loads(body_bytes.decode("utf-8"))
|
|
801
|
-
logger.debug(
|
|
939
|
+
logger.debug(
|
|
940
|
+
f"[Count Tokens Request] {json.dumps(body_json, ensure_ascii=False, indent=2)}"
|
|
941
|
+
)
|
|
802
942
|
except json.JSONDecodeError as e:
|
|
803
943
|
error_response = AnthropicErrorResponse(
|
|
804
|
-
error=AnthropicError(
|
|
944
|
+
error=AnthropicError(
|
|
945
|
+
type="invalid_request_error", message=f"Invalid JSON: {e}"
|
|
946
|
+
)
|
|
805
947
|
)
|
|
806
948
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
807
949
|
except Exception as e:
|
|
@@ -813,14 +955,19 @@ async def count_tokens(
|
|
|
813
955
|
# Validate required fields
|
|
814
956
|
if not isinstance(body_json, dict):
|
|
815
957
|
error_response = AnthropicErrorResponse(
|
|
816
|
-
error=AnthropicError(
|
|
958
|
+
error=AnthropicError(
|
|
959
|
+
type="invalid_request_error",
|
|
960
|
+
message="Request body must be a JSON object",
|
|
961
|
+
)
|
|
817
962
|
)
|
|
818
963
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
819
964
|
|
|
820
965
|
messages = body_json.get("messages", [])
|
|
821
966
|
if not isinstance(messages, list):
|
|
822
967
|
error_response = AnthropicErrorResponse(
|
|
823
|
-
error=AnthropicError(
|
|
968
|
+
error=AnthropicError(
|
|
969
|
+
type="invalid_request_error", message="messages must be a list"
|
|
970
|
+
)
|
|
824
971
|
)
|
|
825
972
|
return JSONResponse(status_code=422, content=error_response.model_dump())
|
|
826
973
|
|
|
@@ -831,13 +978,13 @@ async def count_tokens(
|
|
|
831
978
|
try:
|
|
832
979
|
# Use tiktoken for token counting
|
|
833
980
|
import tiktoken
|
|
834
|
-
|
|
981
|
+
|
|
835
982
|
# Map model names to tiktoken encoding
|
|
836
983
|
# Claude models don't have direct tiktoken encodings, so we use cl100k_base as approximation
|
|
837
984
|
encoding = tiktoken.get_encoding("cl100k_base")
|
|
838
|
-
|
|
985
|
+
|
|
839
986
|
total_tokens = 0
|
|
840
|
-
|
|
987
|
+
|
|
841
988
|
# Count system prompt tokens if present
|
|
842
989
|
if system:
|
|
843
990
|
if isinstance(system, str):
|
|
@@ -846,7 +993,7 @@ async def count_tokens(
|
|
|
846
993
|
for block in system:
|
|
847
994
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
848
995
|
total_tokens += len(encoding.encode(block.get("text", "")))
|
|
849
|
-
|
|
996
|
+
|
|
850
997
|
# Count message tokens
|
|
851
998
|
for msg in messages:
|
|
852
999
|
content = msg.get("content", "")
|
|
@@ -861,24 +1008,24 @@ async def count_tokens(
|
|
|
861
1008
|
# Images are typically counted as a fixed number of tokens
|
|
862
1009
|
# This is an approximation
|
|
863
1010
|
total_tokens += 85 # Standard approximation for images
|
|
864
|
-
|
|
1011
|
+
|
|
865
1012
|
# Count tool definitions tokens
|
|
866
1013
|
if tools:
|
|
867
1014
|
for tool in tools:
|
|
868
1015
|
tool_def = tool if isinstance(tool, dict) else tool.model_dump()
|
|
869
1016
|
# Rough approximation for tool definitions
|
|
870
1017
|
total_tokens += len(encoding.encode(json.dumps(tool_def)))
|
|
871
|
-
|
|
1018
|
+
|
|
872
1019
|
logger.debug(f"[Count Tokens Response] input_tokens: {total_tokens}")
|
|
873
|
-
|
|
874
|
-
return JSONResponse(content={
|
|
875
|
-
|
|
876
|
-
})
|
|
877
|
-
|
|
1020
|
+
|
|
1021
|
+
return JSONResponse(content={"input_tokens": total_tokens})
|
|
1022
|
+
|
|
878
1023
|
except Exception as e:
|
|
879
1024
|
logger.error(f"Token counting error: {e}")
|
|
880
1025
|
error_response = AnthropicErrorResponse(
|
|
881
|
-
error=AnthropicError(
|
|
1026
|
+
error=AnthropicError(
|
|
1027
|
+
type="internal_error", message=f"Failed to count tokens: {str(e)}"
|
|
1028
|
+
)
|
|
882
1029
|
)
|
|
883
1030
|
return JSONResponse(status_code=500, content=error_response.model_dump())
|
|
884
1031
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-openai2anthropic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: A lightweight proxy server that converts Anthropic Messages API to OpenAI API
|
|
5
5
|
Project-URL: Homepage, https://github.com/dongfangzan/local-openai2anthropic
|
|
6
6
|
Project-URL: Repository, https://github.com/dongfangzan/local-openai2anthropic
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
local_openai2anthropic/__init__.py,sha256=IEn8YcQGsaEaCr04s3hS2AcgsIt5NU5Qa2C8Uwz7RdY,1059
|
|
2
2
|
local_openai2anthropic/__main__.py,sha256=K21u5u7FN8-DbO67TT_XDF0neGqJeFrVNkteRauCRQk,179
|
|
3
3
|
local_openai2anthropic/config.py,sha256=3M5ZAz3uYNMGxaottEBseEOZF-GnVaGuioH9Hpmgnd8,1918
|
|
4
|
-
local_openai2anthropic/converter.py,sha256
|
|
4
|
+
local_openai2anthropic/converter.py,sha256=-cxPlZIPcey4LFIb7250YLlhLntN2uuh1YUpWGCsmfQ,15969
|
|
5
5
|
local_openai2anthropic/daemon.py,sha256=pZnRojGFcuIpR8yLDNjV-b0LJRBVhgRAa-dKeRRse44,10017
|
|
6
6
|
local_openai2anthropic/daemon_runner.py,sha256=rguOH0PgpbjqNsKYei0uCQX8JQOQ1wmtQH1CtW95Dbw,3274
|
|
7
7
|
local_openai2anthropic/main.py,sha256=FK5JBBpzB_T44y3N16lPl1hK4ht4LEQqRKzVmkIjIoo,9866
|
|
8
8
|
local_openai2anthropic/openai_types.py,sha256=jFdCvLwtXYoo5gGRqOhbHQcVaxcsxNnCP_yFPIv7rG4,3823
|
|
9
9
|
local_openai2anthropic/protocol.py,sha256=vUEgxtRPFll6jEtLc4DyxTLCBjrWIEScZXhEqe4uibk,5185
|
|
10
|
-
local_openai2anthropic/router.py,sha256=
|
|
10
|
+
local_openai2anthropic/router.py,sha256=YZkpncYX9bI1VYtUE7srSImF3o0FZLAfM1Lfc2G-AeI,43927
|
|
11
11
|
local_openai2anthropic/tavily_client.py,sha256=QsBhnyF8BFWPAxB4XtWCCpHCquNL5SW93-zjTTi4Meg,3774
|
|
12
12
|
local_openai2anthropic/server_tools/__init__.py,sha256=QlJfjEta-HOCtLe7NaY_fpbEKv-ZpInjAnfmSqE9tbk,615
|
|
13
13
|
local_openai2anthropic/server_tools/base.py,sha256=pNFsv-jSgxVrkY004AHAcYMNZgVSO8ZOeCzQBUtQ3vU,5633
|
|
14
14
|
local_openai2anthropic/server_tools/web_search.py,sha256=1C7lX_cm-tMaN3MsCjinEZYPJc_Hj4yAxYay9h8Zbvs,6543
|
|
15
|
-
local_openai2anthropic-0.2.
|
|
16
|
-
local_openai2anthropic-0.2.
|
|
17
|
-
local_openai2anthropic-0.2.
|
|
18
|
-
local_openai2anthropic-0.2.
|
|
19
|
-
local_openai2anthropic-0.2.
|
|
15
|
+
local_openai2anthropic-0.2.9.dist-info/METADATA,sha256=tDbUvOtBGJo5DXRzVfxm7UH7XgGzRTLvokU_XcrKnkA,11240
|
|
16
|
+
local_openai2anthropic-0.2.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
local_openai2anthropic-0.2.9.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
|
|
18
|
+
local_openai2anthropic-0.2.9.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
|
|
19
|
+
local_openai2anthropic-0.2.9.dist-info/RECORD,,
|
|
File without changes
|
{local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{local_openai2anthropic-0.2.7.dist-info → local_openai2anthropic-0.2.9.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|