studiorelay 0.3.0__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.
- kimibridge/__init__.py +1 -0
- kimibridge/cli.py +461 -0
- kimibridge/compatibility/__init__.py +25 -0
- kimibridge/compatibility/errors.py +62 -0
- kimibridge/compatibility/model_aliases.py +44 -0
- kimibridge/compatibility/models.py +69 -0
- kimibridge/compatibility/parameters.py +29 -0
- kimibridge/compatibility/pipeline.py +42 -0
- kimibridge/compatibility/profiles.py +138 -0
- kimibridge/compatibility/responses.py +766 -0
- kimibridge/compatibility/roles.py +33 -0
- kimibridge/config.py +195 -0
- kimibridge/health.py +51 -0
- kimibridge/ports.py +255 -0
- kimibridge/server.py +309 -0
- kimibridge/service.py +182 -0
- kimibridge/upstream.py +168 -0
- studiorelay/__init__.py +1 -0
- studiorelay/cli.py +514 -0
- studiorelay/compatibility/__init__.py +25 -0
- studiorelay/compatibility/errors.py +62 -0
- studiorelay/compatibility/model_aliases.py +44 -0
- studiorelay/compatibility/models.py +69 -0
- studiorelay/compatibility/parameters.py +29 -0
- studiorelay/compatibility/pipeline.py +42 -0
- studiorelay/compatibility/profiles.py +138 -0
- studiorelay/compatibility/responses.py +766 -0
- studiorelay/compatibility/roles.py +33 -0
- studiorelay/config.py +326 -0
- studiorelay/health.py +53 -0
- studiorelay/ports.py +277 -0
- studiorelay/server.py +321 -0
- studiorelay/service.py +237 -0
- studiorelay/upstream.py +171 -0
- studiorelay-0.3.0.dist-info/METADATA +395 -0
- studiorelay-0.3.0.dist-info/RECORD +39 -0
- studiorelay-0.3.0.dist-info/WHEEL +5 -0
- studiorelay-0.3.0.dist-info/entry_points.txt +4 -0
- studiorelay-0.3.0.dist-info/top_level.txt +2 -0
kimibridge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
kimibridge/cli.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from kimibridge import __version__
|
|
9
|
+
from kimibridge.compatibility import normalized_models
|
|
10
|
+
from kimibridge.compatibility.pipeline import VALID_MODES
|
|
11
|
+
from kimibridge.config import (
|
|
12
|
+
DEFAULT_BASE_URL,
|
|
13
|
+
CompatibilityConfig,
|
|
14
|
+
KimiConfig,
|
|
15
|
+
ServerConfig,
|
|
16
|
+
config_to_dict,
|
|
17
|
+
default_config_path,
|
|
18
|
+
list_configured_providers,
|
|
19
|
+
load_config,
|
|
20
|
+
normalize_provider_name,
|
|
21
|
+
save_config,
|
|
22
|
+
)
|
|
23
|
+
from kimibridge.health import check_health
|
|
24
|
+
from kimibridge.ports import resolve_port
|
|
25
|
+
from kimibridge.service import service_action, service_logs, service_name, service_uninstall
|
|
26
|
+
from kimibridge.server import run_server
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
31
|
+
parser = argparse.ArgumentParser(prog="kimibridge")
|
|
32
|
+
parser.add_argument("--version", action="store_true", help="Print version and exit.")
|
|
33
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
34
|
+
|
|
35
|
+
setup = subparsers.add_parser("setup", help="Quick setup for a target provider (deepseek, kimi, all, openai).")
|
|
36
|
+
setup.add_argument("provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"])
|
|
37
|
+
setup.add_argument("--port", type=int, help="Port to bind.")
|
|
38
|
+
setup.add_argument("--base-url", help="Custom upstream base URL. Defaults to provider standard.")
|
|
39
|
+
setup.add_argument("--no-restart", action="store_true", help="Do not restart background service after setup.")
|
|
40
|
+
|
|
41
|
+
use = subparsers.add_parser("use", help="Alias for setup: switch target provider.")
|
|
42
|
+
use.add_argument("provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"])
|
|
43
|
+
use.add_argument("--port", type=int, help="Port to bind.")
|
|
44
|
+
use.add_argument("--base-url", help="Custom upstream base URL. Defaults to provider standard.")
|
|
45
|
+
use.add_argument("--no-restart", action="store_true", help="Do not restart background service after setup.")
|
|
46
|
+
|
|
47
|
+
start = subparsers.add_parser("start", help="Start the local proxy.")
|
|
48
|
+
start.add_argument("--host", help="Host to bind. Defaults to config or 127.0.0.1.")
|
|
49
|
+
start.add_argument("--port", type=int, help="Port to bind. Defaults to config or 5001.")
|
|
50
|
+
start.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider profile.")
|
|
51
|
+
start.add_argument("--config", help="Custom configuration file path.")
|
|
52
|
+
start.add_argument(
|
|
53
|
+
"--auto-port",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Choose the next available port if the configured port is busy.",
|
|
56
|
+
)
|
|
57
|
+
start.add_argument(
|
|
58
|
+
"--save-port",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Persist the selected fallback port to the config file.",
|
|
61
|
+
)
|
|
62
|
+
start.add_argument(
|
|
63
|
+
"--dry-run",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Resolve configuration and print the endpoint without starting the server.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
doctor_parser = subparsers.add_parser("doctor", help="Check local proxy configuration.")
|
|
69
|
+
doctor_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Check specific provider.")
|
|
70
|
+
|
|
71
|
+
status_parser = subparsers.add_parser("status", help="Check whether the local proxy is running.")
|
|
72
|
+
status_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Check specific provider.")
|
|
73
|
+
|
|
74
|
+
stop_parser = subparsers.add_parser("stop", help="Stop the installed background service.")
|
|
75
|
+
stop_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider.")
|
|
76
|
+
|
|
77
|
+
restart_parser = subparsers.add_parser("restart", help="Restart the installed background service.")
|
|
78
|
+
restart_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider.")
|
|
79
|
+
|
|
80
|
+
logs_parser = subparsers.add_parser("logs", help="Show recent background service logs.")
|
|
81
|
+
logs_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider.")
|
|
82
|
+
|
|
83
|
+
uninstall_parser = subparsers.add_parser("uninstall", help="Remove installed service and app files.")
|
|
84
|
+
uninstall_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider to uninstall.")
|
|
85
|
+
uninstall_parser.add_argument("--all", action="store_true", help="Remove all instances.")
|
|
86
|
+
|
|
87
|
+
config = subparsers.add_parser("config", help="Show or update local config.")
|
|
88
|
+
config_subparsers = config.add_subparsers(dest="config_command")
|
|
89
|
+
show_parser = config_subparsers.add_parser("show", help="Print the active config.")
|
|
90
|
+
show_parser.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider.")
|
|
91
|
+
|
|
92
|
+
set_config = config_subparsers.add_parser("set", help="Set a config value.")
|
|
93
|
+
set_config.add_argument(
|
|
94
|
+
"key",
|
|
95
|
+
choices=["base-url", "compatibility-mode", "fallback-base-url", "host", "port", "provider"],
|
|
96
|
+
)
|
|
97
|
+
set_config.add_argument("value")
|
|
98
|
+
set_config.add_argument("--provider", choices=["deepseek", "kimi", "moonshot", "openai", "all"], help="Target provider.")
|
|
99
|
+
return parser
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> int:
|
|
103
|
+
parser = build_parser()
|
|
104
|
+
args = parser.parse_args(argv)
|
|
105
|
+
|
|
106
|
+
if args.version:
|
|
107
|
+
print(__version__)
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
if args.command in {"setup", "use"}:
|
|
111
|
+
return setup_provider(args)
|
|
112
|
+
|
|
113
|
+
if args.command == "start":
|
|
114
|
+
return start(args)
|
|
115
|
+
|
|
116
|
+
if args.command == "doctor":
|
|
117
|
+
return doctor(args)
|
|
118
|
+
|
|
119
|
+
if args.command == "status":
|
|
120
|
+
return status(args)
|
|
121
|
+
|
|
122
|
+
if args.command == "stop":
|
|
123
|
+
return run_service_action("stop", provider=getattr(args, "provider", None))
|
|
124
|
+
|
|
125
|
+
if args.command == "restart":
|
|
126
|
+
return run_service_action("restart", provider=getattr(args, "provider", None))
|
|
127
|
+
|
|
128
|
+
if args.command == "logs":
|
|
129
|
+
return logs(args)
|
|
130
|
+
|
|
131
|
+
if args.command == "uninstall":
|
|
132
|
+
return uninstall(args)
|
|
133
|
+
|
|
134
|
+
if args.command == "config":
|
|
135
|
+
return config_command(args)
|
|
136
|
+
|
|
137
|
+
parser.print_help()
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def setup_provider(args: argparse.Namespace) -> int:
|
|
142
|
+
provider = normalize_provider_name(args.provider)
|
|
143
|
+
config = load_config(provider=provider)
|
|
144
|
+
|
|
145
|
+
if getattr(args, "base_url", None):
|
|
146
|
+
base_url = args.base_url.strip()
|
|
147
|
+
elif provider == "deepseek":
|
|
148
|
+
base_url = "https://api.deepseek.com"
|
|
149
|
+
elif provider == "kimi":
|
|
150
|
+
base_url = "https://api.moonshot.ai"
|
|
151
|
+
elif provider == "openai":
|
|
152
|
+
base_url = "https://api.openai.com"
|
|
153
|
+
else: # all
|
|
154
|
+
base_url = config.kimi.base_url
|
|
155
|
+
|
|
156
|
+
target_port = getattr(args, "port", None) or config.server.port
|
|
157
|
+
resolution = resolve_port(
|
|
158
|
+
config.server.host,
|
|
159
|
+
target_port,
|
|
160
|
+
auto_port=True,
|
|
161
|
+
target_provider=provider,
|
|
162
|
+
)
|
|
163
|
+
port = resolution.selected_port
|
|
164
|
+
|
|
165
|
+
new_config = replace(
|
|
166
|
+
config,
|
|
167
|
+
server=ServerConfig(host=config.server.host, port=port),
|
|
168
|
+
provider=provider,
|
|
169
|
+
kimi=replace(config.kimi, base_url=base_url),
|
|
170
|
+
)
|
|
171
|
+
config_path = save_config(new_config, provider=provider)
|
|
172
|
+
|
|
173
|
+
endpoint = f"http://{new_config.server.host}:{new_config.server.port}/v1"
|
|
174
|
+
print(f"KimiBridge configured for provider: {provider.upper()}")
|
|
175
|
+
print(f" Target Provider: {provider}")
|
|
176
|
+
print(f" Upstream Base URL: {base_url}")
|
|
177
|
+
print(f" Proxy Endpoint: {endpoint}")
|
|
178
|
+
print(f" Saved to: {config_path}")
|
|
179
|
+
|
|
180
|
+
models_resp = normalized_models(provider, base_url=base_url)
|
|
181
|
+
models_list = [m["id"] for m in models_resp.get("data", [])]
|
|
182
|
+
if models_list:
|
|
183
|
+
print(f" Available Models: {', '.join(models_list[:6])}" + ("..." if len(models_list) > 6 else ""))
|
|
184
|
+
|
|
185
|
+
if not getattr(args, "no_restart", False):
|
|
186
|
+
health = check_health(new_config.server.host, new_config.server.port)
|
|
187
|
+
if health.ok:
|
|
188
|
+
result = service_action("restart", provider=provider)
|
|
189
|
+
if result.ok:
|
|
190
|
+
print(f" Background Service ({service_name(provider)}): Successfully restarted")
|
|
191
|
+
else:
|
|
192
|
+
print(f" Background Service: {result.message}")
|
|
193
|
+
else:
|
|
194
|
+
print(f" Background Service: Not currently running (run 'kimibridge start --provider {provider} --auto-port' to start)")
|
|
195
|
+
|
|
196
|
+
print()
|
|
197
|
+
print("In Android Studio / Cursor / Aider:")
|
|
198
|
+
print(f" 1. Set Base URL: {endpoint}")
|
|
199
|
+
print(f" 2. Enter your {provider.capitalize()} API Key")
|
|
200
|
+
print(" 3. Click Refresh: Models will load into the dropdown!")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def start(args: argparse.Namespace) -> int:
|
|
205
|
+
provider = getattr(args, "provider", None)
|
|
206
|
+
config_path = Path(args.config) if getattr(args, "config", None) else default_config_path(provider)
|
|
207
|
+
config = load_config(path=config_path, provider=provider)
|
|
208
|
+
target_provider = provider or config.provider
|
|
209
|
+
host = args.host or config.server.host
|
|
210
|
+
requested_port = args.port or config.server.port
|
|
211
|
+
resolution = resolve_port(host, requested_port, auto_port=args.auto_port, target_provider=target_provider)
|
|
212
|
+
|
|
213
|
+
print(resolution.message)
|
|
214
|
+
|
|
215
|
+
if resolution.status == "already_running" and not args.save_port:
|
|
216
|
+
print(f"Endpoint: http://{host}:{resolution.selected_port}/v1")
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
if resolution.status in {"conflict", "no_available_port", "unknown"}:
|
|
220
|
+
return 2
|
|
221
|
+
|
|
222
|
+
selected_config = replace(
|
|
223
|
+
config,
|
|
224
|
+
server=ServerConfig(host=host, port=resolution.selected_port),
|
|
225
|
+
provider=target_provider,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if (args.save_port and resolution.changed) or (args.save_port and provider):
|
|
229
|
+
saved_path = save_config(selected_config, path=config_path, provider=target_provider)
|
|
230
|
+
print(f"Saved selected port to {saved_path}")
|
|
231
|
+
|
|
232
|
+
print(f"Endpoint: http://{host}:{resolution.selected_port}/v1")
|
|
233
|
+
if args.dry_run:
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
run_server(selected_config, config_path=config_path)
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def doctor(args: argparse.Namespace | None = None) -> int:
|
|
241
|
+
provider = getattr(args, "provider", None) if args else None
|
|
242
|
+
if provider:
|
|
243
|
+
return _doctor_single(provider=provider)
|
|
244
|
+
|
|
245
|
+
configured = list_configured_providers()
|
|
246
|
+
if len(configured) > 1:
|
|
247
|
+
print("KimiBridge Doctor (Multi-Provider Setup)")
|
|
248
|
+
print(f"Version: {__version__}")
|
|
249
|
+
exit_code = 0
|
|
250
|
+
for p, path, cfg in configured:
|
|
251
|
+
print("\n----------------------------------------")
|
|
252
|
+
code = _doctor_single(provider=p, path=path, config=cfg)
|
|
253
|
+
if code != 0:
|
|
254
|
+
exit_code = code
|
|
255
|
+
return exit_code
|
|
256
|
+
|
|
257
|
+
return _doctor_single(provider=provider)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _doctor_single(
|
|
261
|
+
provider: str | None = None,
|
|
262
|
+
path: Path | None = None,
|
|
263
|
+
config: AppConfig | None = None,
|
|
264
|
+
) -> int:
|
|
265
|
+
cfg = config or load_config(path=path, provider=provider)
|
|
266
|
+
cfg_path = path or default_config_path(provider or cfg.provider)
|
|
267
|
+
resolution = resolve_port(cfg.server.host, cfg.server.port, target_provider=cfg.provider)
|
|
268
|
+
endpoint = f"http://{cfg.server.host}:{cfg.server.port}/v1"
|
|
269
|
+
|
|
270
|
+
print(f"KimiBridge Doctor ({cfg.provider.upper()})")
|
|
271
|
+
print(f"Version: {__version__}")
|
|
272
|
+
print(f"Config path: {cfg_path}")
|
|
273
|
+
print(f"Configured endpoint: {endpoint}")
|
|
274
|
+
print(f"Provider: {cfg.provider}")
|
|
275
|
+
print(f"Compatibility mode: {cfg.compatibility.mode}")
|
|
276
|
+
print(f"Kimi base URL: {cfg.kimi.base_url}")
|
|
277
|
+
print(f"Kimi fallback base URL: {cfg.kimi.fallback_base_url or 'None'}")
|
|
278
|
+
print(f"Port status: {resolution.status}")
|
|
279
|
+
print(resolution.message)
|
|
280
|
+
|
|
281
|
+
if resolution.status == "already_running":
|
|
282
|
+
health = check_health(cfg.server.host, cfg.server.port)
|
|
283
|
+
print(f"Health: {health.status}")
|
|
284
|
+
print(health.message)
|
|
285
|
+
return 0 if health.ok else 1
|
|
286
|
+
|
|
287
|
+
if resolution.status == "available":
|
|
288
|
+
print("Service: not running")
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
if resolution.status in {"conflict", "fallback"}:
|
|
292
|
+
suggestion = resolve_port(
|
|
293
|
+
cfg.server.host,
|
|
294
|
+
cfg.server.port,
|
|
295
|
+
auto_port=True,
|
|
296
|
+
target_provider=cfg.provider,
|
|
297
|
+
)
|
|
298
|
+
if suggestion.status == "fallback":
|
|
299
|
+
print(f"Suggested endpoint: http://{cfg.server.host}:{suggestion.selected_port}/v1")
|
|
300
|
+
|
|
301
|
+
return 1
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def status(args: argparse.Namespace | None = None) -> int:
|
|
305
|
+
provider = getattr(args, "provider", None) if args else None
|
|
306
|
+
if provider:
|
|
307
|
+
config = load_config(provider=provider)
|
|
308
|
+
health = check_health(config.server.host, config.server.port)
|
|
309
|
+
endpoint = f"http://{config.server.host}:{config.server.port}/v1"
|
|
310
|
+
print(f"Provider: {provider}")
|
|
311
|
+
print(f"Endpoint: {endpoint}")
|
|
312
|
+
print(f"Status: {health.status}")
|
|
313
|
+
print(health.message)
|
|
314
|
+
return 0 if health.ok else 1
|
|
315
|
+
|
|
316
|
+
configured = list_configured_providers()
|
|
317
|
+
if configured:
|
|
318
|
+
print("KimiBridge Configured Endpoints:")
|
|
319
|
+
any_running = False
|
|
320
|
+
for p, path, cfg in configured:
|
|
321
|
+
h = check_health(cfg.server.host, cfg.server.port)
|
|
322
|
+
stat = "running" if h.ok else "stopped"
|
|
323
|
+
if h.ok:
|
|
324
|
+
any_running = True
|
|
325
|
+
print(f" • {p.capitalize():<10} http://{cfg.server.host}:{cfg.server.port}/v1 [{stat}] (config: {path.name})")
|
|
326
|
+
return 0 if any_running else 1
|
|
327
|
+
|
|
328
|
+
config = load_config()
|
|
329
|
+
health = check_health(config.server.host, config.server.port)
|
|
330
|
+
endpoint = f"http://{config.server.host}:{config.server.port}/v1"
|
|
331
|
+
print(f"Endpoint: {endpoint}")
|
|
332
|
+
print(f"Status: {health.status}")
|
|
333
|
+
print(health.message)
|
|
334
|
+
return 0 if health.ok else 1
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def run_service_action(action: str, provider: str | None = None) -> int:
|
|
338
|
+
result = service_action(action, provider=provider)
|
|
339
|
+
print(result.message)
|
|
340
|
+
if result.output:
|
|
341
|
+
print(result.output.rstrip())
|
|
342
|
+
return 0 if result.ok else 1
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def logs(args: argparse.Namespace | None = None) -> int:
|
|
346
|
+
provider = getattr(args, "provider", None) if args else None
|
|
347
|
+
result = service_logs(provider=provider)
|
|
348
|
+
print(result.message)
|
|
349
|
+
if result.output:
|
|
350
|
+
print(result.output.rstrip())
|
|
351
|
+
return 0 if result.ok else 1
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def uninstall(args: argparse.Namespace | None = None) -> int:
|
|
355
|
+
is_all = getattr(args, "all", False) if args else False
|
|
356
|
+
provider = getattr(args, "provider", None) if args and not is_all else None
|
|
357
|
+
result = service_uninstall(provider=provider, all_instances=is_all)
|
|
358
|
+
print(result.message)
|
|
359
|
+
if result.output:
|
|
360
|
+
print(result.output.rstrip())
|
|
361
|
+
return 0 if result.ok else 1
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def config_command(args: argparse.Namespace) -> int:
|
|
365
|
+
provider = getattr(args, "provider", None)
|
|
366
|
+
if args.config_command == "show":
|
|
367
|
+
config = load_config(provider=provider)
|
|
368
|
+
print(json.dumps(config_to_dict(config), indent=2))
|
|
369
|
+
print(f"Config path: {default_config_path(provider)}")
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
if args.config_command == "set":
|
|
373
|
+
return set_config_value(args.key, args.value, provider=provider)
|
|
374
|
+
|
|
375
|
+
print("Usage: kimibridge config show|set")
|
|
376
|
+
return 2
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def set_config_value(key: str, value: str, provider: str | None = None) -> int:
|
|
380
|
+
config = load_config(provider=provider)
|
|
381
|
+
|
|
382
|
+
if key == "port":
|
|
383
|
+
try:
|
|
384
|
+
port = int(value)
|
|
385
|
+
except ValueError:
|
|
386
|
+
print("Port must be a number.")
|
|
387
|
+
return 2
|
|
388
|
+
|
|
389
|
+
if not 1 <= port <= 65535:
|
|
390
|
+
print("Port must be between 1 and 65535.")
|
|
391
|
+
return 2
|
|
392
|
+
|
|
393
|
+
config = replace(config, server=replace(config.server, port=port))
|
|
394
|
+
|
|
395
|
+
elif key == "host":
|
|
396
|
+
config = replace(config, server=replace(config.server, host=value))
|
|
397
|
+
|
|
398
|
+
elif key == "base-url":
|
|
399
|
+
clean_url = value.strip()
|
|
400
|
+
new_provider = config.provider
|
|
401
|
+
if "deepseek.com" in clean_url.lower() and config.provider in {"kimi", "auto"}:
|
|
402
|
+
new_provider = "deepseek"
|
|
403
|
+
elif "moonshot.ai" in clean_url.lower() and config.provider == "deepseek":
|
|
404
|
+
new_provider = "kimi"
|
|
405
|
+
elif "openai.com" in clean_url.lower() and config.provider in {"kimi", "auto"}:
|
|
406
|
+
new_provider = "openai"
|
|
407
|
+
|
|
408
|
+
config = replace(
|
|
409
|
+
config,
|
|
410
|
+
provider=new_provider,
|
|
411
|
+
kimi=replace(config.kimi, base_url=clean_url),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
elif key == "fallback-base-url":
|
|
415
|
+
fallback_val = value if value.strip().lower() != "none" and value.strip() != "" else None
|
|
416
|
+
config = replace(config, kimi=replace(config.kimi, fallback_base_url=fallback_val))
|
|
417
|
+
|
|
418
|
+
elif key == "compatibility-mode":
|
|
419
|
+
if value not in VALID_MODES:
|
|
420
|
+
print(f"Compatibility mode must be one of: {', '.join(sorted(VALID_MODES))}")
|
|
421
|
+
return 2
|
|
422
|
+
config = replace(config, compatibility=CompatibilityConfig(mode=value))
|
|
423
|
+
|
|
424
|
+
elif key == "provider":
|
|
425
|
+
valid_providers = {"kimi", "moonshot", "deepseek", "openai", "generic", "auto", "all"}
|
|
426
|
+
val_clean = value.strip().lower()
|
|
427
|
+
if val_clean not in valid_providers:
|
|
428
|
+
print(f"Provider must be one of: {', '.join(sorted(valid_providers))}")
|
|
429
|
+
return 2
|
|
430
|
+
|
|
431
|
+
new_base_url = config.kimi.base_url
|
|
432
|
+
if val_clean == "deepseek" and (config.kimi.base_url == DEFAULT_BASE_URL or "moonshot.ai" in config.kimi.base_url):
|
|
433
|
+
new_base_url = "https://api.deepseek.com"
|
|
434
|
+
elif val_clean in {"kimi", "moonshot"} and ("deepseek.com" in config.kimi.base_url):
|
|
435
|
+
new_base_url = DEFAULT_BASE_URL
|
|
436
|
+
elif val_clean == "openai" and (config.kimi.base_url == DEFAULT_BASE_URL or "moonshot.ai" in config.kimi.base_url):
|
|
437
|
+
new_base_url = "https://api.openai.com"
|
|
438
|
+
|
|
439
|
+
config = replace(
|
|
440
|
+
config,
|
|
441
|
+
provider=val_clean,
|
|
442
|
+
kimi=replace(config.kimi, base_url=new_base_url),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
print(f"Unknown config key: {key}")
|
|
447
|
+
return 2
|
|
448
|
+
|
|
449
|
+
config_path = save_config(config, provider=provider or config.provider)
|
|
450
|
+
print(f"Saved config to {config_path}")
|
|
451
|
+
health = check_health(config.server.host, config.server.port)
|
|
452
|
+
if health.ok:
|
|
453
|
+
restart_res = service_action("restart", provider=provider or config.provider)
|
|
454
|
+
if restart_res.ok:
|
|
455
|
+
print("Background service restarted to apply changes.")
|
|
456
|
+
return 0
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
sys.exit(main())
|
|
461
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from kimibridge.compatibility.errors import normalize_error
|
|
2
|
+
from kimibridge.compatibility.models import normalized_models
|
|
3
|
+
from kimibridge.compatibility.pipeline import normalize_request
|
|
4
|
+
from kimibridge.compatibility.profiles import (
|
|
5
|
+
DEEPSEEK_PROFILE,
|
|
6
|
+
KIMI_PROFILE,
|
|
7
|
+
OPENAI_PROFILE,
|
|
8
|
+
ProviderProfile,
|
|
9
|
+
detect_provider,
|
|
10
|
+
get_provider_profile,
|
|
11
|
+
)
|
|
12
|
+
from kimibridge.compatibility.roles import normalize_roles
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DEEPSEEK_PROFILE",
|
|
16
|
+
"KIMI_PROFILE",
|
|
17
|
+
"OPENAI_PROFILE",
|
|
18
|
+
"ProviderProfile",
|
|
19
|
+
"detect_provider",
|
|
20
|
+
"get_provider_profile",
|
|
21
|
+
"normalize_error",
|
|
22
|
+
"normalized_models",
|
|
23
|
+
"normalize_request",
|
|
24
|
+
"normalize_roles",
|
|
25
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_error(status: int, body: bytes) -> dict[str, Any]:
|
|
8
|
+
message = "Upstream error"
|
|
9
|
+
error_type = "invalid_request_error" if status in {400, 422} else "upstream_error"
|
|
10
|
+
code = "invalid_parameters" if status == 422 else None
|
|
11
|
+
|
|
12
|
+
if status in {401, 403}:
|
|
13
|
+
message = "Upstream authentication or permission error"
|
|
14
|
+
error_type = "authentication_error" if status == 401 else "permission_error"
|
|
15
|
+
code = "upstream_auth_error" if status == 401 else "upstream_permission_denied"
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
payload = json.loads(body.decode("utf-8"))
|
|
19
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
20
|
+
payload = None
|
|
21
|
+
|
|
22
|
+
if isinstance(payload, dict):
|
|
23
|
+
raw_error = payload.get("error")
|
|
24
|
+
if isinstance(raw_error, dict):
|
|
25
|
+
message = str(raw_error.get("message", message))
|
|
26
|
+
error_type = str(raw_error.get("type", error_type))
|
|
27
|
+
code = raw_error.get("code", code)
|
|
28
|
+
elif isinstance(raw_error, str):
|
|
29
|
+
message = raw_error
|
|
30
|
+
elif "detail" in payload:
|
|
31
|
+
raw_detail = payload["detail"]
|
|
32
|
+
if isinstance(raw_detail, str):
|
|
33
|
+
message = raw_detail
|
|
34
|
+
elif isinstance(raw_detail, list):
|
|
35
|
+
parts = []
|
|
36
|
+
for item in raw_detail:
|
|
37
|
+
if isinstance(item, dict):
|
|
38
|
+
loc = ".".join(str(p) for p in item.get("loc", []))
|
|
39
|
+
msg = item.get("msg", "")
|
|
40
|
+
parts.append(f"{loc}: {msg}" if loc else str(msg))
|
|
41
|
+
else:
|
|
42
|
+
parts.append(str(item))
|
|
43
|
+
message = "; ".join(parts) if parts else "Invalid request parameters"
|
|
44
|
+
else:
|
|
45
|
+
message = str(raw_detail)
|
|
46
|
+
elif "message" in payload:
|
|
47
|
+
message = str(payload["message"])
|
|
48
|
+
|
|
49
|
+
if status == 403 and message == "Upstream authentication or permission error":
|
|
50
|
+
message = (
|
|
51
|
+
"Upstream returned 403 Forbidden. Check that the API key belongs to the configured "
|
|
52
|
+
"provider and that the base URL matches the key issuer."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"error": {
|
|
57
|
+
"message": message,
|
|
58
|
+
"type": error_type,
|
|
59
|
+
"param": None,
|
|
60
|
+
"code": code or f"upstream_http_{status}",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from kimibridge.compatibility.profiles import ProviderProfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEEPSEEK_MODEL_ALIASES = {
|
|
10
|
+
"deepseek-chat": ("deepseek-flash", None),
|
|
11
|
+
"deepseek-coder": ("deepseek-flash", None),
|
|
12
|
+
"deepseek-v3": ("deepseek-flash", None),
|
|
13
|
+
"deepseek-v3.1": ("deepseek-flash", None),
|
|
14
|
+
"deepseek-v3.2": ("deepseek-flash", None),
|
|
15
|
+
"deepseek-v4-flash": ("deepseek-flash", None),
|
|
16
|
+
"deepseek-v4.1-flash": ("deepseek-flash", None),
|
|
17
|
+
"deepseek-reasoner": ("deepseek-flash", "high"),
|
|
18
|
+
"deepseek-r1": ("deepseek-flash", "high"),
|
|
19
|
+
"deepseek-v3-pro": ("deepseek-v4-pro", None),
|
|
20
|
+
"deepseek-v3.1-pro": ("deepseek-v4-pro", None),
|
|
21
|
+
"deepseek-v3.2-pro": ("deepseek-v4-pro", None),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_model_aliases(
|
|
26
|
+
payload: dict[str, Any],
|
|
27
|
+
profile: ProviderProfile,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
normalized = deepcopy(payload)
|
|
30
|
+
if profile.name != "deepseek":
|
|
31
|
+
return normalized
|
|
32
|
+
|
|
33
|
+
model = normalized.get("model")
|
|
34
|
+
if not isinstance(model, str):
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
upstream_model, default_effort = DEEPSEEK_MODEL_ALIASES.get(model.lower(), (None, None))
|
|
38
|
+
if upstream_model is None:
|
|
39
|
+
return normalized
|
|
40
|
+
|
|
41
|
+
normalized["model"] = upstream_model
|
|
42
|
+
if default_effort is not None and "reasoning_effort" not in normalized:
|
|
43
|
+
normalized["reasoning_effort"] = default_effort
|
|
44
|
+
return normalized
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from kimibridge.compatibility.profiles import (
|
|
6
|
+
DEEPSEEK_PROFILE,
|
|
7
|
+
KIMI_PROFILE,
|
|
8
|
+
OPENAI_PROFILE,
|
|
9
|
+
ProviderProfile,
|
|
10
|
+
detect_provider,
|
|
11
|
+
get_provider_profile,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DEFAULT_MODELS = list(KIMI_PROFILE.default_models)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalized_models(
|
|
18
|
+
provider: str | ProviderProfile | None = None,
|
|
19
|
+
base_url: str | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
target_provider = provider
|
|
22
|
+
if (
|
|
23
|
+
target_provider is None
|
|
24
|
+
or (isinstance(target_provider, str) and target_provider.strip().lower() in {"auto", "default"})
|
|
25
|
+
) and base_url:
|
|
26
|
+
target_provider = detect_provider(base_url=base_url)
|
|
27
|
+
|
|
28
|
+
if target_provider is not None:
|
|
29
|
+
if isinstance(target_provider, str) and target_provider.strip().lower() in {"all", "both"}:
|
|
30
|
+
combined: list[dict[str, Any]] = []
|
|
31
|
+
seen_ids: set[str] = set()
|
|
32
|
+
profiles_order = (
|
|
33
|
+
(DEEPSEEK_PROFILE, KIMI_PROFILE, OPENAI_PROFILE)
|
|
34
|
+
if (base_url and "deepseek" in base_url.lower())
|
|
35
|
+
else (KIMI_PROFILE, DEEPSEEK_PROFILE, OPENAI_PROFILE)
|
|
36
|
+
)
|
|
37
|
+
for profile in profiles_order:
|
|
38
|
+
for m in profile.default_models:
|
|
39
|
+
if m["id"] not in seen_ids:
|
|
40
|
+
seen_ids.add(m["id"])
|
|
41
|
+
combined.append(m)
|
|
42
|
+
return {
|
|
43
|
+
"object": "list",
|
|
44
|
+
"data": combined,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
profile = (
|
|
48
|
+
target_provider
|
|
49
|
+
if isinstance(target_provider, ProviderProfile)
|
|
50
|
+
else get_provider_profile(target_provider)
|
|
51
|
+
)
|
|
52
|
+
if profile.default_models:
|
|
53
|
+
return {
|
|
54
|
+
"object": "list",
|
|
55
|
+
"data": list(profile.default_models),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if base_url:
|
|
59
|
+
detected = detect_provider(base_url=base_url)
|
|
60
|
+
if detected.default_models and detected.name != "generic":
|
|
61
|
+
return {
|
|
62
|
+
"object": "list",
|
|
63
|
+
"data": list(detected.default_models),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"object": "list",
|
|
68
|
+
"data": DEFAULT_MODELS,
|
|
69
|
+
}
|