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.
@@ -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(f"Converted {msg_count} messages, total OpenAI messages: {len(openai_messages)}")
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 (enabled_server_tools or []):
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 = tool_choice if isinstance(tool_choice, dict) else tool_choice.model_dump()
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
- "type": "image_url",
251
- "image_url": {"url": url},
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
- "id": tool_id,
267
- "type": "function",
268
- "function": {
269
- "name": name,
270
- "arguments": json.dumps(input_data) if isinstance(input_data, dict) else str(input_data),
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, 'reasoning_content', None)
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(choice.finish_reason or "stop", "end_turn")
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(completion.usage, "cache_creation_input_tokens", None),
426
- "cache_read_input_tokens": getattr(completion.usage, "cache_read_input_tokens", None),
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)
@@ -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 = ''.join(secrets.choice(chars) for _ in range(24))
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("POST", url, headers=headers, json=json_data) as response:
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("message", error_body.decode())
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(f"[OpenAI Stream Chunk] {json.dumps(chunk, ensure_ascii=False)}")
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(f"[Anthropic Stream Event] message_start: {json.dumps(start_event, ensure_ascii=False)}")
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 = {"stop": "end_turn", "length": "max_tokens", "tool_calls": "tool_use"}
143
- delta_event = {'type': 'message_delta', 'delta': {'stop_reason': stop_reason_map.get(finish_reason or 'stop', 'end_turn')}, 'usage': {'input_tokens': usage.get('prompt_tokens', 0), 'output_tokens': usage.get('completion_tokens', 0), 'cache_creation_input_tokens': None, 'cache_read_input_tokens': None}}
144
- logger.debug(f"[Anthropic Stream Event] message_delta: {json.dumps(delta_event, ensure_ascii=False)}")
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 != 'thinking':
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 = {'type': 'content_block_stop', 'index': content_block_index}
163
- logger.debug(f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}")
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 = {'type': 'content_block_start', 'index': content_block_index, 'content_block': {'type': 'thinking', 'thinking': ''}}
167
- logger.debug(f"[Anthropic Stream Event] content_block_start (thinking): {json.dumps(start_block, ensure_ascii=False)}")
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 = 'thinking'
221
+ current_block_type = "thinking"
171
222
 
172
- delta_block = {'type': 'content_block_delta', 'index': content_block_index, 'delta': {'type': 'thinking_delta', 'thinking': reasoning}}
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 != 'text':
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 = {'type': 'content_block_stop', 'index': content_block_index}
182
- logger.debug(f"[Anthropic Stream Event] content_block_stop ({current_block_type}): {json.dumps(stop_block, ensure_ascii=False)}")
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 = {'type': 'content_block_start', 'index': content_block_index, 'content_block': {'type': 'text', 'text': ''}}
186
- logger.debug(f"[Anthropic Stream Event] content_block_start (text): {json.dumps(start_block, ensure_ascii=False)}")
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 = 'text'
255
+ current_block_type = "text"
190
256
 
191
- delta_block = {'type': 'content_block_delta', 'index': content_block_index, 'delta': {'type': 'text_delta', 'text': delta['content']}}
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
- if delta.get("tool_calls"):
196
- tool_call = delta["tool_calls"][0]
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('function') or {}
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 = 'tool_use'
280
+ current_block_type = "tool_use"
208
281
 
209
- elif (tool_call.get('function') or {}).get("arguments"):
210
- args = (tool_call.get('function') or {}).get("arguments", "")
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 = {'type': 'content_block_stop', 'index': content_block_index}
216
- logger.debug(f"[Anthropic Stream Event] content_block_stop (final): {json.dumps(stop_block, ensure_ascii=False)}")
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 = {'type': 'message_stop'}
221
- logger.debug(f"[Anthropic Stream Event] message_stop: {json.dumps(stop_event, ensure_ascii=False)}")
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 = {"end_turn": "stop", "max_tokens": "length", "tool_use": "tool_calls"}
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(f"Request body: {json.dumps(params, indent=2, default=str)[:3000]}")
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(f"OpenAI API error: {response.status_code} - {response.text}")
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(f"OpenAI response: {json.dumps(completion_data, indent=2)[:500]}...")
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(f"Model returned tool_calls: {len(tool_calls) if tool_calls else 0}")
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
- "id": tc.id,
443
- "function": {"name": func_name, "arguments": ""},
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 if tc.function else "{}",
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(f"Server tool calls: {len(server_tool_calls)}, Other: {len(other_tool_calls)}")
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"] = accumulated_content + message_dict.get("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(f"Response content blocks: {json.dumps(message_dict.get('content', []), ensure_ascii=False)[:1000]}")
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
- "id": call["id"],
525
- "type": "function",
526
- "function": {
527
- "name": call["function"]["name"],
528
- "arguments": call["function"]["arguments"],
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(type="timeout_error", message="Request timed out")
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
- "role": "tool",
581
- "tool_call_id": call["id"],
582
- "content": json.dumps({
583
- "error": "max_uses_exceeded",
584
- "message": "Maximum tool uses exceeded.",
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(f"[Anthropic Request] {json.dumps(body_json, ensure_ascii=False, indent=2)}")
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(type="invalid_request_error", message=f"Invalid JSON: {e}")
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(type="invalid_request_error", message="Request body must be a JSON object")
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(type="invalid_request_error", message="Model must be a non-empty string")
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(type="invalid_request_error", message="Messages must be a non-empty list")
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(type="invalid_request_error", message="max_tokens is required")
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(f"[OpenAI Request] {json.dumps(log_params, ensure_ascii=False, indent=2)}")
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(f"[OpenAI Response] {json.dumps(openai_completion, ensure_ascii=False, indent=2)}")
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(f"[Anthropic Response] {json.dumps(anthropic_response, ensure_ascii=False, indent=2)}")
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(type="timeout_error", message="Request timed out")
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(f"[Count Tokens Request] {json.dumps(body_json, ensure_ascii=False, indent=2)}")
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(type="invalid_request_error", message=f"Invalid JSON: {e}")
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(type="invalid_request_error", message="Request body must be a JSON object")
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(type="invalid_request_error", message="messages must be a list")
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
- "input_tokens": total_tokens
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(type="internal_error", message=f"Failed to count tokens: {str(e)}")
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.7
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=d-qYwtv6FIbpKSRsZN4jhnKM4D4k52la-_bpEYPTAS0,15790
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=imzvgduneiniwHroTgeT9d8q4iF5GAuptaVP38sakUg,40226
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.7.dist-info/METADATA,sha256=eA34CtgLACHsE4gf4Scuj7yU5IBg_Ys26x8nMnCd_eM,11240
16
- local_openai2anthropic-0.2.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- local_openai2anthropic-0.2.7.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
18
- local_openai2anthropic-0.2.7.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
19
- local_openai2anthropic-0.2.7.dist-info/RECORD,,
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,,