fix route grouping and codex websocket regressions

This commit is contained in:
cita
2026-03-18 20:38:01 +08:00
parent 332007d4fb
commit f769f43909
38 changed files with 2453 additions and 636 deletions
+4 -1
View File
@@ -8,8 +8,9 @@ CREATE TABLE IF NOT EXISTS `model_availability` (`id` INT AUTO_INCREMENT NOT NUL
CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `owner_type` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `filename` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `purpose` TEXT, `byte_size` INT NOT NULL, `sha256` TEXT NOT NULL, `content_base64` TEXT NOT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `deleted_at` DATETIME);
CREATE TABLE IF NOT EXISTS `proxy_logs` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT, `channel_id` INT, `account_id` INT, `model_requested` TEXT, `model_actual` TEXT, `status` TEXT, `http_status` INT, `latency_ms` INT, `prompt_tokens` INT, `completion_tokens` INT, `total_tokens` INT, `estimated_cost` DOUBLE, `error_message` TEXT, `retry_count` INT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `billing_details` JSON, `downstream_api_key_id` INT);
CREATE TABLE IF NOT EXISTS `proxy_video_tasks` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `upstream_video_id` TEXT NOT NULL, `site_url` TEXT NOT NULL, `token_value` TEXT NOT NULL, `requested_model` TEXT, `actual_model` TEXT, `channel_id` INT, `account_id` INT, `status_snapshot` JSON, `upstream_response_meta` JSON, `last_upstream_status` INT, `last_polled_at` DATETIME, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE IF NOT EXISTS `token_routes` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `model_pattern` TEXT NOT NULL, `model_mapping` JSON, `enabled` BOOLEAN DEFAULT true, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `display_name` TEXT, `display_icon` TEXT, `decision_snapshot` JSON, `decision_refreshed_at` DATETIME, `routing_strategy` VARCHAR(191) DEFAULT 'weighted');
CREATE TABLE IF NOT EXISTS `token_routes` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `model_pattern` TEXT NOT NULL, `model_mapping` JSON, `enabled` BOOLEAN DEFAULT true, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `display_name` TEXT, `display_icon` TEXT, `decision_snapshot` JSON, `decision_refreshed_at` DATETIME, `routing_strategy` VARCHAR(191) DEFAULT 'weighted', `route_mode` VARCHAR(191) DEFAULT 'pattern');
CREATE TABLE IF NOT EXISTS `route_channels` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT NOT NULL, `account_id` INT NOT NULL, `token_id` INT, `source_model` TEXT, `priority` INT DEFAULT 0, `weight` INT DEFAULT 10, `enabled` BOOLEAN DEFAULT true, `manual_override` BOOLEAN DEFAULT false, `success_count` INT DEFAULT 0, `fail_count` INT DEFAULT 0, `total_latency_ms` INT DEFAULT 0, `total_cost` DOUBLE DEFAULT 0, `last_used_at` DATETIME, `last_selected_at` DATETIME, `last_fail_at` DATETIME, `consecutive_fail_count` INT NOT NULL DEFAULT 0, `cooldown_level` INT NOT NULL DEFAULT 0, `cooldown_until` DATETIME, FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE, FOREIGN KEY (`route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE SET NULL);
CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` INT NOT NULL, `source_route_id` INT NOT NULL, FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS `settings` (`key` VARCHAR(191) NOT NULL PRIMARY KEY, `value` TEXT);
CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `model_name` TEXT NOT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS `token_model_availability` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `token_id` INT NOT NULL, `model_name` TEXT NOT NULL, `available` BOOLEAN, `latency_ms` INT, `checked_at` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE CASCADE);
@@ -17,6 +18,7 @@ CREATE UNIQUE INDEX `downstream_api_keys_key_unique` ON `downstream_api_keys` (`
CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191));
CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191));
CREATE UNIQUE INDEX `proxy_video_tasks_public_id_unique` ON `proxy_video_tasks` (`public_id`(191));
CREATE UNIQUE INDEX `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`, `source_route_id`);
CREATE UNIQUE INDEX `site_disabled_models_site_model_unique` ON `site_disabled_models` (`site_id`, `model_name`(191));
CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`(191), `url`(191));
CREATE UNIQUE INDEX `token_model_availability_token_model_unique` ON `token_model_availability` (`token_id`, `model_name`(191));
@@ -52,6 +54,7 @@ CREATE INDEX `route_channels_route_enabled_idx` ON `route_channels` (`route_id`,
CREATE INDEX `route_channels_route_id_idx` ON `route_channels` (`route_id`);
CREATE INDEX `route_channels_route_token_idx` ON `route_channels` (`route_id`, `token_id`);
CREATE INDEX `route_channels_token_id_idx` ON `route_channels` (`token_id`);
CREATE INDEX `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`);
CREATE INDEX `site_disabled_models_site_id_idx` ON `site_disabled_models` (`site_id`);
CREATE INDEX `sites_status_idx` ON `sites` (`status`(191));
CREATE INDEX `token_model_availability_available_idx` ON `token_model_availability` (`available`);
+4 -6
View File
@@ -1,6 +1,4 @@
ALTER TABLE `accounts` ADD COLUMN `oauth_provider` TEXT;
ALTER TABLE `accounts` ADD COLUMN `oauth_account_key` TEXT;
ALTER TABLE `accounts` ADD COLUMN `oauth_project_id` TEXT;
CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`(191), `url`(191));
CREATE INDEX `accounts_oauth_identity_idx` ON `accounts` (`oauth_provider`(191), `oauth_account_key`(191), `oauth_project_id`(191));
CREATE INDEX `accounts_oauth_provider_idx` ON `accounts` (`oauth_provider`(191));
CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` INT NOT NULL, `source_route_id` INT NOT NULL, FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE);
ALTER TABLE `token_routes` ADD COLUMN `route_mode` VARCHAR(191) DEFAULT 'pattern';
CREATE UNIQUE INDEX `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`, `source_route_id`);
CREATE INDEX `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`);
@@ -8,8 +8,9 @@ CREATE TABLE IF NOT EXISTS "model_availability" ("id" INTEGER GENERATED BY DEFAU
CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "deleted_at" TIMESTAMP);
CREATE TABLE IF NOT EXISTS "proxy_logs" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER, "channel_id" INTEGER, "account_id" INTEGER, "model_requested" TEXT, "model_actual" TEXT, "status" TEXT, "http_status" INTEGER, "latency_ms" INTEGER, "prompt_tokens" INTEGER, "completion_tokens" INTEGER, "total_tokens" INTEGER, "estimated_cost" DOUBLE PRECISION, "error_message" TEXT, "retry_count" INTEGER DEFAULT 0, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "billing_details" JSONB, "downstream_api_key_id" INTEGER);
CREATE TABLE IF NOT EXISTS "proxy_video_tasks" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "upstream_video_id" TEXT NOT NULL, "site_url" TEXT NOT NULL, "token_value" TEXT NOT NULL, "requested_model" TEXT, "actual_model" TEXT, "channel_id" INTEGER, "account_id" INTEGER, "status_snapshot" JSONB, "upstream_response_meta" JSONB, "last_upstream_status" INTEGER, "last_polled_at" TIMESTAMP, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE IF NOT EXISTS "token_routes" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "model_pattern" TEXT NOT NULL, "model_mapping" JSONB, "enabled" BOOLEAN DEFAULT true, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "display_name" TEXT, "display_icon" TEXT, "decision_snapshot" JSONB, "decision_refreshed_at" TIMESTAMP, "routing_strategy" TEXT DEFAULT 'weighted');
CREATE TABLE IF NOT EXISTS "token_routes" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "model_pattern" TEXT NOT NULL, "model_mapping" JSONB, "enabled" BOOLEAN DEFAULT true, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "display_name" TEXT, "display_icon" TEXT, "decision_snapshot" JSONB, "decision_refreshed_at" TIMESTAMP, "routing_strategy" TEXT DEFAULT 'weighted', "route_mode" TEXT DEFAULT 'pattern');
CREATE TABLE IF NOT EXISTS "route_channels" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER NOT NULL, "account_id" INTEGER NOT NULL, "token_id" INTEGER, "source_model" TEXT, "priority" INTEGER DEFAULT 0, "weight" INTEGER DEFAULT 10, "enabled" BOOLEAN DEFAULT true, "manual_override" BOOLEAN DEFAULT false, "success_count" INTEGER DEFAULT 0, "fail_count" INTEGER DEFAULT 0, "total_latency_ms" INTEGER DEFAULT 0, "total_cost" DOUBLE PRECISION DEFAULT 0, "last_used_at" TIMESTAMP, "last_selected_at" TIMESTAMP, "last_fail_at" TIMESTAMP, "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0, "cooldown_level" INTEGER NOT NULL DEFAULT 0, "cooldown_until" TIMESTAMP, FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE, FOREIGN KEY ("route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE SET NULL);
CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "settings" ("key" TEXT NOT NULL PRIMARY KEY, "value" TEXT);
CREATE TABLE IF NOT EXISTS "site_disabled_models" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS "token_model_availability" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "token_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "available" BOOLEAN, "latency_ms" INTEGER, "checked_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE CASCADE);
@@ -17,6 +18,7 @@ CREATE UNIQUE INDEX "downstream_api_keys_key_unique" ON "downstream_api_keys" ("
CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name");
CREATE UNIQUE INDEX "proxy_files_public_id_unique" ON "proxy_files" ("public_id");
CREATE UNIQUE INDEX "proxy_video_tasks_public_id_unique" ON "proxy_video_tasks" ("public_id");
CREATE UNIQUE INDEX "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id");
CREATE UNIQUE INDEX "site_disabled_models_site_model_unique" ON "site_disabled_models" ("site_id", "model_name");
CREATE UNIQUE INDEX "sites_platform_url_unique" ON "sites" ("platform", "url");
CREATE UNIQUE INDEX "token_model_availability_token_model_unique" ON "token_model_availability" ("token_id", "model_name");
@@ -52,6 +54,7 @@ CREATE INDEX "route_channels_route_enabled_idx" ON "route_channels" ("route_id",
CREATE INDEX "route_channels_route_id_idx" ON "route_channels" ("route_id");
CREATE INDEX "route_channels_route_token_idx" ON "route_channels" ("route_id", "token_id");
CREATE INDEX "route_channels_token_id_idx" ON "route_channels" ("token_id");
CREATE INDEX "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id");
CREATE INDEX "site_disabled_models_site_id_idx" ON "site_disabled_models" ("site_id");
CREATE INDEX "sites_status_idx" ON "sites" ("status");
CREATE INDEX "token_model_availability_available_idx" ON "token_model_availability" ("available");
+4 -6
View File
@@ -1,6 +1,4 @@
ALTER TABLE "accounts" ADD COLUMN "oauth_provider" TEXT;
ALTER TABLE "accounts" ADD COLUMN "oauth_account_key" TEXT;
ALTER TABLE "accounts" ADD COLUMN "oauth_project_id" TEXT;
CREATE UNIQUE INDEX "sites_platform_url_unique" ON "sites" ("platform", "url");
CREATE INDEX "accounts_oauth_identity_idx" ON "accounts" ("oauth_provider", "oauth_account_key", "oauth_project_id");
CREATE INDEX "accounts_oauth_provider_idx" ON "accounts" ("oauth_provider");
CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE);
ALTER TABLE "token_routes" ADD COLUMN "route_mode" TEXT DEFAULT 'pattern';
CREATE UNIQUE INDEX "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id");
CREATE INDEX "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id");
@@ -868,6 +868,28 @@
}
}
},
"route_group_sources": {
"columns": {
"id": {
"logicalType": "integer",
"notNull": true,
"defaultValue": null,
"primaryKey": true
},
"group_route_id": {
"logicalType": "integer",
"notNull": true,
"defaultValue": null,
"primaryKey": false
},
"source_route_id": {
"logicalType": "integer",
"notNull": true,
"defaultValue": null,
"primaryKey": false
}
}
},
"settings": {
"columns": {
"key": {
@@ -1113,6 +1135,12 @@
"notNull": false,
"defaultValue": "'weighted'",
"primaryKey": false
},
"route_mode": {
"logicalType": "text",
"notNull": false,
"defaultValue": "'pattern'",
"primaryKey": false
}
}
}
@@ -1423,6 +1451,23 @@
],
"unique": false
},
{
"name": "route_group_sources_group_source_unique",
"table": "route_group_sources",
"columns": [
"group_route_id",
"source_route_id"
],
"unique": true
},
{
"name": "route_group_sources_source_route_id_idx",
"table": "route_group_sources",
"columns": [
"source_route_id"
],
"unique": false
},
{
"name": "site_disabled_models_site_id_idx",
"table": "site_disabled_models",
@@ -1538,6 +1583,14 @@
"public_id"
]
},
{
"name": "route_group_sources_group_source_unique",
"table": "route_group_sources",
"columns": [
"group_route_id",
"source_route_id"
]
},
{
"name": "site_disabled_models_site_model_unique",
"table": "site_disabled_models",
@@ -1641,6 +1694,28 @@
],
"onDelete": "SET NULL"
},
{
"table": "route_group_sources",
"columns": [
"group_route_id"
],
"referencedTable": "token_routes",
"referencedColumns": [
"id"
],
"onDelete": "CASCADE"
},
{
"table": "route_group_sources",
"columns": [
"source_route_id"
],
"referencedTable": "token_routes",
"referencedColumns": [
"id"
],
"onDelete": "CASCADE"
},
{
"table": "site_disabled_models",
"columns": [
+21
View File
@@ -27,6 +27,7 @@ const TABLES_WITH_NUMERIC_ID = new Set([
'model_availability',
'token_model_availability',
'token_routes',
'route_group_sources',
'route_channels',
'proxy_logs',
'proxy_video_tasks',
@@ -463,6 +464,10 @@ function ensureRouteGroupingSchema() {
execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN display_icon text;`);
}
if (!tableColumnExists('token_routes', 'route_mode')) {
execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT 'pattern';`);
}
if (!tableColumnExists('token_routes', 'decision_snapshot')) {
execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN decision_snapshot text;`);
}
@@ -490,6 +495,22 @@ function ensureRouteGroupingSchema() {
if (!tableColumnExists('route_channels', 'cooldown_level')) {
execSqliteLegacyCompat(`ALTER TABLE route_channels ADD COLUMN cooldown_level integer NOT NULL DEFAULT 0;`);
}
execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS route_group_sources (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade,
source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade
);
`);
execSqliteLegacyCompat(`
CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique
ON route_group_sources(group_route_id, source_route_id);
`);
execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx
ON route_group_sources(source_route_id);
`);
}
function ensureDownstreamApiKeySchema() {
+4
View File
@@ -32,6 +32,7 @@ export interface LegacySchemaCompatInspector extends
const LEGACY_COMPAT_TABLES = new Set([
'account_tokens',
'token_model_availability',
'route_group_sources',
'proxy_video_tasks',
'proxy_files',
'downstream_api_keys',
@@ -49,6 +50,7 @@ const LEGACY_COMPAT_COLUMNS = new Set([
'account_tokens.value_status',
'token_routes.display_name',
'token_routes.display_icon',
'token_routes.route_mode',
'token_routes.decision_snapshot',
'token_routes.decision_refreshed_at',
'token_routes.routing_strategy',
@@ -80,6 +82,8 @@ const LEGACY_COMPAT_INDEXES = new Set([
'downstream_api_keys_enabled_idx',
'downstream_api_keys_expires_at_idx',
'proxy_logs_downstream_api_key_created_at_idx',
'route_group_sources_group_source_unique',
'route_group_sources_source_route_id_idx',
...SHARED_INDEX_COMPATIBILITY_SPECS.map((spec) => spec.indexName),
]);
@@ -8,9 +8,11 @@ function createInspector(
dialect: RouteGroupingSchemaInspector['dialect'],
options?: {
existingColumnsByTable?: Partial<Record<'token_routes' | 'route_channels', string[]>>;
existingTables?: Array<'token_routes' | 'route_channels' | 'route_group_sources'>;
},
) {
const executedSql: string[] = [];
const existingTables = new Set(options?.existingTables ?? ['token_routes', 'route_channels']);
const existingColumnsByTable = {
token_routes: new Set(options?.existingColumnsByTable?.token_routes ?? []),
route_channels: new Set(options?.existingColumnsByTable?.route_channels ?? []),
@@ -19,7 +21,7 @@ function createInspector(
const inspector: RouteGroupingSchemaInspector = {
dialect,
async tableExists(table) {
return table === 'token_routes' || table === 'route_channels';
return existingTables.has(table as 'token_routes' | 'route_channels' | 'route_group_sources');
},
async columnExists(table, column) {
if (table === 'token_routes' || table === 'route_channels') {
@@ -40,8 +42,12 @@ describe('ensureRouteGroupingSchemaCompatibility', () => {
{
dialect: 'sqlite' as const,
expectedSql: [
'CREATE TABLE IF NOT EXISTS route_group_sources (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade, source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade);',
'CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique ON route_group_sources(group_route_id, source_route_id);',
'CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx ON route_group_sources(source_route_id);',
'ALTER TABLE token_routes ADD COLUMN display_name text;',
'ALTER TABLE token_routes ADD COLUMN display_icon text;',
'ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT \'pattern\';',
'ALTER TABLE token_routes ADD COLUMN decision_snapshot text;',
'ALTER TABLE token_routes ADD COLUMN decision_refreshed_at text;',
'ALTER TABLE token_routes ADD COLUMN routing_strategy text DEFAULT \'weighted\';',
@@ -54,8 +60,12 @@ describe('ensureRouteGroupingSchemaCompatibility', () => {
{
dialect: 'postgres' as const,
expectedSql: [
'CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, CONSTRAINT "route_group_sources_group_route_id_token_routes_id_fk" FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, CONSTRAINT "route_group_sources_source_route_id_token_routes_id_fk" FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE)',
'CREATE UNIQUE INDEX IF NOT EXISTS "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id")',
'CREATE INDEX IF NOT EXISTS "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id")',
'ALTER TABLE "token_routes" ADD COLUMN "display_name" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "display_icon" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "route_mode" TEXT DEFAULT \'pattern\'',
'ALTER TABLE "token_routes" ADD COLUMN "decision_snapshot" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "decision_refreshed_at" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "routing_strategy" TEXT DEFAULT \'weighted\'',
@@ -68,8 +78,12 @@ describe('ensureRouteGroupingSchemaCompatibility', () => {
{
dialect: 'mysql' as const,
expectedSql: [
'CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` int AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` int NOT NULL, `source_route_id` int NOT NULL, CONSTRAINT `route_group_sources_group_route_id_token_routes_id_fk` FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade, CONSTRAINT `route_group_sources_source_route_id_token_routes_id_fk` FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade)',
'CREATE UNIQUE INDEX IF NOT EXISTS `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`,`source_route_id`)',
'CREATE INDEX IF NOT EXISTS `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`)',
'ALTER TABLE `token_routes` ADD COLUMN `display_name` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `display_icon` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `route_mode` VARCHAR(32) NULL DEFAULT \'pattern\'',
'ALTER TABLE `token_routes` ADD COLUMN `decision_snapshot` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `decision_refreshed_at` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `routing_strategy` VARCHAR(32) NULL DEFAULT \'weighted\'',
@@ -90,9 +104,10 @@ describe('ensureRouteGroupingSchemaCompatibility', () => {
it('skips existing columns', async () => {
const { inspector, executedSql } = createInspector('postgres', {
existingColumnsByTable: {
token_routes: ['display_name', 'display_icon', 'decision_snapshot', 'decision_refreshed_at', 'routing_strategy'],
token_routes: ['display_name', 'display_icon', 'route_mode', 'decision_snapshot', 'decision_refreshed_at', 'routing_strategy'],
route_channels: ['source_model', 'last_selected_at', 'consecutive_fail_count', 'cooldown_level'],
},
existingTables: ['token_routes', 'route_channels', 'route_group_sources'],
});
await ensureRouteGroupingSchemaCompatibility(inspector);
@@ -13,6 +13,11 @@ type RouteGroupingColumnCompatibilitySpec = {
addSql: Record<RouteGroupingSchemaDialect, string>;
};
type RouteGroupingTableCompatibilitySpec = {
table: 'route_group_sources';
createSql: Record<RouteGroupingSchemaDialect, string[]>;
};
const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilitySpec[] = [
{
table: 'token_routes',
@@ -32,6 +37,15 @@ const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilit
postgres: 'ALTER TABLE "token_routes" ADD COLUMN "display_icon" TEXT',
},
},
{
table: 'token_routes',
column: 'route_mode',
addSql: {
sqlite: 'ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT \'pattern\';',
mysql: 'ALTER TABLE `token_routes` ADD COLUMN `route_mode` VARCHAR(32) NULL DEFAULT \'pattern\'',
postgres: 'ALTER TABLE "token_routes" ADD COLUMN "route_mode" TEXT DEFAULT \'pattern\'',
},
},
{
table: 'token_routes',
column: 'decision_snapshot',
@@ -97,6 +111,29 @@ const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilit
},
];
const ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS: RouteGroupingTableCompatibilitySpec[] = [
{
table: 'route_group_sources',
createSql: {
sqlite: [
'CREATE TABLE IF NOT EXISTS route_group_sources (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade, source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade);',
'CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique ON route_group_sources(group_route_id, source_route_id);',
'CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx ON route_group_sources(source_route_id);',
],
mysql: [
'CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` int AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` int NOT NULL, `source_route_id` int NOT NULL, CONSTRAINT `route_group_sources_group_route_id_token_routes_id_fk` FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade, CONSTRAINT `route_group_sources_source_route_id_token_routes_id_fk` FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade)',
'CREATE UNIQUE INDEX IF NOT EXISTS `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`,`source_route_id`)',
'CREATE INDEX IF NOT EXISTS `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`)',
],
postgres: [
'CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, CONSTRAINT "route_group_sources_group_route_id_token_routes_id_fk" FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, CONSTRAINT "route_group_sources_source_route_id_token_routes_id_fk" FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE)',
'CREATE UNIQUE INDEX IF NOT EXISTS "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id")',
'CREATE INDEX IF NOT EXISTS "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id")',
],
},
},
];
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
@@ -124,6 +161,18 @@ async function executeAddColumn(inspector: RouteGroupingSchemaInspector, sqlText
export async function ensureRouteGroupingSchemaCompatibility(inspector: RouteGroupingSchemaInspector): Promise<void> {
const tableExistsCache = new Map<string, boolean>();
for (const spec of ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS) {
const hasTable = await inspector.tableExists(spec.table);
tableExistsCache.set(spec.table, hasTable);
if (hasTable) {
continue;
}
for (const sqlText of spec.createSql[inspector.dialect]) {
await executeAddColumn(inspector, sqlText);
}
tableExistsCache.set(spec.table, true);
}
for (const spec of ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS) {
let hasTable = tableExistsCache.get(spec.table);
if (hasTable === undefined) {
+10
View File
@@ -127,6 +127,7 @@ export const tokenRoutes = sqliteTable('token_routes', {
modelPattern: text('model_pattern').notNull(),
displayName: text('display_name'),
displayIcon: text('display_icon'),
routeMode: text('route_mode').default('pattern'),
modelMapping: text('model_mapping'), // JSON
decisionSnapshot: text('decision_snapshot'), // JSON
decisionRefreshedAt: text('decision_refreshed_at'),
@@ -139,6 +140,15 @@ export const tokenRoutes = sqliteTable('token_routes', {
enabledIdx: index('token_routes_enabled_idx').on(table.enabled),
}));
export const routeGroupSources = sqliteTable('route_group_sources', {
id: integer('id').primaryKey({ autoIncrement: true }),
groupRouteId: integer('group_route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
sourceRouteId: integer('source_route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
}, (table) => ({
groupSourceUnique: uniqueIndex('route_group_sources_group_source_unique').on(table.groupRouteId, table.sourceRouteId),
sourceRouteIdx: index('route_group_sources_source_route_id_idx').on(table.sourceRouteId),
}));
export const routeChannels = sqliteTable('route_channels', {
id: integer('id').primaryKey({ autoIncrement: true }),
routeId: integer('route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),