feat: add kimi a7m overlay workflow and remote43 validation
This commit is contained in:
87
cmd/cli/apply_host_overlay.go
Normal file
87
cmd/cli/apply_host_overlay.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/overlay"
|
||||
"sub2api-cn-relay-manager/internal/pack"
|
||||
)
|
||||
|
||||
type applyHostOverlayFunc func(context.Context, applyHostOverlayCLIRequest) (overlay.ApplyResult, error)
|
||||
|
||||
type applyHostOverlayCLIRequest struct {
|
||||
PackDir string
|
||||
ProviderID string
|
||||
HostVersion string
|
||||
SourceDir string
|
||||
OutputDir string
|
||||
TargetHost string
|
||||
OverlayID string
|
||||
}
|
||||
|
||||
func parseApplyHostOverlayCLIArgs(args []string) (applyHostOverlayCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("apply-host-overlay", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req applyHostOverlayCLIRequest
|
||||
fs.StringVar(&req.PackDir, "pack-dir", "", "")
|
||||
fs.StringVar(&req.ProviderID, "provider-id", "", "")
|
||||
fs.StringVar(&req.HostVersion, "host-version", "", "")
|
||||
fs.StringVar(&req.SourceDir, "source-dir", "", "")
|
||||
fs.StringVar(&req.OutputDir, "output-dir", "", "")
|
||||
fs.StringVar(&req.TargetHost, "target-host", "", "")
|
||||
fs.StringVar(&req.OverlayID, "overlay-id", "", "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return applyHostOverlayCLIRequest{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(req.PackDir) == "":
|
||||
return applyHostOverlayCLIRequest{}, fmt.Errorf("--pack-dir is required")
|
||||
case strings.TrimSpace(req.ProviderID) == "":
|
||||
return applyHostOverlayCLIRequest{}, fmt.Errorf("--provider-id is required")
|
||||
case strings.TrimSpace(req.HostVersion) == "":
|
||||
return applyHostOverlayCLIRequest{}, fmt.Errorf("--host-version is required")
|
||||
case strings.TrimSpace(req.SourceDir) == "":
|
||||
return applyHostOverlayCLIRequest{}, fmt.Errorf("--source-dir is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func runApplyHostOverlay(ctx context.Context, req applyHostOverlayCLIRequest) (overlay.ApplyResult, error) {
|
||||
loadedPack, err := pack.LoadDir(req.PackDir)
|
||||
if err != nil {
|
||||
return overlay.ApplyResult{}, err
|
||||
}
|
||||
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
||||
if err != nil {
|
||||
return overlay.ApplyResult{}, err
|
||||
}
|
||||
|
||||
targetHost := strings.TrimSpace(req.TargetHost)
|
||||
if targetHost == "" {
|
||||
targetHost = loadedPack.Manifest.TargetHost
|
||||
}
|
||||
resolvedOverlays, err := pack.ResolveApplicableHostOverlays(providerManifest, targetHost, req.HostVersion)
|
||||
if err != nil {
|
||||
return overlay.ApplyResult{}, err
|
||||
}
|
||||
if len(resolvedOverlays) == 0 {
|
||||
return overlay.ApplyResult{}, fmt.Errorf("no host overlays matched provider %q for host %q version %q", req.ProviderID, targetHost, req.HostVersion)
|
||||
}
|
||||
filteredOverlays, err := overlay.FilterOverlays(resolvedOverlays, req.OverlayID)
|
||||
if err != nil {
|
||||
return overlay.ApplyResult{}, err
|
||||
}
|
||||
|
||||
return overlay.Apply(ctx, overlay.ApplyRequest{
|
||||
PackDir: loadedPack.Dir,
|
||||
SourceDir: req.SourceDir,
|
||||
OutputDir: req.OutputDir,
|
||||
Overlays: filteredOverlays,
|
||||
})
|
||||
}
|
||||
115
cmd/cli/apply_host_overlay_test.go
Normal file
115
cmd/cli/apply_host_overlay_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunApplyHostOverlayAppliesResolvedPackOverlay(t *testing.T) {
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
packDir := createApplyHostOverlayPackFixture(t)
|
||||
result, err := runApplyHostOverlay(context.Background(), applyHostOverlayCLIRequest{
|
||||
PackDir: packDir,
|
||||
ProviderID: "kimi-a7m",
|
||||
HostVersion: "0.1.129",
|
||||
SourceDir: sourceDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runApplyHostOverlay() error = %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error = %v", err)
|
||||
}
|
||||
if string(body) != "patched\n" {
|
||||
t.Fatalf("patched file = %q, want %q", string(body), "patched\n")
|
||||
}
|
||||
if _, err := os.Stat(result.MetadataFilePath); err != nil {
|
||||
t.Fatalf("Stat(metadata) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createApplyHostOverlayPackFixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
packDir := t.TempDir()
|
||||
files := map[string]string{
|
||||
"pack.json": `{
|
||||
"pack_id": "openai-cn-pack",
|
||||
"version": "1.1.3",
|
||||
"vendor": "YourTeam",
|
||||
"target_host": "sub2api",
|
||||
"min_host_version": "0.1.126",
|
||||
"max_host_version": "0.2.x",
|
||||
"providers_dir": "providers",
|
||||
"checksum_file": "checksums.txt"
|
||||
}`,
|
||||
"providers/kimi-a7m.json": `{
|
||||
"provider_id": "kimi-a7m",
|
||||
"display_name": "Kimi A7M OpenAI Compatible",
|
||||
"base_url": "https://kimi.a7m.com.cn/v1",
|
||||
"platform": "openai",
|
||||
"account_type": "apikey",
|
||||
"default_models": ["kimi-k2.6"],
|
||||
"smoke_test_model": "kimi-k2.6",
|
||||
"host_overlays": [
|
||||
{
|
||||
"overlay_id": "sub2api-stock-v0129-kimi-a7m",
|
||||
"display_name": "sub2api stock v0.1.129 Kimi A7M overlay",
|
||||
"target_host": "sub2api",
|
||||
"min_host_version": "0.1.129",
|
||||
"max_host_version": "0.1.129",
|
||||
"apply_mode": "patch",
|
||||
"patch_path": "overlays/kimi-a7m-sub2api-v0.1.129.patch",
|
||||
"notes_path": "overlays/kimi-a7m-sub2api-v0.1.129.md",
|
||||
"reason": "stock host still routes chat traffic into unsupported Responses path"
|
||||
}
|
||||
],
|
||||
"group_template": {"name": "g", "rate_multiplier": 1},
|
||||
"channel_template": {"name": "c", "model_mapping": {"kimi-k2.6": "kimi-k2.6"}},
|
||||
"plan_template": {"name": "p", "price": 1, "validity_days": 30, "validity_unit": "day"},
|
||||
"import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true}
|
||||
}`,
|
||||
"overlays/kimi-a7m-sub2api-v0.1.129.patch": strings.Join([]string{
|
||||
"diff --git a/backend/hello.txt b/backend/hello.txt",
|
||||
"--- a/backend/hello.txt",
|
||||
"+++ b/backend/hello.txt",
|
||||
"@@ -1 +1 @@",
|
||||
"-hello",
|
||||
"+patched",
|
||||
"",
|
||||
}, "\n"),
|
||||
"overlays/kimi-a7m-sub2api-v0.1.129.md": "# overlay\n",
|
||||
}
|
||||
|
||||
checksumLines := make([]string, 0, len(files))
|
||||
for relativePath, body := range files {
|
||||
absolutePath := filepath.Join(packDir, relativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(absolutePath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%q) error = %v", absolutePath, err)
|
||||
}
|
||||
if err := os.WriteFile(absolutePath, []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%q) error = %v", absolutePath, err)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(body))
|
||||
checksumLines = append(checksumLines, hex.EncodeToString(sum[:])+" "+relativePath)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(packDir, "checksums.txt"), []byte(strings.Join(checksumLines, "\n")+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(checksums.txt) error = %v", err)
|
||||
}
|
||||
|
||||
return packDir
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func TestBatchImportCLI(t *testing.T) {
|
||||
"--access-mode", "self_service",
|
||||
"--probe-api-key", "gateway-key",
|
||||
"--confirm-timeout", "15s",
|
||||
}, nil, nil, nil, nil, nil, nil, func(_ context.Context, req batchImportCLIRequest) (batchImportCLIResult, error) {
|
||||
}, nil, nil, nil, nil, nil, nil, nil, func(_ context.Context, req batchImportCLIRequest) (batchImportCLIResult, error) {
|
||||
batchImportCalled = true
|
||||
if req.HostID != "host-1" {
|
||||
t.Fatalf("HostID = %q, want host-1", req.HostID)
|
||||
|
||||
@@ -81,7 +81,7 @@ type rollbackSummary struct {
|
||||
func main() {
|
||||
if err := execute(context.Background(), log.Writer(), os.Args[1:], func(context.Context) (config.StartupConfig, error) {
|
||||
return config.LoadStartupFromEnv()
|
||||
}, runInstallPack, runImportProvider, runPreviewProvider, runRollbackProvider, runReconcileProvider, runBatchImport); err != nil {
|
||||
}, runInstallPack, runImportProvider, runPreviewProvider, runRollbackProvider, runReconcileProvider, runApplyHostOverlay, runBatchImport); err != nil {
|
||||
log.Fatalf("run cli: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ func execute(
|
||||
previewProvider previewProviderFunc,
|
||||
rollbackProvider rollbackProviderFunc,
|
||||
reconcileProvider reconcileProviderFunc,
|
||||
applyHostOverlay applyHostOverlayFunc,
|
||||
batchImport batchImportFunc,
|
||||
) error {
|
||||
if len(args) > 0 && args[0] == "batch-import" {
|
||||
@@ -171,6 +172,22 @@ func execute(
|
||||
_, err = fmt.Fprintf(output, "status=%s\nmissing_count=%d\nextra_count=%d\nprobe_failures=%d\naccess_status=%s\n", result.Status, result.MissingCount, result.ExtraCount, result.ProbeFailureCount, result.AccessStatus)
|
||||
return err
|
||||
}
|
||||
if len(args) > 0 && args[0] == "apply-host-overlay" {
|
||||
req, err := parseApplyHostOverlayCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := applyHostOverlay(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
overlayIDs := make([]string, 0, len(result.AppliedOverlays))
|
||||
for _, hostOverlay := range result.AppliedOverlays {
|
||||
overlayIDs = append(overlayIDs, hostOverlay.OverlayID)
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "output_dir=%s\napplied_overlays=%d\noverlay_ids=%s\nmetadata_file=%s\n", result.OutputDir, len(result.AppliedOverlays), strings.Join(overlayIDs, ","), result.MetadataFilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(ctx)
|
||||
if err != nil {
|
||||
@@ -410,10 +427,16 @@ func runPreviewProvider(ctx context.Context, req previewCLIRequest) (provision.P
|
||||
}
|
||||
|
||||
service := provision.NewPreviewService(client)
|
||||
hostVersion, err := client.GetHostVersion(ctx)
|
||||
if err != nil {
|
||||
return provision.PreviewReport{}, err
|
||||
}
|
||||
return service.PreviewImport(ctx, provision.PreviewRequest{
|
||||
Provider: providerManifest,
|
||||
Mode: req.Mode,
|
||||
Keys: req.Keys,
|
||||
TargetHost: loadedPack.Manifest.TargetHost,
|
||||
HostVersion: hostVersion,
|
||||
Provider: providerManifest,
|
||||
Mode: req.Mode,
|
||||
Keys: req.Keys,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/config"
|
||||
"sub2api-cn-relay-manager/internal/overlay"
|
||||
"sub2api-cn-relay-manager/internal/pack"
|
||||
"sub2api-cn-relay-manager/internal/provision"
|
||||
"sub2api-cn-relay-manager/internal/reconcile"
|
||||
"sub2api-cn-relay-manager/internal/store/sqlite"
|
||||
@@ -33,7 +35,7 @@ func TestExecuteWritesConfigSummaryAfterBootstrap(t *testing.T) {
|
||||
SQLiteDSN: "file:test.db?_foreign_keys=on",
|
||||
},
|
||||
}, nil
|
||||
}, nil, nil, nil, nil, nil, nil)
|
||||
}, nil, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() returned error: %v", err)
|
||||
}
|
||||
@@ -61,7 +63,7 @@ func TestExecuteReturnsBootstrapError(t *testing.T) {
|
||||
|
||||
err := execute(context.Background(), &bytes.Buffer{}, nil, func(context.Context) (config.StartupConfig, error) {
|
||||
return config.StartupConfig{}, wantErr
|
||||
}, nil, nil, nil, nil, nil, nil)
|
||||
}, nil, nil, nil, nil, nil, nil, nil)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("execute() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
@@ -75,7 +77,7 @@ func TestExecuteReturnsWriteError(t *testing.T) {
|
||||
Server: config.ServerConfig{ListenAddr: ":9292"},
|
||||
Database: config.DatabaseConfig{SQLiteDSN: "file:test.db"},
|
||||
}, nil
|
||||
}, nil, nil, nil, nil, nil, nil)
|
||||
}, nil, nil, nil, nil, nil, nil, nil)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("execute() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
@@ -99,7 +101,7 @@ func TestExecuteInstallPackWritesSummary(t *testing.T) {
|
||||
HostVersion: "0.1.126",
|
||||
Providers: []sqlite.Provider{{ProviderID: "deepseek"}},
|
||||
}, nil
|
||||
}, nil, nil, nil, nil, nil)
|
||||
}, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() install-pack error = %v", err)
|
||||
}
|
||||
@@ -134,7 +136,7 @@ func TestExecuteImportProviderWritesSummary(t *testing.T) {
|
||||
AccessStatus: provision.AccessStatusSelfServiceReady,
|
||||
Accounts: []provision.AccountImportResult{{}, {}},
|
||||
}, nil
|
||||
}, nil, nil, nil, nil)
|
||||
}, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() import error = %v", err)
|
||||
}
|
||||
@@ -171,7 +173,7 @@ func TestExecutePreviewProviderWritesSummary(t *testing.T) {
|
||||
"plan": {Action: provision.PreviewActionConflict},
|
||||
},
|
||||
}, nil
|
||||
}, nil, nil, nil)
|
||||
}, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() preview error = %v", err)
|
||||
}
|
||||
@@ -199,7 +201,7 @@ func TestExecuteRollbackProviderWritesSummary(t *testing.T) {
|
||||
t.Fatalf("unexpected rollback request: %+v", req)
|
||||
}
|
||||
return rollbackSummary{Accounts: 2, Plans: 1, Channels: 1, Groups: 1}, nil
|
||||
}, nil, nil)
|
||||
}, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() rollback error = %v", err)
|
||||
}
|
||||
@@ -228,7 +230,7 @@ func TestExecuteReconcileProviderWritesSummary(t *testing.T) {
|
||||
t.Fatalf("unexpected reconcile request: %+v", req)
|
||||
}
|
||||
return reconcile.Result{Status: "drifted", MissingCount: 1, ExtraCount: 2, ProbeFailureCount: 1, AccessStatus: provision.AccessStatusBroken}, nil
|
||||
}, nil)
|
||||
}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() reconcile error = %v", err)
|
||||
}
|
||||
@@ -241,6 +243,48 @@ func TestExecuteReconcileProviderWritesSummary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteApplyHostOverlayWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
applyCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"apply-host-overlay",
|
||||
"--pack-dir", "/tmp/pack",
|
||||
"--provider-id", "kimi-a7m",
|
||||
"--host-version", "0.1.129",
|
||||
"--source-dir", "/tmp/sub2api-src",
|
||||
}, nil, nil, nil, nil, nil, nil, func(_ context.Context, req applyHostOverlayCLIRequest) (overlay.ApplyResult, error) {
|
||||
applyCalled = true
|
||||
if req.ProviderID != "kimi-a7m" || req.HostVersion != "0.1.129" {
|
||||
t.Fatalf("unexpected apply-host-overlay request: %+v", req)
|
||||
}
|
||||
return overlay.ApplyResult{
|
||||
OutputDir: "/tmp/sub2api-src-patched-sub2api-stock-v0129-kimi-a7m",
|
||||
AppliedOverlays: []pack.HostOverlay{{
|
||||
OverlayID: "sub2api-stock-v0129-kimi-a7m",
|
||||
}},
|
||||
MetadataFilePath: "/tmp/sub2api-src-patched-sub2api-stock-v0129-kimi-a7m/.sub2api-cn-relay-manager-overlay.json",
|
||||
}, nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() apply-host-overlay error = %v", err)
|
||||
}
|
||||
if !applyCalled {
|
||||
t.Fatal("execute() did not invoke applyHostOverlay")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "applied_overlays=1") || !strings.Contains(got, "overlay_ids=sub2api-stock-v0129-kimi-a7m") {
|
||||
t.Fatalf("execute() apply-host-overlay output = %q, want overlay summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseApplyHostOverlayCLIArgsRequiresSourceDir(t *testing.T) {
|
||||
_, err := parseApplyHostOverlayCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "kimi-a7m", "--host-version", "0.1.129"})
|
||||
if err == nil {
|
||||
t.Fatal("parseApplyHostOverlayCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInstallPackCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parseInstallPackCLIArgs([]string{"--pack-path", "/tmp/openai-pack.zip"})
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user