fusionkit 0.8.0__tar.gz → 0.9.2__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.
- fusionkit-0.9.2/PKG-INFO +38 -0
- fusionkit-0.9.2/README.md +14 -0
- fusionkit-0.9.2/pyproject.toml +45 -0
- fusionkit-0.9.2/src/fusionkit_cli/__init__.py +10 -0
- fusionkit-0.9.2/src/fusionkit_cli/main.py +93 -0
- fusionkit-0.8.0/PKG-INFO +0 -12
- fusionkit-0.8.0/pyproject.toml +0 -24
- fusionkit-0.8.0/src/fusionkit_cli/__init__.py +0 -3
- fusionkit-0.8.0/src/fusionkit_cli/main.py +0 -885
- fusionkit-0.8.0/src/fusionkit_cli/onboarding.py +0 -151
- {fusionkit-0.8.0 → fusionkit-0.9.2}/LICENSE +0 -0
fusionkit-0.9.2/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fusionkit
|
|
3
|
+
Version: 0.9.2
|
|
4
|
+
Summary: Internal Python synthesis sidecar for the FusionKit Node CLI.
|
|
5
|
+
Keywords: fusionkit,model-fusion,llm,ensemble,sidecar
|
|
6
|
+
Author: Velum Labs
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Dist: fusionkit-core==0.9.2
|
|
16
|
+
Requires-Dist: fusionkit-server==0.9.2
|
|
17
|
+
Requires-Dist: typer>=0.27.0
|
|
18
|
+
Requires-Dist: uvicorn>=0.50.0
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Homepage, https://fusionkit.velum-labs.com
|
|
21
|
+
Project-URL: Repository, https://github.com/velum-labs/handoffkit
|
|
22
|
+
Project-URL: Documentation, https://fusionkit.velum-labs.com
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# fusionkit-sidecar (PyPI package `fusionkit`)
|
|
26
|
+
|
|
27
|
+
PyPI `fusionkit` contains the internal synthesis sidecar driven by the npm
|
|
28
|
+
`@fusionkit/cli` front door.
|
|
29
|
+
|
|
30
|
+
The Python package installs only `fusionkit-sidecar`; it deliberately does not
|
|
31
|
+
install a `fusionkit` binary. The user-facing `fusionkit` command belongs to
|
|
32
|
+
the Node package. The Node CLI provisions the sidecar automatically.
|
|
33
|
+
|
|
34
|
+
Maintainer benchmark commands are published separately as `fusionkit-bench`
|
|
35
|
+
by `fusionkit-evals`.
|
|
36
|
+
|
|
37
|
+
Docs: https://fusionkit.velum-labs.com
|
|
38
|
+
Repository: https://github.com/velum-labs/handoffkit
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# fusionkit-sidecar (PyPI package `fusionkit`)
|
|
2
|
+
|
|
3
|
+
PyPI `fusionkit` contains the internal synthesis sidecar driven by the npm
|
|
4
|
+
`@fusionkit/cli` front door.
|
|
5
|
+
|
|
6
|
+
The Python package installs only `fusionkit-sidecar`; it deliberately does not
|
|
7
|
+
install a `fusionkit` binary. The user-facing `fusionkit` command belongs to
|
|
8
|
+
the Node package. The Node CLI provisions the sidecar automatically.
|
|
9
|
+
|
|
10
|
+
Maintainer benchmark commands are published separately as `fusionkit-bench`
|
|
11
|
+
by `fusionkit-evals`.
|
|
12
|
+
|
|
13
|
+
Docs: https://fusionkit.velum-labs.com
|
|
14
|
+
Repository: https://github.com/velum-labs/handoffkit
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fusionkit"
|
|
3
|
+
version = "0.9.2"
|
|
4
|
+
description = "Internal Python synthesis sidecar for the FusionKit Node CLI."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Velum Labs" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"fusionkit",
|
|
12
|
+
"model-fusion",
|
|
13
|
+
"llm",
|
|
14
|
+
"ensemble",
|
|
15
|
+
"sidecar",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"fusionkit-core==0.9.2",
|
|
27
|
+
"fusionkit-server==0.9.2",
|
|
28
|
+
"typer>=0.27.0",
|
|
29
|
+
"uvicorn>=0.50.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://fusionkit.velum-labs.com"
|
|
34
|
+
Repository = "https://github.com/velum-labs/handoffkit"
|
|
35
|
+
Documentation = "https://fusionkit.velum-labs.com"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
fusionkit-sidecar = "fusionkit_cli.main:app"
|
|
39
|
+
|
|
40
|
+
[tool.uv.build-backend]
|
|
41
|
+
module-name = "fusionkit_cli"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.11.29,<0.12.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Public API for the FusionKit Python CLI package.
|
|
2
|
+
|
|
3
|
+
The package exposes the Typer application object that backs the PyPI `fusionkit`
|
|
4
|
+
console script. Generated code documentation uses this docstring to explain the
|
|
5
|
+
CLI package surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fusionkit_cli.main import app
|
|
9
|
+
|
|
10
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib.metadata import PackageNotFoundError
|
|
5
|
+
from importlib.metadata import version as distribution_version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import uvicorn
|
|
11
|
+
from fusionkit_core.config import load_config
|
|
12
|
+
from fusionkit_core.prompts import SYSTEM_PROMPT_DEFAULTS
|
|
13
|
+
from fusionkit_core.trace import setup_fusion_tracing
|
|
14
|
+
from fusionkit_server.app import create_app
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
help="Internal FusionKit synthesis sidecar.",
|
|
18
|
+
invoke_without_command=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _distribution_version() -> str:
|
|
23
|
+
try:
|
|
24
|
+
return distribution_version("fusionkit")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
return "0.0.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _version_callback(value: bool) -> None:
|
|
30
|
+
if value:
|
|
31
|
+
typer.echo(f"fusionkit-sidecar {_distribution_version()}")
|
|
32
|
+
raise typer.Exit()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback()
|
|
36
|
+
def main(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
version: Annotated[
|
|
39
|
+
bool | None,
|
|
40
|
+
typer.Option(
|
|
41
|
+
"--version",
|
|
42
|
+
"-V",
|
|
43
|
+
callback=_version_callback,
|
|
44
|
+
is_eager=True,
|
|
45
|
+
help="Show the sidecar version and exit.",
|
|
46
|
+
),
|
|
47
|
+
] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if ctx.invoked_subcommand is None and version is None:
|
|
50
|
+
typer.echo(ctx.get_help())
|
|
51
|
+
raise typer.Exit()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def serve(
|
|
56
|
+
config: Annotated[Path, typer.Option("--config", "-c")],
|
|
57
|
+
host: Annotated[str, typer.Option("--host")] = "127.0.0.1",
|
|
58
|
+
port: Annotated[int, typer.Option("--port")] = 8080,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Run the internal RouteKit-backed synthesis sidecar."""
|
|
61
|
+
setup_fusion_tracing("fusionkit-sidecar")
|
|
62
|
+
api = create_app(load_config(config))
|
|
63
|
+
uvicorn.run(api, host=host, port=port)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
prompts_app = typer.Typer(help="Inspect the built-in synthesis prompts.")
|
|
67
|
+
app.add_typer(prompts_app, name="prompts")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@prompts_app.command("dump")
|
|
71
|
+
def prompts_dump(
|
|
72
|
+
directory: Annotated[
|
|
73
|
+
Path | None,
|
|
74
|
+
typer.Option(
|
|
75
|
+
"--dir",
|
|
76
|
+
help="write each prompt to <dir>/<id>.md instead of stdout",
|
|
77
|
+
),
|
|
78
|
+
] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
if directory is not None:
|
|
81
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
for prompt_id, text in SYSTEM_PROMPT_DEFAULTS.items():
|
|
83
|
+
(directory / f"{prompt_id}.md").write_text(text + "\n")
|
|
84
|
+
typer.echo(
|
|
85
|
+
json.dumps(
|
|
86
|
+
{"dir": str(directory), "count": len(SYSTEM_PROMPT_DEFAULTS)}
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return
|
|
90
|
+
typer.echo(json.dumps(SYSTEM_PROMPT_DEFAULTS, indent=2))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["app", "main", "prompts_dump", "serve"]
|
fusionkit-0.8.0/PKG-INFO
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fusionkit
|
|
3
|
-
Version: 0.8.0
|
|
4
|
-
Summary: Command line interface for fusionkit: local response-level model fusion.
|
|
5
|
-
License-Expression: Apache-2.0
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Dist: fusionkit-core==0.8.0
|
|
8
|
-
Requires-Dist: fusionkit-evals==0.8.0
|
|
9
|
-
Requires-Dist: fusionkit-mlx==0.8.0
|
|
10
|
-
Requires-Dist: fusionkit-server==0.8.0
|
|
11
|
-
Requires-Dist: typer>=0.20.0
|
|
12
|
-
Requires-Python: >=3.11
|
fusionkit-0.8.0/pyproject.toml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "fusionkit"
|
|
3
|
-
version = "0.8.0"
|
|
4
|
-
description = "Command line interface for fusionkit: local response-level model fusion."
|
|
5
|
-
requires-python = ">=3.11"
|
|
6
|
-
license = "Apache-2.0"
|
|
7
|
-
license-files = ["LICENSE"]
|
|
8
|
-
dependencies = [
|
|
9
|
-
"fusionkit-core==0.8.0",
|
|
10
|
-
"fusionkit-evals==0.8.0",
|
|
11
|
-
"fusionkit-mlx==0.8.0",
|
|
12
|
-
"fusionkit-server==0.8.0",
|
|
13
|
-
"typer>=0.20.0",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
[project.scripts]
|
|
17
|
-
fusionkit = "fusionkit_cli.main:app"
|
|
18
|
-
|
|
19
|
-
[tool.uv.build-backend]
|
|
20
|
-
module-name = "fusionkit_cli"
|
|
21
|
-
|
|
22
|
-
[build-system]
|
|
23
|
-
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
24
|
-
build-backend = "uv_build"
|
|
@@ -1,885 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import shlex
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Annotated, cast
|
|
9
|
-
|
|
10
|
-
import typer
|
|
11
|
-
import uvicorn
|
|
12
|
-
from fusionkit_core.clients import build_clients
|
|
13
|
-
from fusionkit_core.config import (
|
|
14
|
-
EndpointAuth,
|
|
15
|
-
FusionConfig,
|
|
16
|
-
FusionMode,
|
|
17
|
-
ModelEndpoint,
|
|
18
|
-
ProviderKind,
|
|
19
|
-
SubscriptionAuthMode,
|
|
20
|
-
load_config,
|
|
21
|
-
)
|
|
22
|
-
from fusionkit_core.credentials import SubscriptionStatus, subscription_status
|
|
23
|
-
from fusionkit_core.fusion import FusionEngine
|
|
24
|
-
from fusionkit_core.prompts import SYSTEM_PROMPT_DEFAULTS
|
|
25
|
-
from fusionkit_evals.bench_history import BenchRunRecord, append_run, drift_vs_previous
|
|
26
|
-
from fusionkit_evals.benchmark import BenchmarkRunner, load_jsonl_samples, write_jsonl_results
|
|
27
|
-
from fusionkit_evals.benchmark_panel import get_benchmark_panel
|
|
28
|
-
from fusionkit_evals.candidate_bank import (
|
|
29
|
-
PreparedTask,
|
|
30
|
-
bank_signature,
|
|
31
|
-
build_candidate_bank,
|
|
32
|
-
load_bank,
|
|
33
|
-
save_bank,
|
|
34
|
-
)
|
|
35
|
-
from fusionkit_evals.fusion_bench import (
|
|
36
|
-
CommandHandoffKitExecutor,
|
|
37
|
-
FusionBenchReport,
|
|
38
|
-
FusionBenchRunner,
|
|
39
|
-
build_fusion_bench_report,
|
|
40
|
-
load_benchmark_tasks,
|
|
41
|
-
load_fusion_bench_jsonl,
|
|
42
|
-
write_fusion_bench_jsonl,
|
|
43
|
-
)
|
|
44
|
-
from fusionkit_evals.fusion_reports import (
|
|
45
|
-
write_fusion_bench_html_report,
|
|
46
|
-
write_fusion_bench_markdown_report,
|
|
47
|
-
write_fusion_bench_report_jsonl,
|
|
48
|
-
)
|
|
49
|
-
from fusionkit_evals.livecodebench_data import (
|
|
50
|
-
LCB_PROMPT_SUFFIX,
|
|
51
|
-
load_manifest,
|
|
52
|
-
load_problems,
|
|
53
|
-
prepare_tasks,
|
|
54
|
-
)
|
|
55
|
-
from fusionkit_evals.pareto import load_points, write_pareto_report
|
|
56
|
-
from fusionkit_evals.prompt_tuning import (
|
|
57
|
-
LLMProposer,
|
|
58
|
-
TunableRole,
|
|
59
|
-
TunerRuntime,
|
|
60
|
-
TuningResult,
|
|
61
|
-
optimize,
|
|
62
|
-
select_decision_tasks,
|
|
63
|
-
split_dev_val,
|
|
64
|
-
)
|
|
65
|
-
from fusionkit_evals.public_bench import (
|
|
66
|
-
PUBLIC_BENCHMARK_INFO,
|
|
67
|
-
PUBLIC_BENCHMARK_SUITES,
|
|
68
|
-
CommandExternalBenchmarkExecutor,
|
|
69
|
-
ExternalBenchmarkRequest,
|
|
70
|
-
PublicBenchmarkSuite,
|
|
71
|
-
baselines_for,
|
|
72
|
-
run_public_benchmark,
|
|
73
|
-
write_external_runs_jsonl,
|
|
74
|
-
)
|
|
75
|
-
from fusionkit_evals.public_bench_report import (
|
|
76
|
-
build_benchmark_comparison,
|
|
77
|
-
write_benchmark_comparison_markdown,
|
|
78
|
-
)
|
|
79
|
-
from fusionkit_evals.sandbox import SandboxConfig, build_sandbox
|
|
80
|
-
from fusionkit_evals.tiny import (
|
|
81
|
-
load_tiny_tasks,
|
|
82
|
-
run_tiny_benchmark,
|
|
83
|
-
write_tiny_benchmark_report,
|
|
84
|
-
write_tiny_jsonl,
|
|
85
|
-
)
|
|
86
|
-
from fusionkit_server.app import create_app
|
|
87
|
-
from fusionkit_server.openai_endpoint import (
|
|
88
|
-
PROVIDER_DEFAULT_BASE_URL,
|
|
89
|
-
build_endpoint,
|
|
90
|
-
serve_single_endpoint,
|
|
91
|
-
)
|
|
92
|
-
from rich.console import Console
|
|
93
|
-
from rich.table import Table
|
|
94
|
-
|
|
95
|
-
from fusionkit_cli.onboarding import (
|
|
96
|
-
API_KEY_ENVS,
|
|
97
|
-
api_key_endpoint,
|
|
98
|
-
default_write_path,
|
|
99
|
-
detect_api_keys,
|
|
100
|
-
resolve_config_path,
|
|
101
|
-
subscription_endpoint,
|
|
102
|
-
write_config,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
app = typer.Typer(help="Local model fusion toolkit.")
|
|
106
|
-
|
|
107
|
-
prompts_app = typer.Typer(help="Inspect and export the built-in fusion prompts.")
|
|
108
|
-
app.add_typer(prompts_app, name="prompts")
|
|
109
|
-
|
|
110
|
-
auth_app = typer.Typer(help="Inspect and switch model authentication (API key / subscription).")
|
|
111
|
-
app.add_typer(auth_app, name="auth")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@prompts_app.command("dump")
|
|
115
|
-
def prompts_dump(
|
|
116
|
-
dir: Annotated[
|
|
117
|
-
Path | None,
|
|
118
|
-
typer.Option("--dir", help="write each default prompt to <dir>/<id>.md instead of stdout"),
|
|
119
|
-
] = None,
|
|
120
|
-
) -> None:
|
|
121
|
-
"""Emit the built-in system prompts so a consumer can scaffold editable overrides.
|
|
122
|
-
|
|
123
|
-
With no options this prints a JSON object mapping each prompt id (``judge``,
|
|
124
|
-
``synthesizer``) to its default text. With ``--dir`` it writes one
|
|
125
|
-
``<id>.md`` file per prompt. This keeps the CLI's scaffolded
|
|
126
|
-
``.fusionkit/prompts`` defaults in lockstep with this package's source.
|
|
127
|
-
"""
|
|
128
|
-
if dir is not None:
|
|
129
|
-
dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
-
for prompt_id, text in SYSTEM_PROMPT_DEFAULTS.items():
|
|
131
|
-
(dir / f"{prompt_id}.md").write_text(text + "\n")
|
|
132
|
-
typer.echo(json.dumps({"dir": str(dir), "count": len(SYSTEM_PROMPT_DEFAULTS)}))
|
|
133
|
-
return
|
|
134
|
-
typer.echo(json.dumps(SYSTEM_PROMPT_DEFAULTS, indent=2))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@app.command()
|
|
138
|
-
def serve(
|
|
139
|
-
config: Annotated[Path | None, typer.Option("--config", "-c")] = None,
|
|
140
|
-
host: str = "127.0.0.1",
|
|
141
|
-
port: int = 8080,
|
|
142
|
-
) -> None:
|
|
143
|
-
resolved = resolve_config_path(config)
|
|
144
|
-
if resolved is None:
|
|
145
|
-
typer.secho(
|
|
146
|
-
"No config found. Run `fusionkit init` to create one, or pass --config.",
|
|
147
|
-
fg=typer.colors.RED,
|
|
148
|
-
err=True,
|
|
149
|
-
)
|
|
150
|
-
raise typer.Exit(code=1)
|
|
151
|
-
fusion_config = load_config(resolved)
|
|
152
|
-
api = create_app(fusion_config)
|
|
153
|
-
uvicorn.run(api, host=host, port=port)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _status_label(status: SubscriptionStatus) -> str:
|
|
157
|
-
if not status.available:
|
|
158
|
-
return "not logged in"
|
|
159
|
-
hours = status.hours_to_expiry
|
|
160
|
-
if status.expired:
|
|
161
|
-
return "expired (re-login)"
|
|
162
|
-
if hours is not None:
|
|
163
|
-
return f"logged in (expires in {hours:.1f}h)"
|
|
164
|
-
return "logged in"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@app.command()
|
|
168
|
-
def init(
|
|
169
|
-
output: Annotated[
|
|
170
|
-
Path | None, typer.Option("--output", "-o", help="config path to write")
|
|
171
|
-
] = None,
|
|
172
|
-
global_: Annotated[
|
|
173
|
-
bool, typer.Option("--global", help="write to ~/.config/fusionkit/models.yaml")
|
|
174
|
-
] = False,
|
|
175
|
-
yes: Annotated[
|
|
176
|
-
bool, typer.Option("--yes", "-y", help="accept all detected sources non-interactively")
|
|
177
|
-
] = False,
|
|
178
|
-
force: Annotated[bool, typer.Option("--force", help="overwrite an existing config")] = False,
|
|
179
|
-
) -> None:
|
|
180
|
-
"""Detect logged-in subscriptions + API keys and scaffold a config."""
|
|
181
|
-
target = output or default_write_path(global_)
|
|
182
|
-
if target.exists() and not force:
|
|
183
|
-
typer.secho(
|
|
184
|
-
f"{target} already exists; pass --force to overwrite or -o to choose another path.",
|
|
185
|
-
fg=typer.colors.RED,
|
|
186
|
-
err=True,
|
|
187
|
-
)
|
|
188
|
-
raise typer.Exit(code=1)
|
|
189
|
-
|
|
190
|
-
claude = subscription_status("claude-code")
|
|
191
|
-
codex = subscription_status("codex")
|
|
192
|
-
api_keys = detect_api_keys()
|
|
193
|
-
|
|
194
|
-
typer.echo("Detected:")
|
|
195
|
-
typer.echo(f" Claude Code subscription : {_status_label(claude)}")
|
|
196
|
-
typer.echo(f" Codex subscription : {_status_label(codex)}")
|
|
197
|
-
typer.echo(f" API keys : {', '.join(api_keys.values()) or 'none'}")
|
|
198
|
-
typer.echo("")
|
|
199
|
-
|
|
200
|
-
def want(label: str) -> bool:
|
|
201
|
-
return True if yes else typer.confirm(f"Add {label}?", default=True)
|
|
202
|
-
|
|
203
|
-
endpoints: list[ModelEndpoint] = []
|
|
204
|
-
if claude.available and want("Claude Code subscription (claude-code)"):
|
|
205
|
-
endpoints.append(subscription_endpoint("claude-code"))
|
|
206
|
-
if codex.available and want("Codex subscription (codex)"):
|
|
207
|
-
endpoints.append(subscription_endpoint("codex"))
|
|
208
|
-
for provider, env_var in api_keys.items():
|
|
209
|
-
if want(f"{provider} via {env_var}"):
|
|
210
|
-
endpoints.append(api_key_endpoint(provider))
|
|
211
|
-
|
|
212
|
-
if not endpoints:
|
|
213
|
-
typer.secho(
|
|
214
|
-
"Nothing selected. Log in with `claude` / `codex login`, or set "
|
|
215
|
-
"OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY, then re-run `fusionkit init`.",
|
|
216
|
-
fg=typer.colors.YELLOW,
|
|
217
|
-
err=True,
|
|
218
|
-
)
|
|
219
|
-
raise typer.Exit(code=1)
|
|
220
|
-
|
|
221
|
-
choices = [endpoint.id for endpoint in endpoints]
|
|
222
|
-
default_model = choices[0]
|
|
223
|
-
mode: str = "router"
|
|
224
|
-
if not yes:
|
|
225
|
-
default_model = typer.prompt("Default model id", default=default_model)
|
|
226
|
-
if default_model not in choices:
|
|
227
|
-
raise typer.BadParameter(f"default model must be one of {choices}")
|
|
228
|
-
mode = typer.prompt("Default mode (single/self/panel/router)", default="router")
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
config = FusionConfig(
|
|
232
|
-
endpoints=endpoints,
|
|
233
|
-
default_model=default_model,
|
|
234
|
-
default_mode=cast(FusionMode, mode),
|
|
235
|
-
panel_models=choices,
|
|
236
|
-
)
|
|
237
|
-
except Exception as exc: # noqa: BLE001 - surface pydantic validation as a CLI error
|
|
238
|
-
raise typer.BadParameter(str(exc)) from exc
|
|
239
|
-
|
|
240
|
-
write_config(config, target)
|
|
241
|
-
typer.secho(f"Wrote {target} with {len(endpoints)} endpoint(s).", fg=typer.colors.GREEN)
|
|
242
|
-
discovered = resolve_config_path(None)
|
|
243
|
-
serve_hint = "fusionkit serve" if discovered == target else f"fusionkit serve --config {target}"
|
|
244
|
-
typer.echo(f"Next: {serve_hint}")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@auth_app.command("status")
|
|
248
|
-
def auth_status(
|
|
249
|
-
config: Annotated[Path | None, typer.Option("--config", "-c")] = None,
|
|
250
|
-
) -> None:
|
|
251
|
-
"""Show subscription logins, API keys, and how the config authenticates."""
|
|
252
|
-
console = Console()
|
|
253
|
-
subs = Table(title="Subscriptions")
|
|
254
|
-
subs.add_column("mode")
|
|
255
|
-
subs.add_column("state")
|
|
256
|
-
subs.add_column("account")
|
|
257
|
-
for status in (subscription_status("claude-code"), subscription_status("codex")):
|
|
258
|
-
subs.add_row(status.mode, _status_label(status), status.account_id or "-")
|
|
259
|
-
console.print(subs)
|
|
260
|
-
|
|
261
|
-
api_keys = detect_api_keys()
|
|
262
|
-
console.print(
|
|
263
|
-
"API keys: "
|
|
264
|
-
+ (", ".join(f"{p} ({env})" for p, env in api_keys.items()) if api_keys else "none")
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
resolved = resolve_config_path(config)
|
|
268
|
-
if resolved is None:
|
|
269
|
-
console.print("Config: none found (run `fusionkit init`).")
|
|
270
|
-
return
|
|
271
|
-
cfg = load_config(resolved)
|
|
272
|
-
endpoints_table = Table(title=f"Config endpoints ({resolved})")
|
|
273
|
-
endpoints_table.add_column("id")
|
|
274
|
-
endpoints_table.add_column("provider")
|
|
275
|
-
endpoints_table.add_column("model")
|
|
276
|
-
endpoints_table.add_column("auth")
|
|
277
|
-
for endpoint in cfg.endpoints:
|
|
278
|
-
marker = " (default)" if endpoint.id == cfg.default_model else ""
|
|
279
|
-
endpoints_table.add_row(
|
|
280
|
-
endpoint.id + marker, endpoint.provider, endpoint.model, endpoint.auth.mode
|
|
281
|
-
)
|
|
282
|
-
console.print(endpoints_table)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def _switch_endpoint(
|
|
286
|
-
endpoint: ModelEndpoint, mode: SubscriptionAuthMode, api_key_env: str | None
|
|
287
|
-
) -> ModelEndpoint:
|
|
288
|
-
"""Return a copy of an endpoint with its auth mode changed (keeping provider coherent)."""
|
|
289
|
-
provider: ProviderKind = endpoint.provider
|
|
290
|
-
base_url = endpoint.base_url
|
|
291
|
-
resolved_key_env = endpoint.api_key_env
|
|
292
|
-
if mode == "claude-code":
|
|
293
|
-
provider = "anthropic"
|
|
294
|
-
elif mode == "codex":
|
|
295
|
-
provider = "codex"
|
|
296
|
-
else: # api_key
|
|
297
|
-
if provider == "codex":
|
|
298
|
-
# The codex provider only speaks the subscription Responses API; an
|
|
299
|
-
# API key means standard OpenAI chat completions.
|
|
300
|
-
provider = "openai"
|
|
301
|
-
resolved_key_env = api_key_env or endpoint.api_key_env or API_KEY_ENVS.get(provider)
|
|
302
|
-
if not base_url:
|
|
303
|
-
base_url = PROVIDER_DEFAULT_BASE_URL.get(provider, base_url)
|
|
304
|
-
return endpoint.model_copy(
|
|
305
|
-
update={
|
|
306
|
-
"provider": provider,
|
|
307
|
-
"base_url": base_url,
|
|
308
|
-
"api_key_env": resolved_key_env,
|
|
309
|
-
"auth": EndpointAuth(mode=mode),
|
|
310
|
-
}
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
@auth_app.command("switch")
|
|
315
|
-
def auth_switch(
|
|
316
|
-
endpoint_id: Annotated[str, typer.Argument(help="endpoint id to change")],
|
|
317
|
-
mode: Annotated[
|
|
318
|
-
str, typer.Option("--mode", help="api_key, claude-code, or codex")
|
|
319
|
-
],
|
|
320
|
-
api_key_env: Annotated[
|
|
321
|
-
str | None, typer.Option("--api-key-env", help="env var for api_key mode")
|
|
322
|
-
] = None,
|
|
323
|
-
config: Annotated[Path | None, typer.Option("--config", "-c")] = None,
|
|
324
|
-
) -> None:
|
|
325
|
-
"""Switch one endpoint between API key and a subscription."""
|
|
326
|
-
if mode not in ("api_key", "claude-code", "codex"):
|
|
327
|
-
raise typer.BadParameter("mode must be api_key, claude-code, or codex")
|
|
328
|
-
resolved = resolve_config_path(config)
|
|
329
|
-
if resolved is None:
|
|
330
|
-
raise typer.BadParameter("No config found; run `fusionkit init` or pass --config.")
|
|
331
|
-
cfg = load_config(resolved)
|
|
332
|
-
try:
|
|
333
|
-
endpoint = cfg.endpoint_for(endpoint_id)
|
|
334
|
-
except KeyError:
|
|
335
|
-
raise typer.BadParameter(
|
|
336
|
-
f"unknown endpoint {endpoint_id!r}; known: {[e.id for e in cfg.endpoints]}"
|
|
337
|
-
) from None
|
|
338
|
-
updated = _switch_endpoint(endpoint, cast(SubscriptionAuthMode, mode), api_key_env)
|
|
339
|
-
new_endpoints = [updated if e.id == endpoint_id else e for e in cfg.endpoints]
|
|
340
|
-
write_config(cfg.model_copy(update={"endpoints": new_endpoints}), resolved)
|
|
341
|
-
typer.secho(
|
|
342
|
-
f"{endpoint_id}: auth -> {mode} (provider {updated.provider}) in {resolved}",
|
|
343
|
-
fg=typer.colors.GREEN,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
@auth_app.command("set-default")
|
|
348
|
-
def auth_set_default(
|
|
349
|
-
endpoint_id: Annotated[str, typer.Argument(help="endpoint id to make the default model")],
|
|
350
|
-
config: Annotated[Path | None, typer.Option("--config", "-c")] = None,
|
|
351
|
-
) -> None:
|
|
352
|
-
"""Change the config's default_model."""
|
|
353
|
-
resolved = resolve_config_path(config)
|
|
354
|
-
if resolved is None:
|
|
355
|
-
raise typer.BadParameter("No config found; run `fusionkit init` or pass --config.")
|
|
356
|
-
cfg = load_config(resolved)
|
|
357
|
-
if endpoint_id not in {endpoint.id for endpoint in cfg.endpoints}:
|
|
358
|
-
raise typer.BadParameter(
|
|
359
|
-
f"unknown endpoint {endpoint_id!r}; known: {[e.id for e in cfg.endpoints]}"
|
|
360
|
-
)
|
|
361
|
-
write_config(cfg.model_copy(update={"default_model": endpoint_id}), resolved)
|
|
362
|
-
typer.secho(f"default_model -> {endpoint_id} in {resolved}", fg=typer.colors.GREEN)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
@auth_app.command("login")
|
|
366
|
-
def auth_login(
|
|
367
|
-
provider: Annotated[str, typer.Argument(help="claude-code or codex")],
|
|
368
|
-
) -> None:
|
|
369
|
-
"""Show how to log in (FusionKit reuses the official CLI logins; it does not own OAuth)."""
|
|
370
|
-
if provider not in ("claude-code", "codex"):
|
|
371
|
-
raise typer.BadParameter("provider must be claude-code or codex")
|
|
372
|
-
command = "claude" if provider == "claude-code" else "codex login"
|
|
373
|
-
typer.echo(f"FusionKit reuses the {provider} CLI login read-only. To (re)authenticate, run:")
|
|
374
|
-
typer.secho(f" {command}", fg=typer.colors.CYAN)
|
|
375
|
-
status = subscription_status(cast(SubscriptionAuthMode, provider))
|
|
376
|
-
typer.echo(f"Current status: {_status_label(status)}")
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
@app.command("serve-endpoint")
|
|
380
|
-
def serve_endpoint(
|
|
381
|
-
id: Annotated[str, typer.Option("--id", help="endpoint id exposed via /v1/models")],
|
|
382
|
-
model: Annotated[str, typer.Option("--model", help="provider model name (e.g. gpt-5.5)")],
|
|
383
|
-
port: Annotated[int, typer.Option("--port")],
|
|
384
|
-
provider: Annotated[str, typer.Option("--provider")] = "openai",
|
|
385
|
-
base_url: Annotated[
|
|
386
|
-
str | None, typer.Option("--base-url", help="override the provider base URL")
|
|
387
|
-
] = None,
|
|
388
|
-
api_key_env: Annotated[
|
|
389
|
-
str | None, typer.Option("--api-key-env", help="env var holding the API key")
|
|
390
|
-
] = None,
|
|
391
|
-
auth_mode: Annotated[
|
|
392
|
-
str,
|
|
393
|
-
typer.Option(
|
|
394
|
-
"--auth-mode",
|
|
395
|
-
help="credential source: api_key, claude-code, or codex (reuse the CLI login)",
|
|
396
|
-
),
|
|
397
|
-
] = "api_key",
|
|
398
|
-
credentials_path: Annotated[
|
|
399
|
-
str | None,
|
|
400
|
-
typer.Option("--credentials-path", help="override the CLI credential file path"),
|
|
401
|
-
] = None,
|
|
402
|
-
timeout_s: Annotated[float, typer.Option("--timeout-s")] = 120.0,
|
|
403
|
-
host: Annotated[str, typer.Option("--host")] = "127.0.0.1",
|
|
404
|
-
) -> None:
|
|
405
|
-
"""Front a single provider model as an OpenAI Chat Completions endpoint."""
|
|
406
|
-
endpoint = build_endpoint(
|
|
407
|
-
id=id,
|
|
408
|
-
model=model,
|
|
409
|
-
provider=provider,
|
|
410
|
-
base_url=base_url,
|
|
411
|
-
api_key_env=api_key_env,
|
|
412
|
-
timeout_s=timeout_s,
|
|
413
|
-
auth_mode=auth_mode,
|
|
414
|
-
credentials_path=credentials_path,
|
|
415
|
-
)
|
|
416
|
-
serve_single_endpoint(endpoint, host=host, port=port)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
@app.command("eval")
|
|
420
|
-
def run_eval(
|
|
421
|
-
config: Annotated[Path, typer.Option("--config", "-c")],
|
|
422
|
-
samples: Annotated[Path, typer.Option("--samples", "-s")],
|
|
423
|
-
output: Annotated[Path, typer.Option("--output", "-o")],
|
|
424
|
-
mode: FusionMode = "single",
|
|
425
|
-
config_id: str = "local",
|
|
426
|
-
) -> None:
|
|
427
|
-
fusion_config = load_config(config)
|
|
428
|
-
clients = build_clients(fusion_config)
|
|
429
|
-
engine = FusionEngine(config=fusion_config, clients=clients)
|
|
430
|
-
runner = BenchmarkRunner(engine)
|
|
431
|
-
results = asyncio.run(runner.run_samples(load_jsonl_samples(samples), config_id, mode))
|
|
432
|
-
write_jsonl_results(output, results)
|
|
433
|
-
typer.echo(json.dumps({"results": len(results), "output": str(output)}))
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@app.command()
|
|
437
|
-
def pareto(
|
|
438
|
-
points: Annotated[Path, typer.Option("--points", "-p")],
|
|
439
|
-
output: Annotated[Path, typer.Option("--output", "-o")],
|
|
440
|
-
) -> None:
|
|
441
|
-
write_pareto_report(output, load_points(points))
|
|
442
|
-
typer.echo(json.dumps({"output": str(output)}))
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
@app.command("tiny-bench")
|
|
446
|
-
def tiny_bench(
|
|
447
|
-
config: Annotated[Path, typer.Option("--config", "-c")],
|
|
448
|
-
output: Annotated[Path, typer.Option("--output", "-o")],
|
|
449
|
-
report: Annotated[Path | None, typer.Option("--report", "-r")] = None,
|
|
450
|
-
mode: FusionMode = "panel",
|
|
451
|
-
config_id: str = "local",
|
|
452
|
-
) -> None:
|
|
453
|
-
fusion_config = load_config(config)
|
|
454
|
-
clients = build_clients(fusion_config)
|
|
455
|
-
engine = FusionEngine(config=fusion_config, clients=clients)
|
|
456
|
-
results = asyncio.run(
|
|
457
|
-
run_tiny_benchmark(
|
|
458
|
-
engine,
|
|
459
|
-
config_id=config_id,
|
|
460
|
-
mode=mode,
|
|
461
|
-
tasks=load_tiny_tasks(),
|
|
462
|
-
model_versions={endpoint.id: endpoint.model for endpoint in fusion_config.endpoints},
|
|
463
|
-
)
|
|
464
|
-
)
|
|
465
|
-
write_tiny_jsonl(output, results)
|
|
466
|
-
response = {"results": len(results), "output": str(output)}
|
|
467
|
-
if report is not None:
|
|
468
|
-
write_tiny_benchmark_report(report, results)
|
|
469
|
-
response["report"] = str(report)
|
|
470
|
-
typer.echo(json.dumps(response))
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
@app.command("fusion-bench")
|
|
474
|
-
def fusion_bench(
|
|
475
|
-
config: Annotated[Path, typer.Option("--config", "-c")],
|
|
476
|
-
output: Annotated[Path, typer.Option("--output", "-o")],
|
|
477
|
-
manifest: Annotated[Path | None, typer.Option("--manifest", "-m")] = None,
|
|
478
|
-
run_root: Annotated[Path, typer.Option("--run-root")] = Path(".fusionkit/fusion-bench"),
|
|
479
|
-
report_jsonl: Annotated[Path | None, typer.Option("--report-jsonl")] = None,
|
|
480
|
-
report_markdown: Annotated[
|
|
481
|
-
Path | None,
|
|
482
|
-
typer.Option("--report", "-r", "--report-markdown"),
|
|
483
|
-
] = None,
|
|
484
|
-
report_html: Annotated[Path | None, typer.Option("--report-html")] = None,
|
|
485
|
-
handoff_command: Annotated[
|
|
486
|
-
str | None,
|
|
487
|
-
typer.Option(
|
|
488
|
-
"--handoff-command",
|
|
489
|
-
help=(
|
|
490
|
-
"Optional HandoffKit-compatible command. It receives task JSON on stdin "
|
|
491
|
-
"and emits model-fusion contract records on stdout."
|
|
492
|
-
),
|
|
493
|
-
),
|
|
494
|
-
] = None,
|
|
495
|
-
handoff_timeout_s: Annotated[
|
|
496
|
-
float,
|
|
497
|
-
typer.Option("--handoff-timeout-s", min=1.0),
|
|
498
|
-
] = 300.0,
|
|
499
|
-
mode: FusionMode = "panel",
|
|
500
|
-
config_id: str = "local",
|
|
501
|
-
) -> None:
|
|
502
|
-
fusion_config = load_config(config)
|
|
503
|
-
clients = build_clients(fusion_config)
|
|
504
|
-
engine = FusionEngine(config=fusion_config, clients=clients)
|
|
505
|
-
runner = FusionBenchRunner(
|
|
506
|
-
engine,
|
|
507
|
-
run_root=run_root,
|
|
508
|
-
config_id=config_id,
|
|
509
|
-
mode=mode,
|
|
510
|
-
model_versions={endpoint.id: endpoint.model for endpoint in fusion_config.endpoints},
|
|
511
|
-
handoff_executor=(
|
|
512
|
-
CommandHandoffKitExecutor(
|
|
513
|
-
shlex.split(handoff_command),
|
|
514
|
-
timeout_s=handoff_timeout_s,
|
|
515
|
-
)
|
|
516
|
-
if handoff_command is not None
|
|
517
|
-
else None
|
|
518
|
-
),
|
|
519
|
-
)
|
|
520
|
-
tasks = load_benchmark_tasks(manifest) if manifest else load_benchmark_tasks()
|
|
521
|
-
rows = asyncio.run(runner.run_tasks(tasks))
|
|
522
|
-
write_fusion_bench_jsonl(output, rows)
|
|
523
|
-
response: dict[str, int | str] = {"rows": len(rows), "output": str(output)}
|
|
524
|
-
response.update(
|
|
525
|
-
_write_fusion_bench_reports(
|
|
526
|
-
build_fusion_bench_report(rows),
|
|
527
|
-
report_jsonl=report_jsonl,
|
|
528
|
-
report_markdown=report_markdown,
|
|
529
|
-
report_html=report_html,
|
|
530
|
-
)
|
|
531
|
-
)
|
|
532
|
-
typer.echo(json.dumps(response))
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
@app.command("fusion-bench-report")
|
|
536
|
-
def fusion_bench_report(
|
|
537
|
-
input_path: Annotated[Path, typer.Option("--input", "-i")],
|
|
538
|
-
report_jsonl: Annotated[Path | None, typer.Option("--jsonl")] = None,
|
|
539
|
-
report_markdown: Annotated[Path | None, typer.Option("--markdown", "-m")] = None,
|
|
540
|
-
report_html: Annotated[Path | None, typer.Option("--html")] = None,
|
|
541
|
-
) -> None:
|
|
542
|
-
rows = load_fusion_bench_jsonl(input_path)
|
|
543
|
-
report = build_fusion_bench_report(rows)
|
|
544
|
-
response: dict[str, int | str] = {
|
|
545
|
-
"rows": len(rows),
|
|
546
|
-
"tasks": report.aggregate.total_tasks,
|
|
547
|
-
"skipped": report.aggregate.skipped_tasks,
|
|
548
|
-
"failed": report.aggregate.failed_tasks,
|
|
549
|
-
}
|
|
550
|
-
response.update(
|
|
551
|
-
_write_fusion_bench_reports(
|
|
552
|
-
report,
|
|
553
|
-
report_jsonl=report_jsonl,
|
|
554
|
-
report_markdown=report_markdown,
|
|
555
|
-
report_html=report_html,
|
|
556
|
-
)
|
|
557
|
-
)
|
|
558
|
-
typer.echo(json.dumps(response))
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
@app.command("public-bench")
|
|
562
|
-
def public_bench(
|
|
563
|
-
suite: Annotated[str, typer.Option("--suite", help="public benchmark suite to run")],
|
|
564
|
-
panel: Annotated[
|
|
565
|
-
str,
|
|
566
|
-
typer.Option("--panel", help="benchmark panel id (e.g. decorrelated-peers)"),
|
|
567
|
-
] = "decorrelated-peers",
|
|
568
|
-
gateway_base_url: Annotated[
|
|
569
|
-
str, typer.Option("--gateway-base-url")
|
|
570
|
-
] = "http://127.0.0.1:8080",
|
|
571
|
-
gateway_model: Annotated[
|
|
572
|
-
str | None,
|
|
573
|
-
typer.Option("--gateway-model", help="gateway model alias (defaults per suite)"),
|
|
574
|
-
] = None,
|
|
575
|
-
runner_command: Annotated[
|
|
576
|
-
str | None,
|
|
577
|
-
typer.Option(
|
|
578
|
-
"--runner-command",
|
|
579
|
-
help=(
|
|
580
|
-
"External runner adapter. Receives the request JSON on stdin and emits a "
|
|
581
|
-
"normalized run envelope on stdout. Omit to produce an 'unavailable' report."
|
|
582
|
-
),
|
|
583
|
-
),
|
|
584
|
-
] = None,
|
|
585
|
-
subset: Annotated[
|
|
586
|
-
int | None,
|
|
587
|
-
typer.Option("--subset", min=1, help="run only the first N tasks (subset-first)"),
|
|
588
|
-
] = None,
|
|
589
|
-
runner_timeout_s: Annotated[float, typer.Option("--runner-timeout-s", min=1.0)] = 1800.0,
|
|
590
|
-
output: Annotated[Path | None, typer.Option("--output", "-o")] = None,
|
|
591
|
-
report_markdown: Annotated[Path | None, typer.Option("--report", "-r")] = None,
|
|
592
|
-
ledger: Annotated[
|
|
593
|
-
Path | None,
|
|
594
|
-
typer.Option("--ledger", help="append the run to a history ledger and report drift"),
|
|
595
|
-
] = None,
|
|
596
|
-
) -> None:
|
|
597
|
-
"""Run a public coding benchmark against the gateway and compare to leaderboards."""
|
|
598
|
-
|
|
599
|
-
resolved_suite = _resolve_public_suite(suite)
|
|
600
|
-
info = PUBLIC_BENCHMARK_INFO[resolved_suite]
|
|
601
|
-
benchmark_panel = get_benchmark_panel(panel)
|
|
602
|
-
request = ExternalBenchmarkRequest(
|
|
603
|
-
suite=resolved_suite,
|
|
604
|
-
mount_mode=info.mount_mode,
|
|
605
|
-
gateway_base_url=gateway_base_url,
|
|
606
|
-
gateway_model=gateway_model or info.default_gateway_model,
|
|
607
|
-
panel_id=benchmark_panel.panel_id,
|
|
608
|
-
subset=subset,
|
|
609
|
-
)
|
|
610
|
-
executor = (
|
|
611
|
-
CommandExternalBenchmarkExecutor(
|
|
612
|
-
shlex.split(runner_command),
|
|
613
|
-
timeout_s=runner_timeout_s,
|
|
614
|
-
)
|
|
615
|
-
if runner_command is not None
|
|
616
|
-
else None
|
|
617
|
-
)
|
|
618
|
-
run = asyncio.run(run_public_benchmark(request, executor))
|
|
619
|
-
comparison = build_benchmark_comparison(run, benchmark_panel)
|
|
620
|
-
response: dict[str, int | str | float | None] = {
|
|
621
|
-
"suite": run.suite,
|
|
622
|
-
"panel": run.panel_id,
|
|
623
|
-
"availability": run.availability,
|
|
624
|
-
"fusion_score": comparison.fusion_score,
|
|
625
|
-
"fusion_ci": _fmt_ci(comparison.fusion_ci_low, comparison.fusion_ci_high),
|
|
626
|
-
"measured_oracle": comparison.measured_oracle,
|
|
627
|
-
"measured_regret": comparison.measured_regret,
|
|
628
|
-
"oracle_headroom": comparison.oracle_headroom,
|
|
629
|
-
"lopsided_panel": int(comparison.lopsided),
|
|
630
|
-
"infra_error_tasks": run.infra_error_tasks,
|
|
631
|
-
"excluded_tasks": run.excluded_tasks,
|
|
632
|
-
}
|
|
633
|
-
if output is not None:
|
|
634
|
-
write_external_runs_jsonl(output, [run])
|
|
635
|
-
response["output"] = str(output)
|
|
636
|
-
if report_markdown is not None:
|
|
637
|
-
write_benchmark_comparison_markdown(report_markdown, comparison)
|
|
638
|
-
response["report"] = str(report_markdown)
|
|
639
|
-
if ledger is not None:
|
|
640
|
-
record = BenchRunRecord(
|
|
641
|
-
suite=run.suite,
|
|
642
|
-
panel_id=run.panel_id,
|
|
643
|
-
resolved_tasks=run.resolved_tasks,
|
|
644
|
-
score=comparison.fusion_score,
|
|
645
|
-
ci_low=comparison.fusion_ci_low,
|
|
646
|
-
ci_high=comparison.fusion_ci_high,
|
|
647
|
-
cache_signature=_optional_str(run.raw_metadata.get("cache_signature")),
|
|
648
|
-
repo_sha=_optional_str(run.provenance.get("repo_sha")),
|
|
649
|
-
)
|
|
650
|
-
drift = drift_vs_previous(ledger, record)
|
|
651
|
-
append_run(ledger, record)
|
|
652
|
-
response["ledger"] = str(ledger)
|
|
653
|
-
if drift is not None:
|
|
654
|
-
response["drift_delta"] = drift.delta
|
|
655
|
-
response["regressed"] = int(drift.regressed)
|
|
656
|
-
typer.echo(json.dumps(response))
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
def _fmt_ci(low: float | None, high: float | None) -> str | None:
|
|
660
|
-
if low is None or high is None:
|
|
661
|
-
return None
|
|
662
|
-
return f"[{low:.4f}, {high:.4f}]"
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
def _optional_str(value: object) -> str | None:
|
|
666
|
-
return value if isinstance(value, str) and value else None
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
@app.command("public-bench-baselines")
|
|
670
|
-
def public_bench_baselines(
|
|
671
|
-
suite: Annotated[
|
|
672
|
-
str | None,
|
|
673
|
-
typer.Option("--suite", help="limit to one suite; omit for all suites"),
|
|
674
|
-
] = None,
|
|
675
|
-
) -> None:
|
|
676
|
-
"""Print the published leaderboard baselines used for comparison."""
|
|
677
|
-
|
|
678
|
-
suites = (PUBLIC_BENCHMARK_SUITES if suite is None else (_resolve_public_suite(suite),))
|
|
679
|
-
payload = {
|
|
680
|
-
target: [baseline.model_dump(mode="json") for baseline in baselines_for(target)]
|
|
681
|
-
for target in suites
|
|
682
|
-
}
|
|
683
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
_ROLE_PROMPT_FILE = {
|
|
687
|
-
"judge_system": "judge.md",
|
|
688
|
-
"synthesizer_system": "synthesizer.md",
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
@app.command("tune-prompts")
|
|
693
|
-
def tune_prompts(
|
|
694
|
-
config: Annotated[Path, typer.Option("--config", "-c", help="panel FusionConfig YAML")],
|
|
695
|
-
bank: Annotated[
|
|
696
|
-
Path, typer.Option("--bank", help="candidate bank path (built if missing)")
|
|
697
|
-
] = Path(".fusionkit/tuning/bank.json"),
|
|
698
|
-
role: Annotated[str, typer.Option("--role", help="prompt role to tune")] = "synthesizer_system",
|
|
699
|
-
subset: Annotated[int, typer.Option("--subset", min=1)] = 40,
|
|
700
|
-
bank_max_tests: Annotated[
|
|
701
|
-
int, typer.Option("--bank-max-tests", help="cap tests/task when building the bank (0=all)")
|
|
702
|
-
] = 0,
|
|
703
|
-
optimizer_model: Annotated[
|
|
704
|
-
str | None, typer.Option("--optimizer-model", help="endpoint id (default: judge)")
|
|
705
|
-
] = None,
|
|
706
|
-
max_iterations: Annotated[int, typer.Option("--max-iterations", min=1)] = 8,
|
|
707
|
-
patience: Annotated[int, typer.Option("--patience", min=1)] = 3,
|
|
708
|
-
val_fraction: Annotated[float, typer.Option("--val-fraction", min=0.1, max=0.9)] = 0.4,
|
|
709
|
-
seed: Annotated[int, typer.Option("--seed")] = 0,
|
|
710
|
-
test_timeout_s: Annotated[float, typer.Option("--test-timeout-s", min=1.0)] = 8.0,
|
|
711
|
-
concurrency: Annotated[int, typer.Option("--concurrency", min=1)] = 4,
|
|
712
|
-
prompts_out: Annotated[Path, typer.Option("--prompts-out")] = Path(".fusionkit/prompts"),
|
|
713
|
-
cache_dir: Annotated[Path, typer.Option("--cache-dir")] = Path(".fusionkit/tuning/cache"),
|
|
714
|
-
report: Annotated[Path | None, typer.Option("--report", "-r")] = None,
|
|
715
|
-
ledger: Annotated[Path | None, typer.Option("--ledger")] = None,
|
|
716
|
-
) -> None:
|
|
717
|
-
"""Automated LLM-driven tuning of judge/synth prompts over a frozen bank."""
|
|
718
|
-
|
|
719
|
-
if role not in _ROLE_PROMPT_FILE:
|
|
720
|
-
raise typer.BadParameter(f"role must be one of {sorted(_ROLE_PROMPT_FILE)}")
|
|
721
|
-
resolved_role = cast(TunableRole, role)
|
|
722
|
-
fusion_config = load_config(config)
|
|
723
|
-
clients = build_clients(fusion_config)
|
|
724
|
-
engine = FusionEngine(config=fusion_config, clients=clients)
|
|
725
|
-
sandbox = build_sandbox(SandboxConfig(backend=os.environ.get("BENCH_SANDBOX", "local")))
|
|
726
|
-
|
|
727
|
-
if bank.exists():
|
|
728
|
-
candidate_bank = load_bank(bank)
|
|
729
|
-
else:
|
|
730
|
-
problems = load_problems(
|
|
731
|
-
subset,
|
|
732
|
-
version=os.environ.get("LCB_VERSION", "release_v6"),
|
|
733
|
-
min_date=os.environ.get("LCB_MIN_DATE", "2025-01-01"),
|
|
734
|
-
manifest=load_manifest(os.environ.get("LCB_MANIFEST")),
|
|
735
|
-
)
|
|
736
|
-
prepared = [
|
|
737
|
-
PreparedTask(**task) for task in prepare_tasks(problems, max_tests=bank_max_tests)
|
|
738
|
-
]
|
|
739
|
-
signature = bank_signature(engine, prompt_suffix=LCB_PROMPT_SUFFIX)
|
|
740
|
-
candidate_bank = asyncio.run(
|
|
741
|
-
build_candidate_bank(
|
|
742
|
-
engine,
|
|
743
|
-
sandbox,
|
|
744
|
-
prepared,
|
|
745
|
-
signature=signature,
|
|
746
|
-
test_timeout_s=test_timeout_s,
|
|
747
|
-
concurrency=concurrency,
|
|
748
|
-
)
|
|
749
|
-
)
|
|
750
|
-
save_bank(bank, candidate_bank)
|
|
751
|
-
|
|
752
|
-
decision = select_decision_tasks(candidate_bank)
|
|
753
|
-
if len(decision) < 2:
|
|
754
|
-
typer.echo(
|
|
755
|
-
json.dumps(
|
|
756
|
-
{
|
|
757
|
-
"error": "not enough decision tasks to tune",
|
|
758
|
-
"decision_tasks": len(decision),
|
|
759
|
-
"total_tasks": len(candidate_bank.tasks),
|
|
760
|
-
}
|
|
761
|
-
)
|
|
762
|
-
)
|
|
763
|
-
raise typer.Exit(code=1)
|
|
764
|
-
split = split_dev_val(decision, val_fraction=val_fraction, seed=seed)
|
|
765
|
-
by_id = {task.task_id: task for task in candidate_bank.tasks}
|
|
766
|
-
dev_tasks = [by_id[task_id] for task_id in split.dev]
|
|
767
|
-
val_tasks = [by_id[task_id] for task_id in split.val]
|
|
768
|
-
|
|
769
|
-
runtime = TunerRuntime(
|
|
770
|
-
clients=clients,
|
|
771
|
-
judge_id=fusion_config.resolved_judge_model,
|
|
772
|
-
synth_id=fusion_config.resolved_synthesizer_model,
|
|
773
|
-
bank_signature=candidate_bank.signature,
|
|
774
|
-
sandbox=sandbox,
|
|
775
|
-
cache_dir=cache_dir,
|
|
776
|
-
judge_sampling=fusion_config.sampling.model_copy(update={"temperature": 0.0}),
|
|
777
|
-
synth_sampling=fusion_config.sampling,
|
|
778
|
-
test_timeout_s=test_timeout_s,
|
|
779
|
-
concurrency=concurrency,
|
|
780
|
-
)
|
|
781
|
-
proposer = LLMProposer(
|
|
782
|
-
clients[optimizer_model or fusion_config.resolved_judge_model],
|
|
783
|
-
fusion_config.sampling,
|
|
784
|
-
)
|
|
785
|
-
result = asyncio.run(
|
|
786
|
-
optimize(
|
|
787
|
-
runtime,
|
|
788
|
-
dev_tasks=dev_tasks,
|
|
789
|
-
val_tasks=val_tasks,
|
|
790
|
-
proposer=proposer,
|
|
791
|
-
role=resolved_role,
|
|
792
|
-
max_iterations=max_iterations,
|
|
793
|
-
patience=patience,
|
|
794
|
-
)
|
|
795
|
-
)
|
|
796
|
-
|
|
797
|
-
tuned_prompt = result.best_variant.role_text(resolved_role)
|
|
798
|
-
response: dict[str, object] = {
|
|
799
|
-
"role": result.role,
|
|
800
|
-
"decision_tasks": len(decision),
|
|
801
|
-
"dev": len(dev_tasks),
|
|
802
|
-
"val": len(val_tasks),
|
|
803
|
-
"baseline_dev": result.baseline_dev.score,
|
|
804
|
-
"best_dev": result.best_dev.score,
|
|
805
|
-
"baseline_val": result.baseline_val.score,
|
|
806
|
-
"best_val": result.best_val.score,
|
|
807
|
-
"val_ci": _fmt_ci(result.best_val.ci_low, result.best_val.ci_high),
|
|
808
|
-
"accepted_trials": sum(1 for trial in result.trials if trial.accepted),
|
|
809
|
-
"trials": len(result.trials),
|
|
810
|
-
"improved": result.best_val.score > result.baseline_val.score,
|
|
811
|
-
}
|
|
812
|
-
if tuned_prompt is not None and result.best_val.score > result.baseline_val.score:
|
|
813
|
-
prompts_out.mkdir(parents=True, exist_ok=True)
|
|
814
|
-
out_file = prompts_out / _ROLE_PROMPT_FILE[role]
|
|
815
|
-
out_file.write_text(tuned_prompt + "\n", encoding="utf-8")
|
|
816
|
-
response["prompts_out"] = str(out_file)
|
|
817
|
-
if report is not None:
|
|
818
|
-
report.parent.mkdir(parents=True, exist_ok=True)
|
|
819
|
-
report.write_text(_format_tuning_report(result), encoding="utf-8")
|
|
820
|
-
response["report"] = str(report)
|
|
821
|
-
if ledger is not None:
|
|
822
|
-
append_run(
|
|
823
|
-
ledger,
|
|
824
|
-
BenchRunRecord(
|
|
825
|
-
suite="prompt-tuning",
|
|
826
|
-
panel_id=result.role,
|
|
827
|
-
resolved_tasks=len(val_tasks),
|
|
828
|
-
score=result.best_val.score,
|
|
829
|
-
ci_low=result.best_val.ci_low,
|
|
830
|
-
ci_high=result.best_val.ci_high,
|
|
831
|
-
),
|
|
832
|
-
)
|
|
833
|
-
response["ledger"] = str(ledger)
|
|
834
|
-
typer.echo(json.dumps(response))
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
def _format_tuning_report(result: TuningResult) -> str:
|
|
838
|
-
lines = [
|
|
839
|
-
"# Prompt Tuning Report",
|
|
840
|
-
"",
|
|
841
|
-
f"- Role: {result.role}",
|
|
842
|
-
f"- Baseline dev: {result.baseline_dev.score:.4f} -> best dev: {result.best_dev.score:.4f}",
|
|
843
|
-
f"- Baseline val: {result.baseline_val.score:.4f} -> best val: {result.best_val.score:.4f} "
|
|
844
|
-
f"(95% CI [{result.best_val.ci_low:.4f}, {result.best_val.ci_high:.4f}])",
|
|
845
|
-
f"- Promoted: {result.best_val.score > result.baseline_val.score}",
|
|
846
|
-
"",
|
|
847
|
-
"## Trials (dev)",
|
|
848
|
-
"",
|
|
849
|
-
"| Iter | dev score | wins | losses | accepted |",
|
|
850
|
-
"| ---: | ---: | ---: | ---: | :--: |",
|
|
851
|
-
]
|
|
852
|
-
for trial in result.trials:
|
|
853
|
-
lines.append(
|
|
854
|
-
f"| {trial.iteration} | {trial.dev_score:.4f} | {trial.wins} | {trial.losses} | "
|
|
855
|
-
f"{'yes' if trial.accepted else 'no'} |"
|
|
856
|
-
)
|
|
857
|
-
lines.append("")
|
|
858
|
-
return "\n".join(lines)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
def _resolve_public_suite(suite: str) -> PublicBenchmarkSuite:
|
|
862
|
-
if suite not in PUBLIC_BENCHMARK_SUITES:
|
|
863
|
-
known = ", ".join(PUBLIC_BENCHMARK_SUITES)
|
|
864
|
-
raise typer.BadParameter(f"unknown suite {suite!r}; choose one of: {known}")
|
|
865
|
-
return cast(PublicBenchmarkSuite, suite)
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
def _write_fusion_bench_reports(
|
|
869
|
-
report: FusionBenchReport,
|
|
870
|
-
*,
|
|
871
|
-
report_jsonl: Path | None,
|
|
872
|
-
report_markdown: Path | None,
|
|
873
|
-
report_html: Path | None,
|
|
874
|
-
) -> dict[str, str]:
|
|
875
|
-
outputs = {}
|
|
876
|
-
if report_jsonl is not None:
|
|
877
|
-
write_fusion_bench_report_jsonl(report_jsonl, report)
|
|
878
|
-
outputs["report_jsonl"] = str(report_jsonl)
|
|
879
|
-
if report_markdown is not None:
|
|
880
|
-
write_fusion_bench_markdown_report(report_markdown, report)
|
|
881
|
-
outputs["report_markdown"] = str(report_markdown)
|
|
882
|
-
if report_html is not None:
|
|
883
|
-
write_fusion_bench_html_report(report_html, report)
|
|
884
|
-
outputs["report_html"] = str(report_html)
|
|
885
|
-
return outputs
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"""Helpers for `fusionkit init` / `fusionkit auth`: detection, config discovery,
|
|
2
|
-
and config read/write.
|
|
3
|
-
|
|
4
|
-
FusionKit owns the config file it generates here, so writes re-emit the file from
|
|
5
|
-
the validated model (hand-added YAML comments are not preserved across rewrites).
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
import tomllib
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
import yaml
|
|
14
|
-
from fusionkit_core.config import (
|
|
15
|
-
EndpointAuth,
|
|
16
|
-
FusionConfig,
|
|
17
|
-
ModelEndpoint,
|
|
18
|
-
ProviderKind,
|
|
19
|
-
SubscriptionAuthMode,
|
|
20
|
-
)
|
|
21
|
-
from fusionkit_server.openai_endpoint import PROVIDER_DEFAULT_BASE_URL
|
|
22
|
-
|
|
23
|
-
CONFIG_ENV_VAR = "FUSIONKIT_CONFIG"
|
|
24
|
-
PROJECT_CONFIG = Path("fusionkit.yaml")
|
|
25
|
-
PROJECT_DOTDIR_CONFIG = Path(".fusionkit/models.yaml")
|
|
26
|
-
|
|
27
|
-
# Live-verified defaults; the wizard lets the user override them.
|
|
28
|
-
DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-5"
|
|
29
|
-
DEFAULT_CODEX_MODEL = "gpt-5.5"
|
|
30
|
-
|
|
31
|
-
# API-key providers the wizard knows how to scaffold.
|
|
32
|
-
API_KEY_ENVS: dict[ProviderKind, str] = {
|
|
33
|
-
"openai": "OPENAI_API_KEY",
|
|
34
|
-
"anthropic": "ANTHROPIC_API_KEY",
|
|
35
|
-
"google": "GEMINI_API_KEY",
|
|
36
|
-
}
|
|
37
|
-
DEFAULT_API_MODELS: dict[ProviderKind, str] = {
|
|
38
|
-
"openai": "gpt-5.5",
|
|
39
|
-
"anthropic": "claude-sonnet-4-5",
|
|
40
|
-
"google": "gemini-2.5-flash",
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
CONFIG_HEADER = (
|
|
44
|
-
"# Generated by `fusionkit init`. Edit freely, or manage with `fusionkit auth`.\n"
|
|
45
|
-
"# Subscription endpoints (auth.mode: claude-code / codex) reuse your local\n"
|
|
46
|
-
"# Claude Code / Codex CLI logins read-only; run `claude` or `codex login` to refresh.\n"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def global_config_path() -> Path:
|
|
51
|
-
return Path(os.path.expanduser("~/.config/fusionkit/models.yaml"))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def resolve_config_path(explicit: Path | None) -> Path | None:
|
|
55
|
-
"""Find the config to use: explicit -> $FUSIONKIT_CONFIG -> project -> global."""
|
|
56
|
-
if explicit is not None:
|
|
57
|
-
return explicit
|
|
58
|
-
env_value = os.environ.get(CONFIG_ENV_VAR)
|
|
59
|
-
if env_value:
|
|
60
|
-
return Path(env_value)
|
|
61
|
-
for candidate in (PROJECT_CONFIG, PROJECT_DOTDIR_CONFIG, global_config_path()):
|
|
62
|
-
if candidate.exists():
|
|
63
|
-
return candidate
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def default_write_path(global_: bool) -> Path:
|
|
68
|
-
return global_config_path() if global_ else PROJECT_CONFIG
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def write_config(config: FusionConfig, path: Path) -> None:
|
|
72
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
data = config.model_dump(mode="json", exclude_defaults=True)
|
|
74
|
-
body = yaml.safe_dump(data, sort_keys=False)
|
|
75
|
-
path.write_text(CONFIG_HEADER + body)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def detect_api_keys() -> dict[ProviderKind, str]:
|
|
79
|
-
"""Return {provider: env_var} for API-key env vars that are currently set."""
|
|
80
|
-
return {
|
|
81
|
-
provider: env_var
|
|
82
|
-
for provider, env_var in API_KEY_ENVS.items()
|
|
83
|
-
if os.environ.get(env_var)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def detect_codex_model() -> str | None:
|
|
88
|
-
"""Best-effort read of the model the Codex CLI is pinned to."""
|
|
89
|
-
path = Path(os.path.expanduser("~/.codex/config.toml"))
|
|
90
|
-
if not path.exists():
|
|
91
|
-
return None
|
|
92
|
-
try:
|
|
93
|
-
data = tomllib.loads(path.read_text())
|
|
94
|
-
except (OSError, tomllib.TOMLDecodeError):
|
|
95
|
-
return None
|
|
96
|
-
model = data.get("model")
|
|
97
|
-
return model if isinstance(model, str) and model else None
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def subscription_endpoint(
|
|
101
|
-
mode: SubscriptionAuthMode,
|
|
102
|
-
model: str | None = None,
|
|
103
|
-
endpoint_id: str | None = None,
|
|
104
|
-
) -> ModelEndpoint:
|
|
105
|
-
"""Build a subscription-backed endpoint (base_url omitted; client defaults)."""
|
|
106
|
-
if mode == "claude-code":
|
|
107
|
-
return ModelEndpoint(
|
|
108
|
-
id=endpoint_id or "claude-code",
|
|
109
|
-
provider="anthropic",
|
|
110
|
-
model=model or DEFAULT_CLAUDE_MODEL,
|
|
111
|
-
auth=EndpointAuth(mode="claude-code"),
|
|
112
|
-
)
|
|
113
|
-
if mode == "codex":
|
|
114
|
-
return ModelEndpoint(
|
|
115
|
-
id=endpoint_id or "codex",
|
|
116
|
-
provider="codex",
|
|
117
|
-
model=model or detect_codex_model() or DEFAULT_CODEX_MODEL,
|
|
118
|
-
auth=EndpointAuth(mode="codex"),
|
|
119
|
-
)
|
|
120
|
-
raise ValueError(f"{mode!r} is not a subscription mode")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def api_key_endpoint(
|
|
124
|
-
provider: ProviderKind,
|
|
125
|
-
model: str | None = None,
|
|
126
|
-
endpoint_id: str | None = None,
|
|
127
|
-
) -> ModelEndpoint:
|
|
128
|
-
return ModelEndpoint(
|
|
129
|
-
id=endpoint_id or f"{provider}-api",
|
|
130
|
-
provider=provider,
|
|
131
|
-
model=model or DEFAULT_API_MODELS.get(provider, "model"),
|
|
132
|
-
base_url=PROVIDER_DEFAULT_BASE_URL.get(provider, ""),
|
|
133
|
-
api_key_env=API_KEY_ENVS.get(provider),
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
__all__ = [
|
|
138
|
-
"API_KEY_ENVS",
|
|
139
|
-
"CONFIG_ENV_VAR",
|
|
140
|
-
"DEFAULT_API_MODELS",
|
|
141
|
-
"DEFAULT_CLAUDE_MODEL",
|
|
142
|
-
"DEFAULT_CODEX_MODEL",
|
|
143
|
-
"api_key_endpoint",
|
|
144
|
-
"default_write_path",
|
|
145
|
-
"detect_api_keys",
|
|
146
|
-
"detect_codex_model",
|
|
147
|
-
"global_config_path",
|
|
148
|
-
"resolve_config_path",
|
|
149
|
-
"subscription_endpoint",
|
|
150
|
-
"write_config",
|
|
151
|
-
]
|
|
File without changes
|