fix: harden deepseek official remote43 import closure

This commit is contained in:
phamnazage-jpg
2026-05-27 07:56:24 +08:00
parent bcfc08568c
commit c1172d7714
12 changed files with 172 additions and 29 deletions

View File

@@ -128,6 +128,9 @@ func isAdvisoryAccountProbeFailure(probe sub2api.ProbeResult) bool {
if strings.Contains(message, "api returned 403: forbidden") {
return true
}
if message == "api returned 404:" {
return true
}
if !strings.Contains(message, "responses api") {
return false
@@ -226,6 +229,11 @@ func (s *ImportService) Import(ctx context.Context, req ImportRequest) (report I
return report, fmt.Errorf("batch create accounts: %w", err)
}
rollback.AddAccounts(accounts)
if shouldPreemptivelyDisableOpenAIResponses(req.Provider) {
if err := access.RepairOpenAIResponsesCapability(ctx, s.host, accountRefIDs(accounts)); err != nil {
return failOrDegrade(report, req.Mode, fmt.Errorf("preemptively repair openai responses capability: %w", err))
}
}
for _, account := range accounts {
probe, err := s.host.TestAccount(ctx, account.ID, req.Provider.SmokeTestModel)
if err != nil {
@@ -256,12 +264,6 @@ func (s *ImportService) Import(ctx context.Context, req ImportRequest) (report I
report.AccessStatus = AccessStatusBroken
return report, fmt.Errorf("strict import failed: %d account(s) did not pass smoke validation", failedAccounts)
}
if shouldPreemptivelyDisableOpenAIResponses(req.Provider) {
if err := access.RepairOpenAIResponsesCapability(ctx, s.host, importedAccountIDs(report.Accounts)); err != nil {
return failOrDegrade(report, req.Mode, fmt.Errorf("preemptively repair openai responses capability: %w", err))
}
}
closureService := access.NewService(s.host)
gateway, err := closureService.Close(ctx, access.ClosureRequest{
Mode: req.Access.Mode,
@@ -344,6 +346,16 @@ func importedAccountIDs(accounts []AccountImportResult) []string {
return ids
}
func accountRefIDs(accounts []sub2api.AccountRef) []string {
ids := make([]string, 0, len(accounts))
for _, account := range accounts {
if trimmed := strings.TrimSpace(account.ID); trimmed != "" {
ids = append(ids, trimmed)
}
}
return ids
}
func importedAccountsSuspectResponsesCapabilityMismatch(accounts []AccountImportResult) bool {
for _, account := range accounts {
if !account.SmokeModelSeen {

View File

@@ -291,6 +291,53 @@ func TestImportServiceTreatsForbiddenProbeRaceAsAdvisoryWhenGatewaySucceeds(t *t
}
}
func TestImportServiceTreatsBare404ProbeAsAdvisoryWhenGatewaySucceeds(t *testing.T) {
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
testResults: map[string]sub2api.ProbeResult{
"account_1": {
OK: false,
Status: "failed",
Message: "API returned 404:",
},
},
models: map[string][]sub2api.AccountModel{
"account_1": {{ID: "deepseek-chat"}},
},
gatewayResult: sub2api.GatewayAccessResult{
OK: true,
StatusCode: 200,
HasExpectedModel: true,
Models: []string{"deepseek-chat"},
CompletionOK: true,
CompletionStatus: 200,
CompletionType: "application/json",
},
}
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
Provider: sampleProviderManifest(),
Mode: ImportModePartial,
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
Keys: []string{"key-1"},
})
if err != nil {
t.Fatalf("Import() error = %v", err)
}
if report.BatchStatus != BatchStatusSucceeded {
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
}
if report.ProviderStatus != ProviderStatusActive {
t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive)
}
if report.AccessStatus != AccessStatusSelfServiceReady {
t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSelfServiceReady)
}
if got := report.Accounts[0].ValidationStatus(); got != AccountStatusWarning {
t.Fatalf("ValidationStatus = %q, want %q", got, AccountStatusWarning)
}
}
func TestImportServiceRetriesTransientGatewayCompletionFailure(t *testing.T) {
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}},
@@ -596,6 +643,52 @@ func sampleChatOnlyProviderManifest() pack.ProviderManifest {
return provider
}
func TestImportServicePreemptiveResponsesRepairHappensBeforeAccountProbe(t *testing.T) {
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "kimi-a7m-01"}},
testResults: map[string]sub2api.ProbeResult{
"account_1": {OK: true, Status: "passed"},
},
models: map[string][]sub2api.AccountModel{
"account_1": {{ID: "kimi-k2.6"}},
},
gatewayResult: sub2api.GatewayAccessResult{
OK: true,
StatusCode: 200,
HasExpectedModel: true,
Models: []string{"kimi-k2.6"},
CompletionOK: true,
CompletionStatus: 200,
},
}
report, err := NewImportService(host).Import(context.Background(), ImportRequest{
Provider: sampleChatOnlyProviderManifest(),
Mode: ImportModePartial,
Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"},
Keys: []string{"key-1"},
})
if err != nil {
t.Fatalf("Import() error = %v", err)
}
if report.BatchStatus != BatchStatusSucceeded {
t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded)
}
wantSequence := []string{"disable_responses", "clear_temp_unschedulable", "testAccount:account_1", "gateway", "completion"}
if !reflect.DeepEqual(host.callSequence, wantSequence) {
t.Fatalf("callSequence = %v, want %v", host.callSequence, wantSequence)
}
if host.disableResponsesCalls != 1 {
t.Fatalf("disable responses calls = %d, want 1", host.disableResponsesCalls)
}
if len(host.disabledResponsesAccountIDs) != 1 || host.disabledResponsesAccountIDs[0] != "account_1" {
t.Fatalf("disabled responses account ids = %v, want [account_1]", host.disabledResponsesAccountIDs)
}
if host.clearTempUnschedulableCalls != 1 {
t.Fatalf("clear temp unschedulable calls = %d, want 1", host.clearTempUnschedulableCalls)
}
}
func TestImportReconcilesExistingChannelConfiguration(t *testing.T) {
host := &fakeHostAdapter{
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
@@ -666,7 +759,7 @@ func TestImportDeletesExistingProviderAccountsBeforeGatewayClosure(t *testing.T)
if !reflect.DeepEqual(host.deletedResources, wantDeleted) {
t.Fatalf("deleted resources = %#v, want %#v", host.deletedResources, wantDeleted)
}
if !reflect.DeepEqual(host.callSequence, []string{"deleteAccount:account_old_2", "deleteAccount:account_old_1", "gateway", "completion"}) {
if !reflect.DeepEqual(host.callSequence, []string{"testAccount:account_new_1", "deleteAccount:account_old_2", "deleteAccount:account_old_1", "gateway", "completion"}) {
t.Fatalf("call sequence = %#v, want stale-account cleanup before gateway probe", host.callSequence)
}
}
@@ -810,6 +903,7 @@ func (f *fakeHostAdapter) DeleteAccount(_ context.Context, accountID string) err
return nil
}
func (f *fakeHostAdapter) TestAccount(_ context.Context, accountID, modelID string) (sub2api.ProbeResult, error) {
f.callSequence = append(f.callSequence, "testAccount:"+accountID)
if f.testedModels == nil {
f.testedModels = map[string]string{}
}

View File

@@ -44,6 +44,11 @@ func Open(ctx context.Context, dsn string) (*DB, error) {
if err != nil {
return nil, fmt.Errorf("open sqlite database: %w", err)
}
// SQLite only tolerates a single writer at a time. Pin the pool to one
// connection so HTTP flows that chain several writes (for example host
// probe refresh + import persistence) do not self-deadlock into SQLITE_BUSY.
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
if err := sqlDB.PingContext(ctx); err != nil {
_ = sqlDB.Close()

View File

@@ -15,6 +15,24 @@ func TestOpenClose(t *testing.T) {
}
}
func TestOpenPinsSingleSQLiteConnection(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "single-conn.db")
store, err := Open(context.Background(), "file:"+filepath.ToSlash(dbPath)+"?_busy_timeout=5000")
if err != nil {
t.Fatalf("Open() error = %v", err)
}
defer func() {
if err := store.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
}()
stats := store.SQLDB().Stats()
if stats.MaxOpenConnections != 1 {
t.Fatalf("MaxOpenConnections = %d, want 1", stats.MaxOpenConnections)
}
}
func TestOpenInvalidDSN(t *testing.T) {
_, err := Open(context.Background(), "file:/nonexistent/dir/test.db?_pragma=foreign_keys(0)")
if err == nil {

View File

@@ -23,6 +23,7 @@
- `kimi-k2-5-official.json`
- `kimi-k2-thinking-official.json`
- `minimax-m2-7-official.json`
- `minimax-53hk.json`
- `step-3-5-flash-official.json`
对应的 `provider_id` 与首选模型如下:
@@ -45,6 +46,8 @@
- `kimi-k2-thinking`
- `minimax-m2-7-official`
- `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed`
- `minimax-53hk`
- `MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed`
- `step-3-5-flash-official`
- `step-3.5-flash`
@@ -81,3 +84,4 @@ go run ./cmd/cli apply-host-overlay \
- `qwen.json`
- `glm.json`
- `minimax.json`
- `minimax-53hk.json`

View File

@@ -1,17 +1,18 @@
303bfaea55379f4a4329bd7f720b9c692981dd486a31176b6b93731f56b18cbc providers/minimax-m2-7-official.json
95f1ffc6f764b221b260fd62d649a2f91d136b8e7cf627ac65600cf92c015fd9 pack.json
8158ef1427d59ed10b9f1f91924b8100f5666eeb50d66799be59e1a3fd9fff02 providers/deepseek-chat-official.json
06373a0d6c0337b2cc5e3b4b9269fdd0399b782e092a77ccbcce6d36f7b3354f providers/deepseek-reasoner-official.json
5d33003fb6fefaf2dbb8445be7c18cc817d266215bc996ae91b30fbed5a98e7b providers/deepseek.json
391c3aa02b669eb591f2a3dda45500d66e6a519f07a05c158c8de6be69313e54 providers/glm-4-7-official.json
fe8c84b1b316e8c2e9eb60411383d04bfc0f0ff0c029db3b7d83aa976a6f6d54 providers/glm-5-1-official.json
2db47989a9715464a34b00f7e322ceb1396f96617ddcfa7dee5bd3e7b262c17d providers/kimi-a7m.json
51c8ab51666002c40f0f91744fbb5039d4a97c8c4bbb0b67c5936a5b5f8bf0bc providers/kimi-k2-5-official.json
65e3a1a5e56889ddb0474a3b55294aceb6920fa72dcf6f2c56d3199462daa4cf providers/kimi-k2-thinking-official.json
2da4777edeaa58db5cf79ecfba6cdac4dd3349bd8ff8d804c2e64ee42de4a377 providers/minimax-53hk.json
303bfaea55379f4a4329bd7f720b9c692981dd486a31176b6b93731f56b18cbc providers/minimax-m2-7-official.json
5dcc402daddacce6dcaceb1501020342f1b1121fbffe9097ede4d5aae072f84e providers/minimax.json
fa486a449407f38de8b180ff301568deccef5177ca0436158b1d5b0e6d9328b2 providers/openai-zhongzhuan.json
437028a75810bd970915ec31db1b415d384052143e404fcaa66e335ef2865ae3 providers/qwen-coder-official.json
4817f3faa54dd7fb20cf25a2ded0e702286d55ad829e686c125b70fc55cd52d6 providers/qwen-official.json
51c8ab51666002c40f0f91744fbb5039d4a97c8c4bbb0b67c5936a5b5f8bf0bc providers/kimi-k2-5-official.json
5d33003fb6fefaf2dbb8445be7c18cc817d266215bc996ae91b30fbed5a98e7b providers/deepseek.json
5dcc402daddacce6dcaceb1501020342f1b1121fbffe9097ede4d5aae072f84e providers/minimax.json
65e3a1a5e56889ddb0474a3b55294aceb6920fa72dcf6f2c56d3199462daa4cf providers/kimi-k2-thinking-official.json
a39de44fa68fcb5ee9c3ef38ed3bd5c30acd23cacd2f618d670de0bf9e7096e3 providers/deepseek-reasoner-official.json
2db47989a9715464a34b00f7e322ceb1396f96617ddcfa7dee5bd3e7b262c17d providers/kimi-a7m.json
eda16afc83e12055d3a41b5e37fd0923d3741b66da5af780bcea53ff34fa130e providers/step-3-5-flash-official.json
584991c1a5a3973bda9701ad15bb1c1b167038baa513cb67985708a65bdf6ca6 overlays/kimi-a7m-sub2api-v0.1.129.md
2b2597694ab03409360bf73de43a5cfcea0e26369c4ab18cc8552ff3278729aa overlays/kimi-a7m-sub2api-v0.1.129.patch
4d0069e7bb014b886d4b21fa9a2144fcf65e835f158eda1bb98e092efebd93f3 pack.json
eda16afc83e12055d3a41b5e37fd0923d3741b66da5af780bcea53ff34fa130e providers/step-3-5-flash-official.json
fa486a449407f38de8b180ff301568deccef5177ca0436158b1d5b0e6d9328b2 providers/openai-zhongzhuan.json
fdf7fa2e1ff4aa4f5dcd3f3ec2f55db11d6197625a467d6d0afa8a554a6ba6e6 providers/deepseek-chat-official.json
fe8c84b1b316e8c2e9eb60411383d04bfc0f0ff0c029db3b7d83aa976a6f6d54 providers/glm-5-1-official.json

View File

@@ -1,6 +1,6 @@
{
"pack_id": "openai-cn-pack",
"version": "1.1.3",
"version": "1.1.4",
"vendor": "YourTeam",
"target_host": "sub2api",
"min_host_version": "0.1.126",

View File

@@ -4,6 +4,7 @@
"base_url": "https://api.deepseek.com/v1",
"platform": "openai",
"account_type": "apikey",
"force_disable_openai_responses_api": true,
"default_models": ["deepseek-chat"],
"smoke_test_model": "deepseek-chat",
"group_template": {

View File

@@ -4,6 +4,7 @@
"base_url": "https://api.deepseek.com/v1",
"platform": "openai",
"account_type": "apikey",
"force_disable_openai_responses_api": true,
"default_models": ["deepseek-reasoner"],
"smoke_test_model": "deepseek-reasoner",
"group_template": {

View File

@@ -9,6 +9,7 @@ key_file="${4:-}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "$ROOT_DIR/scripts/host_access_prep_lib.sh"
ARTIFACT_REDACTION_SCRIPT="$ROOT_DIR/scripts/artifact_redaction.py"
KEY="${KEY:-/home/long/下载/zjsea.pem}"
REMOTE="${REMOTE:-ubuntu@43.155.133.187}"
@@ -326,12 +327,11 @@ PY
remote_pg_query "$sql" > "$output_path"
}
write_json_file "$ART/00-local-key-source.json" "$(python3 - <<'PY' "$key_source" "$provider_id" "$upstream_key"
write_json_file "$ART/00-local-key-source.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$key_source" "$provider_id" "$upstream_key"
import json, sys
source, provider_id, key = sys.argv[1:4]
from pathlib import Path
redaction_script, source, provider_id, key = sys.argv[1:5]
import subprocess
result = subprocess.check_output([sys.executable, 'scripts/artifact_redaction.py', 'redact-key', key], text=True)
result = subprocess.check_output([sys.executable, redaction_script, 'redact-key', key], text=True)
redacted = json.loads(result)
print(json.dumps({
'source': source,
@@ -555,10 +555,10 @@ subscription_cache_key="$(build_subscription_billing_cache_key "$sub_uid" "$subs
prep_sql="$(build_subscription_access_prep_sql "$sub_uid" "$sub_key" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$admin_uid" "$SUBSCRIPTION_NOTES")"
remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt"
write_json_file "$ART/05-subscription-access-prep.summary.json" "$(python3 - <<'PY' "$sub_uid" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$sub_key"
write_json_file "$ART/05-subscription-access-prep.summary.json" "$(python3 - <<'PY' "$ARTIFACT_REDACTION_SCRIPT" "$sub_uid" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$sub_key"
import json, subprocess, sys
sub_uid, group_id, min_balance, subscription_days, sub_key = sys.argv[1:6]
redacted = json.loads(subprocess.check_output([sys.executable, 'scripts/artifact_redaction.py', 'redact-key', sub_key], text=True))
redaction_script, sub_uid, group_id, min_balance, subscription_days, sub_key = sys.argv[1:7]
redacted = json.loads(subprocess.check_output([sys.executable, redaction_script, 'redact-key', sub_key], text=True))
print(json.dumps({
'subscription_user_id_hash': __import__('hashlib').sha256(sub_uid.encode('utf-8')).hexdigest(),
'subscription_group_id': int(group_id),

View File

@@ -70,11 +70,14 @@ render_remote43_crm_env() {
local crm_port="$1"
local sqlite_dsn="$2"
local admin_token="$3"
local sqlite_dsn_q admin_token_q
printf -v sqlite_dsn_q '%q' "$sqlite_dsn"
printf -v admin_token_q '%q' "$admin_token"
cat <<EOF
SUB2API_CRM_LISTEN_ADDR=127.0.0.1:$crm_port
SUB2API_CRM_SQLITE_DSN=$sqlite_dsn
SUB2API_CRM_ADMIN_TOKEN=$admin_token
SUB2API_CRM_SQLITE_DSN=$sqlite_dsn_q
SUB2API_CRM_ADMIN_TOKEN=$admin_token_q
SUB2API_CRM_RECONCILE_WORKER_ENABLED=false
EOF
}

View File

@@ -686,7 +686,11 @@ run_test_remote43_patched_stack_renderers() {
assert_contains "$host_env" "DATABASE_HOST=stack-pg"
assert_contains "$host_env" "REDIS_HOST=stack-redis"
assert_contains "$crm_env" "SUB2API_CRM_LISTEN_ADDR=127.0.0.1:18143"
assert_contains "$crm_env" "SUB2API_CRM_SQLITE_DSN="
assert_contains "$crm_env" "SUB2API_CRM_ADMIN_TOKEN=crm-token"
local sourced_dsn
sourced_dsn="$(bash -lc 'set -a; source /dev/stdin; set +a; printf "%s" "$SUB2API_CRM_SQLITE_DSN"' <<<"$crm_env")"
[[ "$sourced_dsn" == "file:/tmp/sub2api.db?_foreign_keys=on" ]] || fail "crm env dsn did not survive bash source"
assert_contains "$bootstrap" 'rm -f "$DATA_DIR/install.lock" "$DATA_DIR/config.yaml" "$DATA_DIR/.installed"'
assert_contains "$bootstrap" '-v "$HOST_BINARY:/app/sub2api:ro"'
assert_contains "$bootstrap" '-p "127.0.0.1:$HOST_PORT:$HOST_CONTAINER_PORT"'