Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

208 lines
6.3 KiB
Python

"""Rewrite stored model IDs across active config tables.
Run this when a provider renames or deprecates a model ID. The catalog
in ``application/core/models/<provider>.yaml`` is updated to the new ID,
but existing agents and schedules still reference the old one and will
fail on the next call. This script rewrites every active config row
in-place inside a single transaction.
Tables touched (active config — would fail against the provider):
* ``agents.default_model_id`` (Text)
* ``agents.models`` (JSONB array of model-id strings)
* ``schedules.model_id`` (Text)
Tables intentionally NOT touched (history):
* ``conversation_messages.model_id`` — records which model wrote each
assistant turn. Rewriting it would falsify history.
* ``sources.model`` — stores the *embeddings* model name captured at
ingestion, not a chat LLM.
* ``user_custom_models.upstream_model_id`` — user-supplied BYOM config
against a non-catalog endpoint. Out of scope for catalog rewrites.
Usage::
# Dry-run with the built-in Gemini preview -> GA mapping (default).
python scripts/db/migrate_model_ids.py
# Apply the built-in mapping.
python scripts/db/migrate_model_ids.py --apply
# Custom mapping (replaces the built-in; repeat --map per pair).
python scripts/db/migrate_model_ids.py \\
--map gemini-3-flash-preview=gemini-3.5-flash \\
--map gemini-3.1-flash-lite-preview=gemini-3.1-flash-lite \\
--apply
Exit codes:
0 — success (dry-run or apply)
1 — bad arguments
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Dict, Iterable
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from sqlalchemy import text # noqa: E402
from application.storage.db.session import db_session # noqa: E402
# Built-in mapping reflects the 2026-05-25 Google preview -> GA swap.
# Update when a new round of catalog churn happens.
DEFAULT_MAPPING: Dict[str, str] = {
"gemini-3-flash-preview": "gemini-3.5-flash",
"gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite",
}
# JSONB array element rewrite. The ``@>`` containment check in the
# WHERE clause skips rows that don't reference the old ID — without it
# every agent would be touched on every iteration.
_UPDATE_AGENTS_MODELS = text(
"""
UPDATE agents
SET models = (
SELECT jsonb_agg(
CASE WHEN elem = to_jsonb(CAST(:old AS text))
THEN to_jsonb(CAST(:new AS text))
ELSE elem
END
)
FROM jsonb_array_elements(models) AS elem
)
WHERE models IS NOT NULL
AND models @> to_jsonb(ARRAY[CAST(:old AS text)])
"""
)
def _parse_overrides(pairs: Iterable[str]) -> Dict[str, str]:
mapping: Dict[str, str] = {}
for raw in pairs:
if "=" not in raw:
raise SystemExit(f"--map expects OLD=NEW, got {raw!r}")
old, new = (s.strip() for s in raw.split("=", 1))
if not old or not new:
raise SystemExit(f"--map values must be non-empty, got {raw!r}")
mapping[old] = new
return mapping
def _count_pre(conn, mapping: Dict[str, str]) -> Dict[str, int]:
"""Count rows that match the OLD IDs across all target columns."""
out = {
"agents.default_model_id": 0,
"agents.models": 0,
"schedules.model_id": 0,
}
for old in mapping:
out["agents.default_model_id"] += conn.execute(
text("SELECT count(*) FROM agents WHERE default_model_id = :old"),
{"old": old},
).scalar_one()
out["agents.models"] += conn.execute(
text(
"SELECT count(*) FROM agents "
"WHERE models IS NOT NULL "
"AND models @> to_jsonb(ARRAY[CAST(:old AS text)])"
),
{"old": old},
).scalar_one()
out["schedules.model_id"] += conn.execute(
text("SELECT count(*) FROM schedules WHERE model_id = :old"),
{"old": old},
).scalar_one()
return out
def _apply(conn, mapping: Dict[str, str]) -> Dict[str, int]:
"""Execute the rewrites inside the caller's transaction."""
out = {
"agents.default_model_id": 0,
"agents.models": 0,
"schedules.model_id": 0,
}
for old, new in mapping.items():
res = conn.execute(
text(
"UPDATE agents SET default_model_id = :new "
"WHERE default_model_id = :old"
),
{"new": new, "old": old},
)
out["agents.default_model_id"] += res.rowcount or 0
res = conn.execute(_UPDATE_AGENTS_MODELS, {"old": old, "new": new})
out["agents.models"] += res.rowcount or 0
res = conn.execute(
text("UPDATE schedules SET model_id = :new WHERE model_id = :old"),
{"new": new, "old": old},
)
out["schedules.model_id"] += res.rowcount or 0
return out
def main() -> int:
parser = argparse.ArgumentParser(
description="Rewrite stored model IDs across active config tables.",
)
parser.add_argument(
"--map",
action="append",
default=[],
metavar="OLD=NEW",
help=(
"Replace the built-in mapping. Repeat for each pair. "
"If any --map is given, the built-in mapping is replaced, "
"not merged."
),
)
parser.add_argument(
"--apply",
action="store_true",
help="Execute the UPDATEs. Default is dry-run.",
)
args = parser.parse_args()
mapping = _parse_overrides(args.map) if args.map else dict(DEFAULT_MAPPING)
print("Mapping:")
for old, new in mapping.items():
print(f" {old} -> {new}")
print()
with db_session() as conn:
counts = _count_pre(conn, mapping)
print("Rows matching old IDs (pre-update):")
for col, n in counts.items():
print(f" {col:30s} {n}")
print()
if sum(counts.values()) == 0:
print("Nothing to do.")
return 0
if not args.apply:
print("Dry run. Re-run with --apply to commit.")
return 0
updated = _apply(conn, mapping)
print("Rows updated:")
for col, n in updated.items():
print(f" {col:30s} {n}")
print("\nDone.")
return 0
if __name__ == "__main__":
sys.exit(main())