local-openai2anthropic 0.2.3__py3-none-any.whl → 0.2.4__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.
@@ -3,7 +3,7 @@
3
3
  local-openai2anthropic: A proxy server that converts Anthropic Messages API to OpenAI API.
4
4
  """
5
5
 
6
- __version__ = "0.2.3"
6
+ __version__ = "0.2.4"
7
7
 
8
8
  from local_openai2anthropic.protocol import (
9
9
  AnthropicError,
@@ -786,6 +786,103 @@ async def list_models(
786
786
  )
787
787
 
788
788
 
789
+ @router.post("/v1/messages/count_tokens")
790
+ async def count_tokens(
791
+ request: Request,
792
+ settings: Settings = Depends(get_request_settings),
793
+ ) -> JSONResponse:
794
+ """
795
+ Count tokens in messages without creating a message.
796
+ Uses tiktoken for local token counting.
797
+ """
798
+ try:
799
+ body_bytes = await request.body()
800
+ 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)}")
802
+ except json.JSONDecodeError as e:
803
+ error_response = AnthropicErrorResponse(
804
+ error=AnthropicError(type="invalid_request_error", message=f"Invalid JSON: {e}")
805
+ )
806
+ return JSONResponse(status_code=422, content=error_response.model_dump())
807
+ except Exception as e:
808
+ error_response = AnthropicErrorResponse(
809
+ error=AnthropicError(type="invalid_request_error", message=str(e))
810
+ )
811
+ return JSONResponse(status_code=400, content=error_response.model_dump())
812
+
813
+ # Validate required fields
814
+ if not isinstance(body_json, dict):
815
+ error_response = AnthropicErrorResponse(
816
+ error=AnthropicError(type="invalid_request_error", message="Request body must be a JSON object")
817
+ )
818
+ return JSONResponse(status_code=422, content=error_response.model_dump())
819
+
820
+ messages = body_json.get("messages", [])
821
+ if not isinstance(messages, list):
822
+ error_response = AnthropicErrorResponse(
823
+ error=AnthropicError(type="invalid_request_error", message="messages must be a list")
824
+ )
825
+ return JSONResponse(status_code=422, content=error_response.model_dump())
826
+
827
+ model = body_json.get("model", "")
828
+ system = body_json.get("system")
829
+ tools = body_json.get("tools", [])
830
+
831
+ try:
832
+ # Use tiktoken for token counting
833
+ import tiktoken
834
+
835
+ # Map model names to tiktoken encoding
836
+ # Claude models don't have direct tiktoken encodings, so we use cl100k_base as approximation
837
+ encoding = tiktoken.get_encoding("cl100k_base")
838
+
839
+ total_tokens = 0
840
+
841
+ # Count system prompt tokens if present
842
+ if system:
843
+ if isinstance(system, str):
844
+ total_tokens += len(encoding.encode(system))
845
+ elif isinstance(system, list):
846
+ for block in system:
847
+ if isinstance(block, dict) and block.get("type") == "text":
848
+ total_tokens += len(encoding.encode(block.get("text", "")))
849
+
850
+ # Count message tokens
851
+ for msg in messages:
852
+ content = msg.get("content", "")
853
+ if isinstance(content, str):
854
+ total_tokens += len(encoding.encode(content))
855
+ elif isinstance(content, list):
856
+ for block in content:
857
+ if isinstance(block, dict):
858
+ if block.get("type") == "text":
859
+ total_tokens += len(encoding.encode(block.get("text", "")))
860
+ elif block.get("type") == "image":
861
+ # Images are typically counted as a fixed number of tokens
862
+ # This is an approximation
863
+ total_tokens += 85 # Standard approximation for images
864
+
865
+ # Count tool definitions tokens
866
+ if tools:
867
+ for tool in tools:
868
+ tool_def = tool if isinstance(tool, dict) else tool.model_dump()
869
+ # Rough approximation for tool definitions
870
+ total_tokens += len(encoding.encode(json.dumps(tool_def)))
871
+
872
+ logger.debug(f"[Count Tokens Response] input_tokens: {total_tokens}")
873
+
874
+ return JSONResponse(content={
875
+ "input_tokens": total_tokens
876
+ })
877
+
878
+ except Exception as e:
879
+ logger.error(f"Token counting error: {e}")
880
+ error_response = AnthropicErrorResponse(
881
+ error=AnthropicError(type="internal_error", message=f"Failed to count tokens: {str(e)}")
882
+ )
883
+ return JSONResponse(status_code=500, content=error_response.model_dump())
884
+
885
+
789
886
  @router.get("/health")
790
887
  async def health_check() -> dict[str, str]:
791
888
  """Health check endpoint."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: local-openai2anthropic
3
- Version: 0.2.3
3
+ Version: 0.2.4
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,4 +1,4 @@
1
- local_openai2anthropic/__init__.py,sha256=bj8tRC4_GyO5x4A5NqRdpxWWrdhAi7pC8xN9-ui0bQo,1059
1
+ local_openai2anthropic/__init__.py,sha256=xPWXxEgbns2l2aiZzW0BDbNBkjcfXK-Ee-2ukgjQPKc,1059
2
2
  local_openai2anthropic/__main__.py,sha256=K21u5u7FN8-DbO67TT_XDF0neGqJeFrVNkteRauCRQk,179
3
3
  local_openai2anthropic/config.py,sha256=bnM7p5htd6rHgLn7Z0Ukmm2jVImLuVjIB5Cnfpf2ClY,1918
4
4
  local_openai2anthropic/converter.py,sha256=qp0LPJBTP0uAb_5l9VINZ03RAjmumxdquP6JqWXiZkQ,15779
@@ -7,13 +7,13 @@ local_openai2anthropic/daemon_runner.py,sha256=rguOH0PgpbjqNsKYei0uCQX8JQOQ1wmtQ
7
7
  local_openai2anthropic/main.py,sha256=5tdgPel8RSCn1iK0d7hYAmcTM9vYHlepgQujaEXA2ic,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=5c9APWIIkM2pi4C6AZ0OWP_yrE6wn5YQmJo1OOHcuVo,36101
10
+ local_openai2anthropic/router.py,sha256=KDIsckdQLx78z5rmVX8Zhr5zWO9m_qB-BjQbTwWjj0s,40224
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.3.dist-info/METADATA,sha256=auO3568iC566_VVykvf8x7oZylGVBhu0qW_zuAgp5WQ,10040
16
- local_openai2anthropic-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- local_openai2anthropic-0.2.3.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
18
- local_openai2anthropic-0.2.3.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
19
- local_openai2anthropic-0.2.3.dist-info/RECORD,,
15
+ local_openai2anthropic-0.2.4.dist-info/METADATA,sha256=nWz75h6XmZzWk3BdkMhTZNT0xlUmUSNmx2jgyFONS10,10040
16
+ local_openai2anthropic-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ local_openai2anthropic-0.2.4.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
18
+ local_openai2anthropic-0.2.4.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
19
+ local_openai2anthropic-0.2.4.dist-info/RECORD,,