ccproxy-api 0.2.0a4__py3-none-any.whl → 0.2.3__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.
ccproxy/core/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.0a4'
32
- __version_tuple__ = version_tuple = (0, 2, 0, 'a4')
31
+ __version__ = version = '0.2.3'
32
+ __version_tuple__ = version_tuple = (0, 2, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,44 @@
1
+ """Shared helpers for Anthropic to OpenAI formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from ccproxy.llms.models import openai as openai_models
9
+
10
+
11
+ def serialize_tool_arguments(tool_input: Any) -> str:
12
+ if isinstance(tool_input, str):
13
+ return tool_input
14
+ try:
15
+ return json.dumps(tool_input, ensure_ascii=False)
16
+ except Exception:
17
+ return json.dumps({"arguments": str(tool_input)})
18
+
19
+
20
+ def build_openai_tool_call(
21
+ *,
22
+ tool_id: str | None,
23
+ tool_name: str | None,
24
+ tool_input: Any,
25
+ arguments: Any = None,
26
+ fallback_index: int = 0,
27
+ ) -> openai_models.ToolCall:
28
+ args_str = (
29
+ arguments
30
+ if isinstance(arguments, str) and arguments
31
+ else serialize_tool_arguments(tool_input)
32
+ )
33
+ call_id = (
34
+ tool_id if isinstance(tool_id, str) and tool_id else f"call_{fallback_index}"
35
+ )
36
+ name = tool_name if isinstance(tool_name, str) and tool_name else "function"
37
+
38
+ return openai_models.ToolCall(
39
+ id=str(call_id),
40
+ function=openai_models.FunctionCall(
41
+ name=str(name),
42
+ arguments=str(args_str),
43
+ ),
44
+ )
@@ -14,6 +14,8 @@ from ccproxy.llms.formatters.constants import ANTHROPIC_TO_OPENAI_FINISH_REASON
14
14
  from ccproxy.llms.models import anthropic as anthropic_models
15
15
  from ccproxy.llms.models import openai as openai_models
16
16
 
17
+ from ._helpers import build_openai_tool_call
18
+
17
19
 
18
20
  logger = ccproxy.core.logging.get_logger(__name__)
19
21
 
@@ -101,6 +103,8 @@ def convert__anthropic_message_to_openai_chat__response(
101
103
  """Convert Anthropic MessageResponse to an OpenAI ChatCompletionResponse."""
102
104
  content_blocks = response.content
103
105
  parts: list[str] = []
106
+ tool_calls: list[openai_models.ToolCall] = []
107
+
104
108
  for block in content_blocks:
105
109
  btype = getattr(block, "type", None)
106
110
  if btype == "text":
@@ -117,8 +121,17 @@ def convert__anthropic_message_to_openai_chat__response(
117
121
  else ""
118
122
  )
119
123
  parts.append(f"<thinking{sig_attr}>{thinking}</thinking>")
124
+ elif btype == "tool_use":
125
+ tool_calls.append(
126
+ build_openai_tool_call(
127
+ tool_id=getattr(block, "id", None),
128
+ tool_name=getattr(block, "name", None),
129
+ tool_input=getattr(block, "input", {}) or {},
130
+ fallback_index=len(tool_calls),
131
+ )
132
+ )
120
133
 
121
- content_text = "".join(parts)
134
+ content_text = "".join(parts) if parts else None
122
135
 
123
136
  stop_reason = response.stop_reason
124
137
  finish_reason = ANTHROPIC_TO_OPENAI_FINISH_REASON.get(
@@ -127,12 +140,16 @@ def convert__anthropic_message_to_openai_chat__response(
127
140
 
128
141
  usage_model = convert__anthropic_usage_to_openai_completion__usage(response.usage)
129
142
 
143
+ message_dict: dict[str, Any] = {"role": "assistant", "content": content_text}
144
+ if tool_calls:
145
+ message_dict["tool_calls"] = [call.model_dump() for call in tool_calls]
146
+
130
147
  payload = {
131
148
  "id": response.id,
132
149
  "choices": [
133
150
  {
134
151
  "index": 0,
135
- "message": {"role": "assistant", "content": content_text},
152
+ "message": message_dict,
136
153
  "finish_reason": finish_reason,
137
154
  }
138
155
  ],
@@ -27,10 +27,9 @@ from ccproxy.llms.models import anthropic as anthropic_models
27
27
  from ccproxy.llms.models import openai as openai_models
28
28
  from ccproxy.llms.streaming.accumulators import ClaudeAccumulator
29
29
 
30
+ from ._helpers import build_openai_tool_call
30
31
  from .requests import _build_responses_payload_from_anthropic_request
31
- from .responses import (
32
- convert__anthropic_usage_to_openai_responses__usage,
33
- )
32
+ from .responses import convert__anthropic_usage_to_openai_responses__usage
34
33
 
35
34
 
36
35
  logger = ccproxy.core.logging.get_logger(__name__)
@@ -100,22 +99,15 @@ def _build_openai_tool_call(
100
99
  function_payload = (
101
100
  tool_call.get("function", {}) if isinstance(tool_call, dict) else {}
102
101
  )
103
- name = function_payload.get("name") or tool_call.get("name") or "function"
102
+ tool_name = function_payload.get("name") or tool_call.get("name")
104
103
  arguments = function_payload.get("arguments")
105
- if not isinstance(arguments, str) or not arguments:
106
- try:
107
- arguments = json.dumps(tool_call.get("input", {}), ensure_ascii=False)
108
- except Exception:
109
- arguments = json.dumps(tool_call.get("input", {}))
110
-
111
- tool_id = tool_call.get("id") or f"call_{block_index}"
112
104
 
113
- return openai_models.ToolCall(
114
- id=str(tool_id),
115
- function=openai_models.FunctionCall(
116
- name=str(name),
117
- arguments=str(arguments),
118
- ),
105
+ return build_openai_tool_call(
106
+ tool_id=tool_call.get("id"),
107
+ tool_name=tool_name,
108
+ tool_input=tool_call.get("input", {}),
109
+ arguments=arguments,
110
+ fallback_index=block_index,
119
111
  )
120
112
 
121
113
  return None
@@ -85,6 +85,11 @@ class ClaudeAPIAdapter(BaseHTTPAdapter):
85
85
  # Always set Authorization from OAuth-managed access token
86
86
  filtered_headers["authorization"] = f"Bearer {token_value}"
87
87
 
88
+ # PATCH: Add Computer Use beta headers for Anthropic API
89
+ # These are required for browser automation tools to work
90
+ filtered_headers["anthropic-version"] = "2023-06-01"
91
+ filtered_headers["anthropic-beta"] = "computer-use-2025-01-24"
92
+
88
93
  # Add CLI headers if available, but never allow overriding auth
89
94
  cli_headers = self._collect_cli_headers()
90
95
  if cli_headers:
@@ -15,6 +15,77 @@ from .models import ClaudeCredentials, ClaudeProfileInfo
15
15
  logger = get_plugin_logger()
16
16
 
17
17
 
18
+ # Keychain service name used by Claude Code
19
+ KEYCHAIN_SERVICE = "Claude Code"
20
+ KEYCHAIN_ACCOUNT = "credentials"
21
+
22
+
23
+ def _is_keyring_available() -> bool:
24
+ """Check if keyring library is available."""
25
+ try:
26
+ import keyring # noqa: F401
27
+
28
+ return True
29
+ except ImportError:
30
+ return False
31
+
32
+
33
+ async def _read_from_keychain() -> dict[str, Any] | None:
34
+ """Read Claude credentials from system keychain.
35
+
36
+ Claude Code stores OAuth credentials in the system keychain and intentionally
37
+ deletes the plain text ~/.claude/.credentials.json file for security.
38
+ See: https://github.com/anthropics/claude-code/issues/1414
39
+
40
+ Uses the keyring library which supports:
41
+ - macOS Keychain
42
+ - Windows Credential Manager
43
+ - Linux Secret Service (GNOME Keyring, KDE Wallet)
44
+
45
+ Returns:
46
+ Parsed credentials dict or None if not found or keyring unavailable
47
+ """
48
+ if not _is_keyring_available():
49
+ logger.debug(
50
+ "keyring_not_available",
51
+ hint="Install keyring package for system keychain support",
52
+ category="auth",
53
+ )
54
+ return None
55
+
56
+ def read_keychain() -> dict[str, Any] | None:
57
+ try:
58
+ import keyring
59
+
60
+ password = keyring.get_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
61
+ if password:
62
+ parsed = json.loads(password)
63
+ if isinstance(parsed, dict):
64
+ return parsed
65
+ logger.debug(
66
+ "keychain_invalid_format",
67
+ expected="dict",
68
+ got=type(parsed).__name__,
69
+ category="auth",
70
+ )
71
+ except json.JSONDecodeError as e:
72
+ logger.debug(
73
+ "keychain_json_decode_error",
74
+ error=str(e),
75
+ category="auth",
76
+ )
77
+ except Exception as e:
78
+ logger.debug(
79
+ "keychain_read_error",
80
+ error=str(e),
81
+ error_type=type(e).__name__,
82
+ category="auth",
83
+ )
84
+ return None
85
+
86
+ return await asyncio.to_thread(read_keychain)
87
+
88
+
18
89
  class ClaudeOAuthStorage(BaseJsonStorage[ClaudeCredentials]):
19
90
  """Claude OAuth-specific token storage implementation."""
20
91
 
@@ -61,24 +132,48 @@ class ClaudeOAuthStorage(BaseJsonStorage[ClaudeCredentials]):
61
132
  return False
62
133
 
63
134
  async def load(self) -> ClaudeCredentials | None:
64
- """Load Claude credentials.
135
+ """Load Claude credentials from file or system keychain.
136
+
137
+ Claude Code stores credentials in the system keychain and intentionally
138
+ deletes the plain text file for security. This method tries file first,
139
+ then falls back to the system keychain (macOS Keychain, Windows Credential
140
+ Manager, or Linux Secret Service).
65
141
 
66
142
  Returns:
67
143
  Stored credentials or None
68
144
  """
69
145
  try:
70
- # Use parent class's read method
146
+ # Try file first (works on all platforms, manual setups)
71
147
  data = await self._read_json()
72
- if not data:
73
- return None
148
+ if data:
149
+ credentials = ClaudeCredentials.model_validate(data)
150
+ logger.debug(
151
+ "claude_oauth_credentials_loaded",
152
+ has_oauth=bool(credentials.claude_ai_oauth),
153
+ source="file",
154
+ category="auth",
155
+ )
156
+ return credentials
157
+
158
+ # Fallback to system keychain (where Claude Code stores credentials)
159
+ keychain_data = await _read_from_keychain()
160
+ if keychain_data:
161
+ credentials = ClaudeCredentials.model_validate(keychain_data)
162
+ logger.debug(
163
+ "claude_oauth_credentials_loaded",
164
+ has_oauth=bool(credentials.claude_ai_oauth),
165
+ source="keychain",
166
+ category="auth",
167
+ )
168
+ return credentials
74
169
 
75
- credentials = ClaudeCredentials.model_validate(data)
76
170
  logger.debug(
77
- "claude_oauth_credentials_loaded",
78
- has_oauth=bool(credentials.claude_ai_oauth),
171
+ "claude_oauth_credentials_not_found",
172
+ checked_file=str(self.file_path),
173
+ checked_keychain=_is_keyring_available(),
79
174
  category="auth",
80
175
  )
81
- return credentials
176
+ return None
82
177
  except Exception as e:
83
178
  logger.error(
84
179
  "claude_oauth_credentials_load_error",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccproxy-api
3
- Version: 0.2.0a4
3
+ Version: 0.2.3
4
4
  Summary: API server that provides an Anthropic and OpenAI compatible interface over Claude Code, allowing to use your Claude OAuth account or over the API.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -20,6 +20,7 @@ Requires-Dist: typing-extensions>=4.0.0
20
20
  Requires-Dist: uvicorn>=0.34.0
21
21
  Provides-Extra: plugins-claude
22
22
  Requires-Dist: claude-agent-sdk>=0.1.0; extra == 'plugins-claude'
23
+ Requires-Dist: keyring>=25.0.0; extra == 'plugins-claude'
23
24
  Requires-Dist: qrcode>=8.2; extra == 'plugins-claude'
24
25
  Provides-Extra: plugins-codex
25
26
  Requires-Dist: pyjwt>=2.10.1; extra == 'plugins-codex'
@@ -196,14 +197,14 @@ To install the latest stable release without cloning the repository, use `uvx`
196
197
  to grab the published wheel and launch the CLI:
197
198
 
198
199
  ```bash
199
- uvx --with "ccproxy-api[all]==0.2.0" ccproxy serve --port 8000
200
+ uvx --with "ccproxy-api[all]" ccproxy serve --port 8000
200
201
  ```
201
202
 
202
203
  If you prefer `pipx`, install the package (optionally with extras) and use the
203
204
  local shim:
204
205
 
205
206
  ```bash
206
- pipx install "ccproxy-api[all]==0.2.0"
207
+ pipx install "ccproxy-api[all]"
207
208
  ccproxy serve # default on localhost:8000
208
209
  ```
209
210
 
@@ -67,7 +67,7 @@ ccproxy/config/settings.py,sha256=uva0RV4KfIvv7VApDr7w3oARft7OoBCWf7ZSM4oM2VM,19
67
67
  ccproxy/config/toml_generator.py,sha256=_txCYDHI8lXWl-mwOK8P1_TsX1TNiLgkG4iryxOruZc,10034
68
68
  ccproxy/config/utils.py,sha256=tuvOPUsMGgznz94MRwuSWw6sZi_AGkB_ri7VWKMVg8Y,11877
69
69
  ccproxy/core/__init__.py,sha256=hQgrBogZjdt8ZQlQyZtbL91I3gX9YUTWrenqTPRfwbM,236
70
- ccproxy/core/_version.py,sha256=fqfgUu99l0Y7LMwL27QHnVn-raAKZVZ_9iTiD4_KO5A,712
70
+ ccproxy/core/_version.py,sha256=kBRz0P2plw1eVdIpt70W6m1LMbEIhLY3RyOfVGdubaI,704
71
71
  ccproxy/core/async_task_manager.py,sha256=zf_mbbDwomh8q0E-oMNSPzFecHwLRi-ZPbhqsb6IPgM,16888
72
72
  ccproxy/core/async_utils.py,sha256=OFCJT8xbgZJO757iDPMAKY5c1Ildyk0PbwuktVF19UI,21676
73
73
  ccproxy/core/constants.py,sha256=FSLlbdNqCmZgZC4VAgvmovwXJh4C9WaUf_YBqDbYXXM,1837
@@ -121,10 +121,11 @@ ccproxy/llms/formatters/context.py,sha256=ULZl3sIcGbYKqzqo-W4HNpYoWD03vPVU-akahg
121
121
  ccproxy/llms/formatters/mapping.py,sha256=Tskj43bbDQAZWjqKEfnoivStwAycPXDGn1vXjuqTaic,1014
122
122
  ccproxy/llms/formatters/utils.py,sha256=7TKOraamcc_TXAxYkTv_HUFXr-fkHnqyBWb7WU9eZn4,10224
123
123
  ccproxy/llms/formatters/anthropic_to_openai/__init__.py,sha256=BR4ZAbaPRPd3q7Urb22ylyUgWaVi7UQ2zJzzGBam_Lw,1992
124
+ ccproxy/llms/formatters/anthropic_to_openai/_helpers.py,sha256=mX2HTsmcofkZ-ocBUUr62q0IxHpG2n5oS_cryAY6yvs,1166
124
125
  ccproxy/llms/formatters/anthropic_to_openai/errors.py,sha256=PCGB7PKr6x1jElIKxqDye0o1mI0_MUdTNMtk_djWbLA,2066
125
126
  ccproxy/llms/formatters/anthropic_to_openai/requests.py,sha256=e7BanFju5owBD64gtd835YIv1T18XYshp-DP-7I4y1o,14660
126
- ccproxy/llms/formatters/anthropic_to_openai/responses.py,sha256=yNjYdt2mor0t66kqjWyHu-LT9PYDmPgZqJalCgIUdAs,5199
127
- ccproxy/llms/formatters/anthropic_to_openai/streams.py,sha256=FWauNClQrF8E8EQLdPfkUi3gkGG3FDGbi_g7VVpnGxo,65989
127
+ ccproxy/llms/formatters/anthropic_to_openai/responses.py,sha256=uDq5NAvfinPDC75pY54j8BJzltb5r1kAeytToz0InFM,5834
128
+ ccproxy/llms/formatters/anthropic_to_openai/streams.py,sha256=CGW74eS-TZ9_0pM9ff2MfdI9yUhfmqXlwP7jS96obZQ,65722
128
129
  ccproxy/llms/formatters/common/__init__.py,sha256=Lnsz81M4P91Cex7t0_oM_hD1Tak7oHAWuykQkWg4b38,1524
129
130
  ccproxy/llms/formatters/common/identifiers.py,sha256=rzTynHqcvmPKhog64YAtxLDcQH27u38kZH8aCI3HIOA,1400
130
131
  ccproxy/llms/formatters/common/streams.py,sha256=kP_QQViwki1dH9IIMtigQLOtTY8wM2KZoQMFMl8vE_Y,8042
@@ -170,7 +171,7 @@ ccproxy/plugins/analytics/routes.py,sha256=RZLbaRkvo1vFvOasnzlqKyyTIm01dLYTg-T4a
170
171
  ccproxy/plugins/analytics/service.py,sha256=9aqS0sNZVsKsbrhYi62jdkkDgeoifcC3ARM3d4dweJ0,11699
171
172
  ccproxy/plugins/claude_api/README.md,sha256=kXpPt1NMbKdQiG0D4UmJKreLytWJKr0FwS9pSrxEgTE,1072
172
173
  ccproxy/plugins/claude_api/__init__.py,sha256=2n3Kw6EGmvEyoSgQzT2DRwLe5Zb1XET7kvvt0mwG3Mg,304
173
- ccproxy/plugins/claude_api/adapter.py,sha256=anX45r4gbeULAUaCzA7W5HlLmcfSbGrWuawCFNOdEu0,30857
174
+ ccproxy/plugins/claude_api/adapter.py,sha256=T1y9lAxJEj5yqiyo210IlZeRfSmuqyi4Gt1Jx6q5Bew,31121
174
175
  ccproxy/plugins/claude_api/config.py,sha256=R-8w5yOYcN42tx4cR3eyzyz99Ldt4B1aTDj_vEy9NdQ,1681
175
176
  ccproxy/plugins/claude_api/detection_service.py,sha256=mJeYvSskPS8mN-RTvwEVkPqN-91LZVfD7BsfYw7mDR8,15975
176
177
  ccproxy/plugins/claude_api/health.py,sha256=s1Vb3JuHWaWslLKOyFJXq6PrDAkzjEhhHXAR2cVXgQ8,5990
@@ -300,7 +301,7 @@ ccproxy/plugins/oauth_claude/manager.py,sha256=0aOTVwPy3nDLhZsX5vcpWRG4kGqkdklOy
300
301
  ccproxy/plugins/oauth_claude/models.py,sha256=90BQzi0MVL9sK7NtTYZjwSWR5DGmEPsU2F8zF60Jbgw,9227
301
302
  ccproxy/plugins/oauth_claude/plugin.py,sha256=_EybVmwvigoJfuzAMETkknO3kQ-fp59earI7i1yWA-s,4918
302
303
  ccproxy/plugins/oauth_claude/provider.py,sha256=lV0RUmBthE8rmnPUNqdtcYuZL_RED95bURBhOOcyDqQ,19500
303
- ccproxy/plugins/oauth_claude/storage.py,sha256=rzMPn3uyHHe7nxA-EuQAFi-uGWS_pN60z7AMwgrYwPY,6547
304
+ ccproxy/plugins/oauth_claude/storage.py,sha256=JsSSmeGkaQHhtdczhQdQSuTEGv4N6gmXX2YIJVzO68A,9844
304
305
  ccproxy/plugins/oauth_codex/README.md,sha256=XgbCxp_JHadpYcq6JW-hC8YvBo9310CLqVbKdprY5Vc,1451
305
306
  ccproxy/plugins/oauth_codex/__init__.py,sha256=pA4sU7Ngj0xY2Ppr_G6K_H4boihe-TL7ppvvusoBKEg,345
306
307
  ccproxy/plugins/oauth_codex/client.py,sha256=Ry0uFDsdXXFUGdI5ol5i-zDgjEZtaToKdaUAlhmgyu4,7357
@@ -410,8 +411,8 @@ ccproxy/utils/id_generator.py,sha256=k6R_W40lJSPi_it4M99EVg9eRD138oC4bv_8Ua3X8ms
410
411
  ccproxy/utils/model_mapper.py,sha256=hnIWc528x8oBuk1y1HuyGHbnwe6dsSxZ2UgA1OYrcJs,3731
411
412
  ccproxy/utils/startup_helpers.py,sha256=u1okOVbm2OeSqrNrbhWco_sXBR0usNo7Wv8zvhBLPhc,7492
412
413
  ccproxy/utils/version_checker.py,sha256=cGRgjD0PUB3MDZDSAdKPQwYIyqnlzFWue0ROyfGngNE,13452
413
- ccproxy_api-0.2.0a4.dist-info/METADATA,sha256=TiTWxEXyms9TVVIWQo7ANRPR41vCIH9yHdAEAa7LfsA,8296
414
- ccproxy_api-0.2.0a4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
415
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt,sha256=bibqQtPpKZJhOY_j5TFvcYzHuR-w7tNovV2i7UcPlU4,1147
416
- ccproxy_api-0.2.0a4.dist-info/licenses/LICENSE,sha256=httxSCpTrEOkipisMeGXSrZhTB-4MRIorQU0hS1B6eQ,1066
417
- ccproxy_api-0.2.0a4.dist-info/RECORD,,
414
+ ccproxy_api-0.2.3.dist-info/METADATA,sha256=X9RSa7zriZwyjtyM4Qm-Br6D62c0u1aOeW7DfDfJl-Q,8338
415
+ ccproxy_api-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
416
+ ccproxy_api-0.2.3.dist-info/entry_points.txt,sha256=bibqQtPpKZJhOY_j5TFvcYzHuR-w7tNovV2i7UcPlU4,1147
417
+ ccproxy_api-0.2.3.dist-info/licenses/LICENSE,sha256=httxSCpTrEOkipisMeGXSrZhTB-4MRIorQU0hS1B6eQ,1066
418
+ ccproxy_api-0.2.3.dist-info/RECORD,,