local-openai2anthropic 0.3.5__py3-none-any.whl → 0.3.7__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.3.5"
6
+ __version__ = "0.3.7"
7
7
 
8
8
  from local_openai2anthropic.protocol import (
9
9
  AnthropicError,
@@ -3,22 +3,242 @@
3
3
  Configuration settings for the proxy server.
4
4
  """
5
5
 
6
+ import sys
6
7
  from functools import lru_cache
8
+ from pathlib import Path
7
9
  from typing import Optional
8
10
 
9
- from pydantic_settings import BaseSettings, SettingsConfigDict
11
+ from pydantic import BaseModel, ConfigDict
10
12
 
11
13
 
12
- class Settings(BaseSettings):
13
- """Application settings loaded from environment variables."""
14
+ def get_config_dir() -> Path:
15
+ """Get platform-specific config directory.
14
16
 
15
- model_config = SettingsConfigDict(
16
- env_prefix="OA2A_", # OpenAI-to-Anthropic prefix
17
- env_file=".env",
18
- env_file_encoding="utf-8",
19
- case_sensitive=False,
20
- extra="ignore",
17
+ Returns:
18
+ Path to the config directory (~/.oa2a)
19
+ """
20
+ return Path.home() / ".oa2a"
21
+
22
+
23
+ def get_config_file() -> Path:
24
+ """Get config file path.
25
+
26
+ Returns:
27
+ Path to the config file (~/.oa2a/config.toml)
28
+ """
29
+ return get_config_dir() / "config.toml"
30
+
31
+
32
+ def create_default_config() -> bool:
33
+ """Create default config file if not exists.
34
+
35
+ Returns:
36
+ True if a new config file was created, False if it already exists
37
+ """
38
+ config_file = get_config_file()
39
+ if config_file.exists():
40
+ return False
41
+
42
+ config_dir = get_config_dir()
43
+ config_dir.mkdir(parents=True, exist_ok=True)
44
+
45
+ # Set restrictive permissions (0o600) for the config directory on Unix-like systems
46
+ if sys.platform != "win32":
47
+ config_dir.chmod(0o700)
48
+
49
+ default_config = """# OA2A Configuration File
50
+ # Place this file at ~/.oa2a/config.toml
51
+
52
+ # OpenAI API Configuration
53
+ openai_api_key = ""
54
+ openai_base_url = "https://api.openai.com/v1"
55
+ openai_org_id = ""
56
+ openai_project_id = ""
57
+
58
+ # Server Configuration
59
+ host = "0.0.0.0"
60
+ port = 8080
61
+ request_timeout = 300.0
62
+
63
+ # API Key for authenticating requests to this server (optional)
64
+ api_key = ""
65
+
66
+ # CORS settings
67
+ cors_origins = ["*"]
68
+ cors_credentials = true
69
+ cors_methods = ["*"]
70
+ cors_headers = ["*"]
71
+
72
+ # Logging
73
+ log_level = "INFO"
74
+ log_dir = "" # Empty uses platform-specific default
75
+
76
+ # Tavily Web Search Configuration
77
+ tavily_api_key = ""
78
+ tavily_timeout = 30.0
79
+ tavily_max_results = 5
80
+ websearch_max_uses = 5
81
+ """
82
+ config_file.write_text(default_config, encoding="utf-8")
83
+
84
+ # Set restrictive permissions (0o600) for the config file on Unix-like systems
85
+ if sys.platform != "win32":
86
+ config_file.chmod(0o600)
87
+
88
+ return True
89
+
90
+
91
+ def interactive_setup() -> dict:
92
+ """Interactive configuration setup wizard.
93
+
94
+ Guides user through setting up essential configuration values.
95
+
96
+ Returns:
97
+ Dictionary containing user-provided configuration
98
+ """
99
+ print("=" * 60)
100
+ print(" Welcome to local-openai2anthropic Setup Wizard")
101
+ print("=" * 60)
102
+ print()
103
+ print("This wizard will help you create the initial configuration.")
104
+ print(f"Config file will be saved to: {get_config_file()}")
105
+ print()
106
+
107
+ config = {}
108
+
109
+ # OpenAI API Key (required)
110
+ print("[1/3] OpenAI API Configuration")
111
+ print("-" * 40)
112
+ while True:
113
+ api_key = input("Enter your OpenAI API Key (required): ").strip()
114
+ if api_key:
115
+ config["openai_api_key"] = api_key
116
+ break
117
+ print("API Key is required. Please enter a valid key.")
118
+
119
+ # Base URL (optional, with default)
120
+ default_url = "https://api.openai.com/v1"
121
+ base_url = input(f"Enter OpenAI Base URL [{default_url}]: ").strip()
122
+ config["openai_base_url"] = base_url if base_url else default_url
123
+
124
+ print()
125
+ print("[2/3] Server Configuration")
126
+ print("-" * 40)
127
+
128
+ # Host (with default)
129
+ default_host = "0.0.0.0"
130
+ host = input(f"Enter server host [{default_host}]: ").strip()
131
+ config["host"] = host if host else default_host
132
+
133
+ # Port (with default)
134
+ default_port = "8080"
135
+ port_input = input(f"Enter server port [{default_port}]: ").strip()
136
+ try:
137
+ config["port"] = int(port_input) if port_input else int(default_port)
138
+ except ValueError:
139
+ print(f"Invalid port number, using default: {default_port}")
140
+ config["port"] = int(default_port)
141
+
142
+ # API Key for server authentication (optional)
143
+ print()
144
+ print("[3/3] Server API Authentication (Optional)")
145
+ print("-" * 40)
146
+ print("Set an API key to authenticate requests to this server.")
147
+ print(
148
+ "Leave empty to allow unauthenticated access (not recommended for production)."
149
+ )
150
+ server_api_key = input("Enter server API key (optional): ").strip()
151
+ if server_api_key:
152
+ config["api_key"] = server_api_key
153
+
154
+ print()
155
+ print("=" * 60)
156
+ print(" Configuration Summary")
157
+ print("=" * 60)
158
+ print(f"OpenAI Base URL: {config.get('openai_base_url', default_url)}")
159
+ print(
160
+ f"Server: {config.get('host', default_host)}:{config.get('port', default_port)}"
21
161
  )
162
+ print(f"OpenAI API Key: {config.get('openai_api_key', '')[:8]}... (configured)")
163
+ if config.get("api_key"):
164
+ print(f"Server Auth: {config['api_key'][:8]}... (configured)")
165
+ print()
166
+
167
+ return config
168
+
169
+
170
+ def create_config_from_dict(config: dict) -> None:
171
+ """Create config file from dictionary.
172
+
173
+ Args:
174
+ config: Dictionary containing configuration values
175
+ """
176
+ import tomli_w
177
+
178
+ config_file = get_config_file()
179
+ config_dir = get_config_dir()
180
+ config_dir.mkdir(parents=True, exist_ok=True)
181
+
182
+ # Set restrictive permissions for the config directory on Unix-like systems
183
+ if sys.platform != "win32":
184
+ config_dir.chmod(0o700)
185
+
186
+ # Build config dict with proper structure
187
+ toml_config: dict = {
188
+ "openai_api_key": config.get("openai_api_key", ""),
189
+ "openai_base_url": config.get("openai_base_url", "https://api.openai.com/v1"),
190
+ "host": config.get("host", "0.0.0.0"),
191
+ "port": config.get("port", 8080),
192
+ "request_timeout": config.get("request_timeout", 300.0),
193
+ "cors_origins": ["*"],
194
+ "cors_credentials": True,
195
+ "cors_methods": ["*"],
196
+ "cors_headers": ["*"],
197
+ "log_level": "INFO",
198
+ "log_dir": "",
199
+ "tavily_timeout": 30.0,
200
+ "tavily_max_results": 5,
201
+ "websearch_max_uses": 5,
202
+ }
203
+
204
+ # Add optional values only if present
205
+ if config.get("api_key"):
206
+ toml_config["api_key"] = config["api_key"]
207
+
208
+ if config.get("tavily_api_key"):
209
+ toml_config["tavily_api_key"] = config["tavily_api_key"]
210
+
211
+ # Write using proper TOML serialization (prevents injection attacks)
212
+ with open(config_file, "wb") as f:
213
+ tomli_w.dump(toml_config, f)
214
+
215
+ # Set restrictive permissions for the config file on Unix-like systems
216
+ if sys.platform != "win32":
217
+ config_file.chmod(0o600)
218
+
219
+
220
+ def load_config_from_file() -> dict:
221
+ """Load configuration from TOML file.
222
+
223
+ Returns:
224
+ Dictionary containing configuration values, empty dict if file doesn't exist
225
+ """
226
+ if sys.version_info >= (3, 11):
227
+ import tomllib
228
+ else:
229
+ import tomli as tomllib
230
+
231
+ config_file = get_config_file()
232
+ if not config_file.exists():
233
+ return {}
234
+ with open(config_file, "rb") as f:
235
+ return tomllib.load(f)
236
+
237
+
238
+ class Settings(BaseModel):
239
+ """Application settings loaded from config file."""
240
+
241
+ model_config = ConfigDict(extra="ignore")
22
242
 
23
243
  # OpenAI API Configuration
24
244
  openai_api_key: Optional[str] = None
@@ -41,7 +261,7 @@ class Settings(BaseSettings):
41
261
  cors_headers: list[str] = ["*"]
42
262
 
43
263
  # Logging
44
- log_level: str = "DEBUG"
264
+ log_level: str = "INFO"
45
265
  log_dir: str = "" # Empty means use platform-specific default
46
266
 
47
267
  # Tavily Web Search Configuration
@@ -62,8 +282,47 @@ class Settings(BaseSettings):
62
282
  headers["OpenAI-Project"] = self.openai_project_id
63
283
  return headers
64
284
 
285
+ @classmethod
286
+ def from_toml(cls) -> "Settings":
287
+ """Load settings from TOML config file.
288
+
289
+ Returns:
290
+ Settings instance populated from config file
291
+ """
292
+ config_data = load_config_from_file()
293
+ return cls(**config_data)
294
+
295
+
296
+ def is_interactive() -> bool:
297
+ """Check if running in an interactive terminal.
298
+
299
+ Returns:
300
+ True if stdin is a TTY (interactive), False otherwise
301
+ """
302
+ return sys.stdin.isatty()
303
+
65
304
 
66
305
  @lru_cache
67
306
  def get_settings() -> Settings:
68
- """Get cached settings instance."""
69
- return Settings()
307
+ """Get cached settings instance.
308
+
309
+ Creates config file interactively if it doesn't exist and running in a TTY.
310
+ Falls back to creating a default config file in non-interactive environments.
311
+
312
+ Returns:
313
+ Settings instance loaded from config file
314
+ """
315
+ config_file = get_config_file()
316
+ if not config_file.exists():
317
+ if is_interactive():
318
+ # Interactive setup wizard
319
+ config = interactive_setup()
320
+ create_config_from_dict(config)
321
+ print(f"\nConfiguration saved to: {config_file}")
322
+ print("You can edit this file later to change settings.\n")
323
+ else:
324
+ # Non-interactive environment: create default config
325
+ create_default_config()
326
+ print(f"Created default config file: {config_file}")
327
+ print("Please edit it to add your API keys and settings.")
328
+ return Settings.from_toml()
@@ -15,7 +15,7 @@ from fastapi import FastAPI, HTTPException, Request
15
15
  from fastapi.middleware.cors import CORSMiddleware
16
16
  from fastapi.responses import JSONResponse
17
17
 
18
- from local_openai2anthropic.config import Settings, get_settings
18
+ from local_openai2anthropic.config import Settings, get_config_file, get_settings
19
19
  from local_openai2anthropic.protocol import AnthropicError, AnthropicErrorResponse
20
20
  from local_openai2anthropic.router import router
21
21
 
@@ -101,7 +101,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
101
101
  app = FastAPI(
102
102
  title="local-openai2anthropic",
103
103
  description="A proxy server that converts Anthropic Messages API to OpenAI API",
104
- version="0.3.5",
104
+ version="0.3.7",
105
105
  docs_url="/docs",
106
106
  redoc_url="/redoc",
107
107
  )
@@ -203,11 +203,13 @@ def run_foreground(settings: Settings) -> None:
203
203
  """Run server in foreground mode (blocking)."""
204
204
  # Validate required settings
205
205
  if not settings.openai_api_key:
206
+ config_file = get_config_file()
206
207
  print(
207
- "Error: OA2A_OPENAI_API_KEY environment variable is required.\n"
208
- "Set it via:\n"
209
- " - Environment variable: export OA2A_OPENAI_API_KEY='your-key'\n"
210
- " - Or create a .env file with OA2A_OPENAI_API_KEY=your-key",
208
+ f"Error: openai_api_key is required.\n"
209
+ f"Please edit the configuration file:\n"
210
+ f" {config_file}\n"
211
+ f"\nSet your OpenAI API key:\n"
212
+ f' openai_api_key = "your-api-key"',
211
213
  file=sys.stderr,
212
214
  )
213
215
  sys.exit(1)
@@ -251,7 +253,7 @@ Examples:
251
253
  parser.add_argument(
252
254
  "--version",
253
255
  action="version",
254
- version="%(prog)s 0.2.5",
256
+ version="%(prog)s 0.3.7",
255
257
  )
256
258
 
257
259
  # Create subparsers for commands
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: local-openai2anthropic
3
- Version: 0.3.5
3
+ Version: 0.3.7
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
@@ -24,6 +24,7 @@ Requires-Dist: httpx>=0.25.0
24
24
  Requires-Dist: openai>=1.30.0
25
25
  Requires-Dist: pydantic-settings>=2.0.0
26
26
  Requires-Dist: pydantic>=2.0.0
27
+ Requires-Dist: tomli-w>=1.0.0
27
28
  Requires-Dist: uvicorn[standard]>=0.23.0
28
29
  Provides-Extra: dev
29
30
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -1,10 +1,10 @@
1
- local_openai2anthropic/__init__.py,sha256=YHTno5vTDXG-rjXCkH_JxAJZJNIPMDuAcphmJJV_pQA,1059
1
+ local_openai2anthropic/__init__.py,sha256=ykkqdWKgqvSffE2-GmDcZQFHJRAe0y4d4aeD3fUq_Os,1059
2
2
  local_openai2anthropic/__main__.py,sha256=K21u5u7FN8-DbO67TT_XDF0neGqJeFrVNkteRauCRQk,179
3
- local_openai2anthropic/config.py,sha256=ZOZKbyWL4oCYHkEcfl-HgTmDbgJoqHRahdZQbTXqkVg,1985
3
+ local_openai2anthropic/config.py,sha256=Cjg6J7H7ydKtVSd5m0RlTj-YF6yht3TpF4LcyodqQP4,9621
4
4
  local_openai2anthropic/converter.py,sha256=og94I514M9km_Wbk9c1ddU6fyaQNEbpd2zfpfnBQaTQ,16029
5
5
  local_openai2anthropic/daemon.py,sha256=pZnRojGFcuIpR8yLDNjV-b0LJRBVhgRAa-dKeRRse44,10017
6
6
  local_openai2anthropic/daemon_runner.py,sha256=rguOH0PgpbjqNsKYei0uCQX8JQOQ1wmtQH1CtW95Dbw,3274
7
- local_openai2anthropic/main.py,sha256=oImU_AhD6mNGf7qUiA7u8R5QddlZDBUpDHADa0EIvpA,12145
7
+ local_openai2anthropic/main.py,sha256=FPCEATNPXvGpkszftdXJh0o0F5sUAOXo2zDagmsWGKI,12174
8
8
  local_openai2anthropic/openai_types.py,sha256=jFdCvLwtXYoo5gGRqOhbHQcVaxcsxNnCP_yFPIv7rG4,3823
9
9
  local_openai2anthropic/protocol.py,sha256=VW3B1YrbYg5UAo7PveQv0Ny5vfuNa6yG6IlHtkuyXiI,5178
10
10
  local_openai2anthropic/router.py,sha256=gwSGCYQGd0tAj4B4cl30UDkIJDIfBP4D8T9KEMKnxyk,16196
@@ -18,8 +18,8 @@ local_openai2anthropic/tools/__init__.py,sha256=OM_6YAwy3G1kbrF7n5NvmBwWPGO0hwq4
18
18
  local_openai2anthropic/tools/handler.py,sha256=SO8AmEUfNIg16s6jOKBaYdajYc0fiI8ciOoiKXIJe_c,14106
19
19
  local_openai2anthropic/utils/__init__.py,sha256=0Apd3lQCmWpQHol4AfjtQe6A3Cpex9Zn-8dyK_FU8Z0,372
20
20
  local_openai2anthropic/utils/tokens.py,sha256=TV3vGAjoGZeyo1xPvwb5jto43p1U1f4HteCApB86X0g,3187
21
- local_openai2anthropic-0.3.5.dist-info/METADATA,sha256=Z26zrkTqEmfovoEt091Bj702JRNlY-JT3poLb--AEKs,11240
22
- local_openai2anthropic-0.3.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
- local_openai2anthropic-0.3.5.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
24
- local_openai2anthropic-0.3.5.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
25
- local_openai2anthropic-0.3.5.dist-info/RECORD,,
21
+ local_openai2anthropic-0.3.7.dist-info/METADATA,sha256=ZqgaeWvxJAKD1fDKC0XWiSwf3BI1DbH5UQpjvOzFewo,11270
22
+ local_openai2anthropic-0.3.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
+ local_openai2anthropic-0.3.7.dist-info/entry_points.txt,sha256=hdc9tSJUNxyNLXcTYye5SuD2K0bEQhxBhGnWTFup6ZM,116
24
+ local_openai2anthropic-0.3.7.dist-info/licenses/LICENSE,sha256=X3_kZy3lJvd_xp8IeyUcIAO2Y367MXZc6aaRx8BYR_s,11369
25
+ local_openai2anthropic-0.3.7.dist-info/RECORD,,