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.
Files changed (39) hide show
  1. kimibridge/__init__.py +1 -0
  2. kimibridge/cli.py +461 -0
  3. kimibridge/compatibility/__init__.py +25 -0
  4. kimibridge/compatibility/errors.py +62 -0
  5. kimibridge/compatibility/model_aliases.py +44 -0
  6. kimibridge/compatibility/models.py +69 -0
  7. kimibridge/compatibility/parameters.py +29 -0
  8. kimibridge/compatibility/pipeline.py +42 -0
  9. kimibridge/compatibility/profiles.py +138 -0
  10. kimibridge/compatibility/responses.py +766 -0
  11. kimibridge/compatibility/roles.py +33 -0
  12. kimibridge/config.py +195 -0
  13. kimibridge/health.py +51 -0
  14. kimibridge/ports.py +255 -0
  15. kimibridge/server.py +309 -0
  16. kimibridge/service.py +182 -0
  17. kimibridge/upstream.py +168 -0
  18. studiorelay/__init__.py +1 -0
  19. studiorelay/cli.py +514 -0
  20. studiorelay/compatibility/__init__.py +25 -0
  21. studiorelay/compatibility/errors.py +62 -0
  22. studiorelay/compatibility/model_aliases.py +44 -0
  23. studiorelay/compatibility/models.py +69 -0
  24. studiorelay/compatibility/parameters.py +29 -0
  25. studiorelay/compatibility/pipeline.py +42 -0
  26. studiorelay/compatibility/profiles.py +138 -0
  27. studiorelay/compatibility/responses.py +766 -0
  28. studiorelay/compatibility/roles.py +33 -0
  29. studiorelay/config.py +326 -0
  30. studiorelay/health.py +53 -0
  31. studiorelay/ports.py +277 -0
  32. studiorelay/server.py +321 -0
  33. studiorelay/service.py +237 -0
  34. studiorelay/upstream.py +171 -0
  35. studiorelay-0.3.0.dist-info/METADATA +395 -0
  36. studiorelay-0.3.0.dist-info/RECORD +39 -0
  37. studiorelay-0.3.0.dist-info/WHEEL +5 -0
  38. studiorelay-0.3.0.dist-info/entry_points.txt +4 -0
  39. 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
+ }