ai-code-switcher 0.1.5__tar.gz → 0.1.7__tar.gz
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.
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/PKG-INFO +1 -1
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/pyproject.toml +1 -1
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/PKG-INFO +1 -1
- ai_code_switcher-0.1.7/src/code_ai/__init__.py +1 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/launcher.py +15 -2
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/models.py +2 -2
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/profiles.py +23 -13
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_integration.py +91 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_launcher.py +33 -0
- ai_code_switcher-0.1.5/src/code_ai/__init__.py +0 -1
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/README.md +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/setup.cfg +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/SOURCES.txt +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/dependency_links.txt +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/entry_points.txt +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/requires.txt +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/top_level.txt +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/cli.py +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/code_ai/config.py +0 -0
- {ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/tests/test_models.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.6"
|
|
@@ -32,12 +32,25 @@ def prepare_environment(profile):
|
|
|
32
32
|
|
|
33
33
|
# Handle authentication based on profile type
|
|
34
34
|
if isinstance(profile, LoginProfile):
|
|
35
|
-
# Login mode: clear API environment variables and set
|
|
35
|
+
# Login mode: clear API environment variables and set CONFIG_DIR
|
|
36
36
|
for env_var in spec["env"]:
|
|
37
37
|
env.pop(env_var, None)
|
|
38
38
|
# Expand ~ to home directory
|
|
39
39
|
credentials_path = os.path.expanduser(profile.credentials_path)
|
|
40
|
-
env
|
|
40
|
+
# Set the appropriate config dir env var based on profile type
|
|
41
|
+
# Claude uses CLAUDE_CONFIG_DIR, Codex uses CODEX_HOME
|
|
42
|
+
config_dir_vars = {"claude": "CLAUDE_CONFIG_DIR", "codex": "CODEX_HOME"}
|
|
43
|
+
config_dir_var = config_dir_vars.get(ptype)
|
|
44
|
+
if config_dir_var:
|
|
45
|
+
os.makedirs(credentials_path, exist_ok=True)
|
|
46
|
+
# For codex: ensure config.toml exists with default openai provider
|
|
47
|
+
# to prevent inheriting custom providers from ~/.codex/config.toml
|
|
48
|
+
if ptype == "codex":
|
|
49
|
+
config_toml = os.path.join(credentials_path, "config.toml")
|
|
50
|
+
if not os.path.exists(config_toml):
|
|
51
|
+
with open(config_toml, "w") as f:
|
|
52
|
+
f.write('model_provider = "openai"\n')
|
|
53
|
+
env[config_dir_var] = credentials_path
|
|
41
54
|
elif isinstance(profile, ApiProfile):
|
|
42
55
|
# API mode: set API environment variables
|
|
43
56
|
for env_var, config_key in spec["env"].items():
|
|
@@ -22,7 +22,7 @@ class ApiProfile(BaseProfile):
|
|
|
22
22
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class LoginProfile(BaseProfile):
|
|
25
|
-
"""Login mode: authenticate via OAuth credentials directory (Claude
|
|
25
|
+
"""Login mode: authenticate via OAuth credentials directory (Claude/Codex)"""
|
|
26
26
|
credentials_path: str = "" # Path to existing OAuth credentials
|
|
27
27
|
|
|
28
28
|
|
|
@@ -33,7 +33,7 @@ def profile_from_dict(data: dict) -> BaseProfile:
|
|
|
33
33
|
mode = data.get("mode", "api") # Default to api for backward compatibility
|
|
34
34
|
proxy = data.get("proxy")
|
|
35
35
|
|
|
36
|
-
if ptype == "claude" and mode == "login":
|
|
36
|
+
if (ptype == "claude" or ptype == "codex") and mode == "login":
|
|
37
37
|
return LoginProfile(
|
|
38
38
|
name=name,
|
|
39
39
|
type=ptype,
|
|
@@ -66,8 +66,8 @@ def add_profile(config):
|
|
|
66
66
|
"type": ptype,
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
# Handle mode for Claude
|
|
70
|
-
if ptype
|
|
69
|
+
# Handle mode for Claude and Codex
|
|
70
|
+
if ptype in ("claude", "codex"):
|
|
71
71
|
mode = input("Mode (api/login) [api]: ").strip().lower() or "api"
|
|
72
72
|
if mode not in ("api", "login"):
|
|
73
73
|
print("Error: mode must be 'api' or 'login'.")
|
|
@@ -77,17 +77,22 @@ def add_profile(config):
|
|
|
77
77
|
if mode == "login":
|
|
78
78
|
credentials_path = input("Credentials path (optional, auto-generate if empty): ").strip()
|
|
79
79
|
if not credentials_path:
|
|
80
|
-
credentials_path = f"~/.
|
|
80
|
+
credentials_path = f"~/.{ptype}-profiles/{name}"
|
|
81
81
|
profile_data["credentials_path"] = credentials_path
|
|
82
82
|
else: # api mode
|
|
83
83
|
base_url = input("Base URL: ").strip()
|
|
84
84
|
if not base_url:
|
|
85
85
|
print("Error: base URL cannot be empty.")
|
|
86
86
|
sys.exit(1)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
if ptype == "claude":
|
|
88
|
+
token = input("Auth token: ").strip()
|
|
89
|
+
profile_data["base_url"] = base_url
|
|
90
|
+
profile_data["token"] = token
|
|
91
|
+
else: # codex
|
|
92
|
+
api_key = input("API key: ").strip()
|
|
93
|
+
profile_data["base_url"] = base_url
|
|
94
|
+
profile_data["api_key"] = api_key
|
|
95
|
+
else: # gemini - API mode only
|
|
91
96
|
base_url = input("Base URL: ").strip()
|
|
92
97
|
if not base_url:
|
|
93
98
|
print("Error: base URL cannot be empty.")
|
|
@@ -117,8 +122,8 @@ def show_profile(config, name):
|
|
|
117
122
|
print(f"Profile: {name}")
|
|
118
123
|
print(f"Type: {profile.type}")
|
|
119
124
|
|
|
120
|
-
# Display mode for Claude
|
|
121
|
-
if profile.type
|
|
125
|
+
# Display mode for Claude and Codex types
|
|
126
|
+
if profile.type in ("claude", "codex"):
|
|
122
127
|
mode = profile_dict.get("mode", "api")
|
|
123
128
|
print(f"Mode: {mode}")
|
|
124
129
|
|
|
@@ -127,10 +132,15 @@ def show_profile(config, name):
|
|
|
127
132
|
print(f"Credentials Path: {credentials_path}")
|
|
128
133
|
else: # api mode
|
|
129
134
|
base_url = profile_dict.get("base_url", "N/A")
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
if profile.type == "claude":
|
|
136
|
+
token = profile_dict.get("token", "N/A")
|
|
137
|
+
print(f"Base URL: {base_url}")
|
|
138
|
+
print(f"Token: {token}")
|
|
139
|
+
else: # codex
|
|
140
|
+
api_key = profile_dict.get("api_key", "N/A")
|
|
141
|
+
print(f"Base URL: {base_url}")
|
|
142
|
+
print(f"API Key: {api_key}")
|
|
143
|
+
else: # gemini
|
|
134
144
|
base_url = profile_dict.get("base_url", "N/A")
|
|
135
145
|
api_key = profile_dict.get("api_key", "N/A")
|
|
136
146
|
print(f"Base URL: {base_url}")
|
|
@@ -218,3 +218,94 @@ class TestBackwardCompatibility:
|
|
|
218
218
|
env = prepare_environment(profile)
|
|
219
219
|
assert env["GOOGLE_GEMINI_BASE_URL"] == "https://generativelanguage.googleapis.com"
|
|
220
220
|
assert env["GEMINI_API_KEY"] == "AIza-test-key"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestCodexProfiles:
|
|
224
|
+
"""Test codex profile workflows"""
|
|
225
|
+
|
|
226
|
+
def test_codex_api_profile(self, tmp_path):
|
|
227
|
+
"""Test codex API mode profile"""
|
|
228
|
+
config_file = tmp_path / "config.yaml"
|
|
229
|
+
|
|
230
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
231
|
+
# Initialize config
|
|
232
|
+
config = {"profiles": {}}
|
|
233
|
+
save_config(config)
|
|
234
|
+
|
|
235
|
+
# Add codex API profile
|
|
236
|
+
inputs = [
|
|
237
|
+
"my-codex-api", # Profile name
|
|
238
|
+
"codex", # Type
|
|
239
|
+
"api", # Mode
|
|
240
|
+
"https://api.openai.com/v1", # Base URL
|
|
241
|
+
"sk-test-key", # API key
|
|
242
|
+
"", # No proxy
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
with patch("builtins.input", side_effect=inputs):
|
|
246
|
+
config = load_config()
|
|
247
|
+
config = add_profile(config)
|
|
248
|
+
save_config(config)
|
|
249
|
+
|
|
250
|
+
# Verify profile was added
|
|
251
|
+
config = load_config()
|
|
252
|
+
assert "my-codex-api" in config["profiles"]
|
|
253
|
+
profile_dict = config["profiles"]["my-codex-api"]
|
|
254
|
+
assert profile_dict["type"] == "codex"
|
|
255
|
+
assert profile_dict["mode"] == "api"
|
|
256
|
+
|
|
257
|
+
# Convert to profile object
|
|
258
|
+
profile = profile_from_dict(profile_dict)
|
|
259
|
+
assert isinstance(profile, ApiProfile)
|
|
260
|
+
assert profile.type == "codex"
|
|
261
|
+
|
|
262
|
+
# Test environment preparation
|
|
263
|
+
env = prepare_environment(profile)
|
|
264
|
+
# Should have both OPENAI_API_KEY and OPENAI_BASE_URL
|
|
265
|
+
assert env["OPENAI_API_KEY"] == "sk-test-key"
|
|
266
|
+
assert env["OPENAI_BASE_URL"] == "https://api.openai.com/v1"
|
|
267
|
+
|
|
268
|
+
def test_codex_login_profile(self, tmp_path):
|
|
269
|
+
"""Test codex login mode profile"""
|
|
270
|
+
config_file = tmp_path / "config.yaml"
|
|
271
|
+
|
|
272
|
+
with patch("src.code_ai.config.CONFIG_FILE", config_file):
|
|
273
|
+
# Initialize config
|
|
274
|
+
config = {"profiles": {}}
|
|
275
|
+
save_config(config)
|
|
276
|
+
|
|
277
|
+
# Add codex login profile
|
|
278
|
+
inputs = [
|
|
279
|
+
"my-codex-login", # Profile name
|
|
280
|
+
"codex", # Type
|
|
281
|
+
"login", # Mode
|
|
282
|
+
"~/.codex-profiles/account-a", # Credentials path
|
|
283
|
+
"", # No proxy
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
with patch("builtins.input", side_effect=inputs):
|
|
287
|
+
config = load_config()
|
|
288
|
+
config = add_profile(config)
|
|
289
|
+
save_config(config)
|
|
290
|
+
|
|
291
|
+
# Verify profile was added
|
|
292
|
+
config = load_config()
|
|
293
|
+
assert "my-codex-login" in config["profiles"]
|
|
294
|
+
profile_dict = config["profiles"]["my-codex-login"]
|
|
295
|
+
assert profile_dict["type"] == "codex"
|
|
296
|
+
assert profile_dict["mode"] == "login"
|
|
297
|
+
assert profile_dict["credentials_path"] == "~/.codex-profiles/account-a"
|
|
298
|
+
|
|
299
|
+
# Convert to profile object
|
|
300
|
+
profile = profile_from_dict(profile_dict)
|
|
301
|
+
assert isinstance(profile, LoginProfile)
|
|
302
|
+
assert profile.type == "codex"
|
|
303
|
+
|
|
304
|
+
# Test environment preparation
|
|
305
|
+
env = prepare_environment(profile)
|
|
306
|
+
# Login mode should NOT have API environment variables
|
|
307
|
+
assert "OPENAI_API_KEY" not in env
|
|
308
|
+
# Should have CODEX_HOME with expanded path
|
|
309
|
+
import os
|
|
310
|
+
expected_path = os.path.expanduser("~/.codex-profiles/account-a")
|
|
311
|
+
assert env["CODEX_HOME"] == expected_path
|
|
@@ -72,3 +72,36 @@ def test_prepare_env_login_clears_api_vars():
|
|
|
72
72
|
# Should be cleared
|
|
73
73
|
assert "ANTHROPIC_BASE_URL" not in env
|
|
74
74
|
assert "ANTHROPIC_AUTH_TOKEN" not in env
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_prepare_env_codex_api_mode():
|
|
78
|
+
"""Test environment preparation for codex API mode"""
|
|
79
|
+
profile = ApiProfile(
|
|
80
|
+
name="test-codex-api",
|
|
81
|
+
type="codex",
|
|
82
|
+
base_url="https://api.openai.com/v1",
|
|
83
|
+
api_key="sk-test"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
env = prepare_environment(profile)
|
|
87
|
+
|
|
88
|
+
# Should have both OPENAI_API_KEY and OPENAI_BASE_URL
|
|
89
|
+
assert env["OPENAI_API_KEY"] == "sk-test"
|
|
90
|
+
assert env["OPENAI_BASE_URL"] == "https://api.openai.com/v1"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_prepare_env_codex_login_mode():
|
|
94
|
+
"""Test environment preparation for codex login mode"""
|
|
95
|
+
profile = LoginProfile(
|
|
96
|
+
name="test-codex-login",
|
|
97
|
+
type="codex",
|
|
98
|
+
credentials_path="~/.codex-profiles/account-a"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
env = prepare_environment(profile)
|
|
102
|
+
|
|
103
|
+
# Should NOT have API environment variables
|
|
104
|
+
assert "OPENAI_API_KEY" not in env
|
|
105
|
+
# Should have CODEX_HOME with expanded path
|
|
106
|
+
expected_path = os.path.expanduser("~/.codex-profiles/account-a")
|
|
107
|
+
assert env["CODEX_HOME"] == expected_path
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{ai_code_switcher-0.1.5 → ai_code_switcher-0.1.7}/src/ai_code_switcher.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|