Parcourir la source

feat: add cache TTL preference functionality across key and provider management

- Introduced a new cache TTL preference feature for keys and providers, allowing users to specify cache control settings.
- Updated related forms and UI components to support cache TTL selection and display.
- Enhanced backend logic to handle cache TTL preferences during key creation, editing, and provider management.
- Added new fields in the database schema to store cache TTL preferences and updated validation schemas accordingly.
- Improved usage logs to track cache creation tokens based on TTL settings.

close #277
ding113 il y a 4 mois
Parent
commit
869442b590

+ 5 - 0
drizzle/0031_rare_roxanne_simpson.sql

@@ -0,0 +1,5 @@
+ALTER TABLE "keys" ADD COLUMN "cache_ttl_preference" varchar(10);--> statement-breakpoint
+ALTER TABLE "message_request" ADD COLUMN "cache_creation_5m_input_tokens" integer;--> statement-breakpoint
+ALTER TABLE "message_request" ADD COLUMN "cache_creation_1h_input_tokens" integer;--> statement-breakpoint
+ALTER TABLE "message_request" ADD COLUMN "cache_ttl_applied" varchar(10);--> statement-breakpoint
+ALTER TABLE "providers" ADD COLUMN "cache_ttl_preference" varchar(10);

+ 1853 - 0
drizzle/meta/0031_snapshot.json

@@ -0,0 +1,1853 @@
+{
+  "id": "2a30cb64-6325-46c7-a147-9758de5c3a00",
+  "prevId": "11225ca4-98e4-4ff3-b2e0-ff3430ae3808",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.error_rules": {
+      "name": "error_rules",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "pattern": {
+          "name": "pattern",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'regex'"
+        },
+        "category": {
+          "name": "category",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_response": {
+          "name": "override_response",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_status_code": {
+          "name": "override_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_error_rules_enabled": {
+          "name": "idx_error_rules_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "unique_pattern": {
+          "name": "unique_pattern",
+          "columns": [
+            {
+              "expression": "pattern",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_category": {
+          "name": "idx_category",
+          "columns": [
+            {
+              "expression": "category",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_match_type": {
+          "name": "idx_match_type",
+          "columns": [
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "can_login_web_ui": {
+          "name": "can_login_web_ui",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(21, 15)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_chain": {
+          "name": "provider_chain",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "api_type": {
+          "name": "api_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "endpoint": {
+          "name": "endpoint",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "original_model": {
+          "name": "original_model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_applied": {
+          "name": "cache_ttl_applied",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_reason": {
+          "name": "blocked_reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "messages_count": {
+          "name": "messages_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_query": {
+          "name": "idx_message_request_user_query",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id": {
+          "name": "idx_message_request_session_id",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_endpoint": {
+          "name": "idx_message_request_endpoint",
+          "columns": [
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_settings": {
+      "name": "notification_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_enabled": {
+          "name": "circuit_breaker_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_webhook": {
+          "name": "circuit_breaker_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_enabled": {
+          "name": "daily_leaderboard_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "daily_leaderboard_webhook": {
+          "name": "daily_leaderboard_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_time": {
+          "name": "daily_leaderboard_time",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'09:00'"
+        },
+        "daily_leaderboard_top_n": {
+          "name": "daily_leaderboard_top_n",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "cost_alert_enabled": {
+          "name": "cost_alert_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "cost_alert_webhook": {
+          "name": "cost_alert_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_alert_threshold": {
+          "name": "cost_alert_threshold",
+          "type": "numeric(5, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.80'"
+        },
+        "cost_alert_check_interval": {
+          "name": "cost_alert_check_interval",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'1.0'"
+        },
+        "group_tag": {
+          "name": "group_tag",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "model_redirects": {
+          "name": "model_redirects",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "codex_instructions_strategy": {
+          "name": "codex_instructions_strategy",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'auto'"
+        },
+        "mcp_passthrough_type": {
+          "name": "mcp_passthrough_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'none'"
+        },
+        "mcp_passthrough_url": {
+          "name": "mcp_passthrough_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "max_retry_attempts": {
+          "name": "max_retry_attempts",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "circuit_breaker_failure_threshold": {
+          "name": "circuit_breaker_failure_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "circuit_breaker_open_duration": {
+          "name": "circuit_breaker_open_duration",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1800000
+        },
+        "circuit_breaker_half_open_success_threshold": {
+          "name": "circuit_breaker_half_open_success_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 2
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "first_byte_timeout_streaming_ms": {
+          "name": "first_byte_timeout_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "streaming_idle_timeout_ms": {
+          "name": "streaming_idle_timeout_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "request_timeout_non_streaming_ms": {
+          "name": "request_timeout_non_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_providers_enabled_priority": {
+          "name": "idx_providers_enabled_priority",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "weight",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_group": {
+          "name": "idx_providers_group",
+          "columns": [
+            {
+              "expression": "group_tag",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_created_at": {
+          "name": "idx_providers_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_deleted_at": {
+          "name": "idx_providers_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.request_filters": {
+      "name": "request_filters",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "target": {
+          "name": "target",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "replacement": {
+          "name": "replacement",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_request_filters_enabled": {
+          "name": "idx_request_filters_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_scope": {
+          "name": "idx_request_filters_scope",
+          "columns": [
+            {
+              "expression": "scope",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_action": {
+          "name": "idx_request_filters_action",
+          "columns": [
+            {
+              "expression": "action",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sensitive_words": {
+      "name": "sensitive_words",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "word": {
+          "name": "word",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'contains'"
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_sensitive_words_enabled": {
+          "name": "idx_sensitive_words_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_sensitive_words_created_at": {
+          "name": "idx_sensitive_words_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.system_settings": {
+      "name": "system_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "site_title": {
+          "name": "site_title",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'Claude Code Hub'"
+        },
+        "allow_global_usage_view": {
+          "name": "allow_global_usage_view",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "currency_display": {
+          "name": "currency_display",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'USD'"
+        },
+        "billing_model_source": {
+          "name": "billing_model_source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'original'"
+        },
+        "enable_auto_cleanup": {
+          "name": "enable_auto_cleanup",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "cleanup_retention_days": {
+          "name": "cleanup_retention_days",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 30
+        },
+        "cleanup_schedule": {
+          "name": "cleanup_schedule",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0 2 * * *'"
+        },
+        "cleanup_batch_size": {
+          "name": "cleanup_batch_size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10000
+        },
+        "enable_client_version_check": {
+          "name": "enable_client_version_check",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "verbose_provider_error": {
+          "name": "verbose_provider_error",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_http2": {
+          "name": "enable_http2",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "tags": {
+          "name": "tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_enabled_expires_at": {
+          "name": "idx_users_enabled_expires_at",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "expires_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -218,6 +218,13 @@
       "when": 1765030525131,
       "tag": "0030_unusual_goliath",
       "breakpoints": true
+    },
+    {
+      "idx": 31,
+      "version": "7",
+      "when": 1765095940032,
+      "tag": "0031_rare_roxanne_simpson",
+      "breakpoints": true
     }
   ]
 }

+ 5 - 0
src/actions/keys.ts

@@ -35,6 +35,7 @@ export async function addKey(data: {
   limitTotalUsd?: number | null;
   limitConcurrentSessions?: number;
   providerGroup?: string | null;
+  cacheTtlPreference?: "inherit" | "5m" | "1h";
 }): Promise<ActionResult<{ generatedKey: string; name: string }>> {
   try {
     // 权限检查:用户只能给自己添加Key,管理员可以给所有人添加Key
@@ -59,6 +60,7 @@ export async function addKey(data: {
       limitTotalUsd: data.limitTotalUsd,
       limitConcurrentSessions: data.limitConcurrentSessions,
       providerGroup: data.providerGroup,
+      cacheTtlPreference: data.cacheTtlPreference,
     });
 
     // 检查是否存在同名的生效key
@@ -154,6 +156,7 @@ export async function addKey(data: {
       limit_total_usd: validatedData.limitTotalUsd,
       limit_concurrent_sessions: validatedData.limitConcurrentSessions,
       provider_group: validatedData.providerGroup || null,
+      cache_ttl_preference: validatedData.cacheTtlPreference,
     });
 
     revalidatePath("/dashboard");
@@ -183,6 +186,7 @@ export async function editKey(
     limitTotalUsd?: number | null;
     limitConcurrentSessions?: number;
     providerGroup?: string | null;
+    cacheTtlPreference?: "inherit" | "5m" | "1h";
   }
 ): Promise<ActionResult> {
   try {
@@ -290,6 +294,7 @@ export async function editKey(
       limit_total_usd: validatedData.limitTotalUsd,
       limit_concurrent_sessions: validatedData.limitConcurrentSessions,
       provider_group: validatedData.providerGroup || null,
+      cache_ttl_preference: validatedData.cacheTtlPreference,
     });
 
     revalidatePath("/dashboard");

+ 2 - 0
src/actions/providers.ts

@@ -204,6 +204,7 @@ export async function getProviders(): Promise<ProviderDisplay[]> {
         requestTimeoutNonStreamingMs: provider.requestTimeoutNonStreamingMs,
         websiteUrl: provider.websiteUrl,
         faviconUrl: provider.faviconUrl,
+        cacheTtlPreference: provider.cacheTtlPreference,
         tpm: provider.tpm,
         rpm: provider.rpm,
         rpd: provider.rpd,
@@ -347,6 +348,7 @@ export async function addProvider(data: {
       request_timeout_non_streaming_ms:
         validated.request_timeout_non_streaming_ms ??
         PROVIDER_TIMEOUT_DEFAULTS.REQUEST_TIMEOUT_NON_STREAMING_MS,
+      cache_ttl_preference: validated.cache_ttl_preference ?? "inherit",
       website_url: validated.website_url ?? null,
       favicon_url: faviconUrl,
       tpm: validated.tpm ?? null,

+ 22 - 0
src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx

@@ -45,6 +45,7 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) {
       expiresAt: "",
       canLoginWebUi: true,
       providerGroup: "",
+      cacheTtlPreference: "inherit",
       limit5hUsd: null,
       limitDailyUsd: null,
       dailyResetMode: "fixed" as const,
@@ -74,6 +75,7 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) {
           limitMonthlyUsd: data.limitMonthlyUsd,
           limitTotalUsd: data.limitTotalUsd,
           limitConcurrentSessions: data.limitConcurrentSessions,
+          cacheTtlPreference: data.cacheTtlPreference,
         });
 
         if (!result.ok) {
@@ -169,6 +171,26 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) {
         touched={form.getFieldProps("providerGroup").touched}
       />
 
+      <div className="space-y-2">
+        <Label>Cache TTL 覆写</Label>
+        <Select
+          value={form.values.cacheTtlPreference}
+          onValueChange={(val) => form.setValue("cacheTtlPreference", val as "inherit" | "5m" | "1h")}
+        >
+          <SelectTrigger>
+            <SelectValue placeholder="inherit" />
+          </SelectTrigger>
+          <SelectContent>
+            <SelectItem value="inherit">不覆写(跟随供应商/客户端)</SelectItem>
+            <SelectItem value="5m">5m</SelectItem>
+            <SelectItem value="1h">1h</SelectItem>
+          </SelectContent>
+        </Select>
+        <p className="text-xs text-muted-foreground">
+          强制为包含 cache_control 的请求设置 Anthropic prompt cache TTL。
+        </p>
+      </div>
+
       <FormGrid columns={2}>
         <NumberField
           label={t("limit5hUsd.label")}

+ 23 - 0
src/app/[locale]/dashboard/_components/user/forms/edit-key-form.tsx

@@ -27,6 +27,7 @@ interface EditKeyFormProps {
     expiresAt: string;
     canLoginWebUi?: boolean;
     providerGroup?: string | null;
+    cacheTtlPreference?: "inherit" | "5m" | "1h";
     limit5hUsd?: number | null;
     limitDailyUsd?: number | null;
     dailyResetMode?: "fixed" | "rolling";
@@ -68,6 +69,7 @@ export function EditKeyForm({ keyData, user, onSuccess }: EditKeyFormProps) {
       expiresAt: formatExpiresAt(keyData?.expiresAt || ""),
       canLoginWebUi: keyData?.canLoginWebUi ?? true,
       providerGroup: keyData?.providerGroup || "",
+      cacheTtlPreference: keyData?.cacheTtlPreference ?? "inherit",
       limit5hUsd: keyData?.limit5hUsd ?? null,
       limitDailyUsd: keyData?.limitDailyUsd ?? null,
       dailyResetMode: keyData?.dailyResetMode ?? "fixed",
@@ -89,6 +91,7 @@ export function EditKeyForm({ keyData, user, onSuccess }: EditKeyFormProps) {
             expiresAt: data.expiresAt || undefined,
             canLoginWebUi: data.canLoginWebUi,
             providerGroup: data.providerGroup || null,
+            cacheTtlPreference: data.cacheTtlPreference,
             limit5hUsd: data.limit5hUsd,
             limitDailyUsd: data.limitDailyUsd,
             dailyResetMode: data.dailyResetMode,
@@ -182,6 +185,26 @@ export function EditKeyForm({ keyData, user, onSuccess }: EditKeyFormProps) {
         touched={form.getFieldProps("providerGroup").touched}
       />
 
+      <div className="space-y-2">
+        <Label>Cache TTL 覆写</Label>
+        <Select
+          value={form.values.cacheTtlPreference}
+          onValueChange={(val) => form.setValue("cacheTtlPreference", val as "inherit" | "5m" | "1h")}
+        >
+          <SelectTrigger>
+            <SelectValue placeholder="inherit" />
+          </SelectTrigger>
+          <SelectContent>
+            <SelectItem value="inherit">不覆写(跟随供应商/客户端)</SelectItem>
+            <SelectItem value="5m">5m</SelectItem>
+            <SelectItem value="1h">1h</SelectItem>
+          </SelectContent>
+        </Select>
+        <p className="text-xs text-muted-foreground">
+          强制为包含 cache_control 的请求设置 Anthropic prompt cache TTL。
+        </p>
+      </div>
+
       <FormGrid columns={2}>
         <NumberField
           label={t("limit5hUsd.label")}

+ 18 - 1
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -235,7 +235,24 @@ export function UsageLogsTable({
                       {formatTokenAmount(log.outputTokens)}
                     </TableCell>
                     <TableCell className="text-right font-mono text-xs">
-                      {formatTokenAmount(log.cacheCreationInputTokens)}
+                      <TooltipProvider>
+                        <Tooltip delayDuration={250}>
+                          <TooltipTrigger asChild>
+                            <div className="flex items-center justify-end gap-1 cursor-help">
+                              <span>{formatTokenAmount(log.cacheCreationInputTokens)}</span>
+                              {log.cacheTtlApplied ? (
+                                <Badge variant="outline" className="text-[10px] leading-tight px-1">
+                                  {log.cacheTtlApplied}
+                                </Badge>
+                              ) : null}
+                            </div>
+                          </TooltipTrigger>
+                          <TooltipContent align="end" className="text-xs space-y-1">
+                            <div>5m: {formatTokenAmount(log.cacheCreation5mInputTokens)}</div>
+                            <div>1h: {formatTokenAmount(log.cacheCreation1hInputTokens)}</div>
+                          </TooltipContent>
+                        </Tooltip>
+                      </TooltipProvider>
                     </TableCell>
                     <TableCell className="text-right font-mono text-xs">
                       {formatTokenAmount(log.cacheReadInputTokens)}

+ 25 - 0
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx

@@ -118,6 +118,9 @@ export function ProviderForm({
   const [joinClaudePool, setJoinClaudePool] = useState<boolean>(
     sourceProvider?.joinClaudePool ?? false
   );
+  const [cacheTtlPreference, setCacheTtlPreference] = useState<"inherit" | "5m" | "1h">(
+    sourceProvider?.cacheTtlPreference ?? "inherit"
+  );
 
   // 熔断器配置(以分钟为单位显示,提交时转换为毫秒)
   // 允许 undefined,用户可以清空输入框,提交时使用默认值
@@ -337,6 +340,7 @@ export function ProviderForm({
             limit_weekly_usd: limitWeeklyUsd,
             limit_monthly_usd: limitMonthlyUsd,
             limit_concurrent_sessions: limitConcurrentSessions,
+            cache_ttl_preference: cacheTtlPreference,
             max_retry_attempts: maxRetryAttempts,
             circuit_breaker_failure_threshold: failureThreshold ?? 5,
             circuit_breaker_open_duration: openDurationMinutes
@@ -395,6 +399,7 @@ export function ProviderForm({
             limit_weekly_usd: limitWeeklyUsd,
             limit_monthly_usd: limitMonthlyUsd,
             limit_concurrent_sessions: limitConcurrentSessions ?? 0,
+            cache_ttl_preference: cacheTtlPreference,
             max_retry_attempts: maxRetryAttempts,
             circuit_breaker_failure_threshold: failureThreshold ?? 5,
             circuit_breaker_open_duration: openDurationMinutes
@@ -853,6 +858,26 @@ export function ProviderForm({
                     {t("sections.routing.scheduleParams.group.desc")}
                   </p>
                 </div>
+                <div className="space-y-2">
+                  <Label>Cache TTL 覆写</Label>
+                  <Select
+                    value={cacheTtlPreference}
+                    onValueChange={(val) => setCacheTtlPreference(val as "inherit" | "5m" | "1h")}
+                    disabled={isPending}
+                  >
+                    <SelectTrigger className="w-full">
+                      <SelectValue placeholder="inherit" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="inherit">不覆写(跟随客户端)</SelectItem>
+                      <SelectItem value="5m">5m</SelectItem>
+                      <SelectItem value="1h">1h</SelectItem>
+                    </SelectContent>
+                  </Select>
+                  <p className="text-xs text-muted-foreground">
+                    强制设置 prompt cache TTL;仅影响包含 cache_control 的请求。
+                  </p>
+                </div>
               </div>
             </div>
           </CollapsibleContent>

+ 108 - 0
src/app/v1/_lib/proxy/forwarder.ts

@@ -23,6 +23,7 @@ import { GeminiAuth } from "../gemini/auth";
 import { GEMINI_PROTOCOL } from "../gemini/protocol";
 import { HeaderProcessor } from "../headers";
 import { buildProxyUrl } from "../url";
+import type { CacheTtlPreference, CacheTtlResolved } from "@/types/cache";
 import {
   categorizeErrorAsync,
   ErrorCategory,
@@ -46,6 +47,63 @@ const STANDARD_ENDPOINTS = [
 const RETRY_LIMITS = PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS;
 const MAX_PROVIDER_SWITCHES = 20; // 保险栓:最多切换 20 次供应商(防止无限循环)
 
+type CacheTtlOption = CacheTtlPreference | null | undefined;
+
+function resolveCacheTtlPreference(
+  keyPref: CacheTtlOption,
+  providerPref: CacheTtlOption
+): CacheTtlResolved | null {
+  const normalize = (value: CacheTtlOption): CacheTtlResolved | null => {
+    if (!value || value === "inherit") return null;
+    return value;
+  };
+
+  return normalize(keyPref) ?? normalize(providerPref) ?? null;
+}
+
+function applyCacheTtlOverrideToMessage(
+  message: Record<string, unknown>,
+  ttl: CacheTtlResolved
+): boolean {
+  let applied = false;
+  const messages = (message as Record<string, unknown>).messages;
+
+  if (!Array.isArray(messages)) {
+    return applied;
+  }
+
+  for (const msg of messages) {
+    if (!msg || typeof msg !== "object") continue;
+    const msgObj = msg as Record<string, unknown>;
+    const content = msgObj.content;
+
+    if (!Array.isArray(content)) continue;
+
+    msgObj.content = content.map((item) => {
+      if (!item || typeof item !== "object") return item;
+      const itemObj = item as Record<string, unknown>;
+      const cacheControl = itemObj.cache_control;
+
+      if (cacheControl && typeof cacheControl === "object") {
+        const ccObj = cacheControl as Record<string, unknown>;
+        if (ccObj.type === "ephemeral") {
+          applied = true;
+          return {
+            ...itemObj,
+            cache_control: {
+              ...ccObj,
+              ttl: ttl === "1h" ? "1h" : "5m",
+            },
+          };
+        }
+      }
+      return item;
+    });
+  }
+
+  return applied;
+}
+
 function clampRetryAttempts(value: number): number {
   const numeric = Number(value);
   if (!Number.isFinite(numeric)) return RETRY_LIMITS.MIN;
@@ -637,6 +695,12 @@ export class ProxyForwarder {
       throw new Error("Provider is required");
     }
 
+    const resolvedCacheTtl = resolveCacheTtlPreference(
+      session.authState?.key?.cacheTtlPreference,
+      provider.cacheTtlPreference
+    );
+    session.setCacheTtlResolved(resolvedCacheTtl);
+
     // 应用模型重定向(如果配置了)
     const wasRedirected = ModelRedirector.apply(session, provider);
     if (wasRedirected) {
@@ -743,6 +807,20 @@ export class ProxyForwarder {
         }
       }
 
+      if (
+        resolvedCacheTtl &&
+        (provider.providerType === "claude" || provider.providerType === "claude-auth")
+      ) {
+        const applied = applyCacheTtlOverrideToMessage(session.request.message, resolvedCacheTtl);
+        if (applied) {
+          logger.info("ProxyForwarder: Applied cache TTL override to request", {
+            providerId: provider.id,
+            providerName: provider.name,
+            cacheTtl: resolvedCacheTtl,
+          });
+        }
+      }
+
       // Codex 请求清洗(即使格式相同也要执行,除非是官方客户端)
       if (toFormat === "codex") {
         const isOfficialClient = isOfficialCodexClient(session.userAgent);
@@ -803,6 +881,19 @@ export class ProxyForwarder {
         }
       }
 
+      if (
+        resolvedCacheTtl &&
+        (provider.providerType === "claude" || provider.providerType === "claude-auth")
+      ) {
+        const applied = applyCacheTtlOverrideToMessage(session.request.message, resolvedCacheTtl);
+        if (applied) {
+          logger.debug("ProxyForwarder: Applied cache TTL override to request", {
+            providerId: provider.id,
+            ttl: resolvedCacheTtl,
+          });
+        }
+      }
+
       processedHeaders = ProxyForwarder.buildHeaders(session, provider);
 
       if (process.env.NODE_ENV === "development") {
@@ -1494,6 +1585,23 @@ export class ProxyForwarder {
       });
     }
 
+    // 针对 1h 缓存 TTL,补充 Anthropic beta header(避免客户端遗漏)
+    if (session.getCacheTtlResolved && session.getCacheTtlResolved() === "1h") {
+      const existingBeta = session.headers.get("anthropic-beta") || "";
+      const betaFlags = new Set(
+        existingBeta
+          .split(",")
+          .map((s) => s.trim())
+          .filter(Boolean)
+      );
+      betaFlags.add("extended-cache-ttl-2025-04-11");
+      // 确保包含基础的 prompt-caching 标记
+      if (betaFlags.size === 1) {
+        betaFlags.add("prompt-caching-2024-07-31");
+      }
+      overrides["anthropic-beta"] = Array.from(betaFlags).join(", ");
+    }
+
     const headerProcessor = HeaderProcessor.createForProxy({
       blacklist: ["content-length", "connection"], // 删除 content-length(动态计算)和 connection(undici 自动管理)
       overrides,

+ 69 - 15
src/app/v1/_lib/proxy/response-handler.ts

@@ -26,6 +26,9 @@ export type UsageMetrics = {
   input_tokens?: number;
   output_tokens?: number;
   cache_creation_input_tokens?: number;
+  cache_creation_5m_input_tokens?: number;
+  cache_creation_1h_input_tokens?: number;
+  cache_ttl?: "5m" | "1h" | "mixed";
   cache_read_input_tokens?: number;
 };
 
@@ -1196,6 +1199,39 @@ function extractUsageMetrics(value: unknown): UsageMetrics | null {
     hasAny = true;
   }
 
+  const cacheCreationDetails = usage.cache_creation as Record<string, unknown> | undefined;
+  let cacheCreationDetailedTotal = 0;
+
+  if (cacheCreationDetails) {
+    if (typeof cacheCreationDetails.ephemeral_5m_input_tokens === "number") {
+      result.cache_creation_5m_input_tokens = cacheCreationDetails.ephemeral_5m_input_tokens;
+      cacheCreationDetailedTotal += cacheCreationDetails.ephemeral_5m_input_tokens;
+      hasAny = true;
+    }
+    if (typeof cacheCreationDetails.ephemeral_1h_input_tokens === "number") {
+      result.cache_creation_1h_input_tokens = cacheCreationDetails.ephemeral_1h_input_tokens;
+      cacheCreationDetailedTotal += cacheCreationDetails.ephemeral_1h_input_tokens;
+      hasAny = true;
+    }
+  }
+
+  if (
+    result.cache_creation_input_tokens === undefined &&
+    cacheCreationDetailedTotal > 0
+  ) {
+    result.cache_creation_input_tokens = cacheCreationDetailedTotal;
+  }
+
+  if (!result.cache_ttl) {
+    if (result.cache_creation_1h_input_tokens && result.cache_creation_5m_input_tokens) {
+      result.cache_ttl = "mixed";
+    } else if (result.cache_creation_1h_input_tokens) {
+      result.cache_ttl = "1h";
+    } else if (result.cache_creation_5m_input_tokens) {
+      result.cache_ttl = "5m";
+    }
+  }
+
   // Claude 格式:顶层 cache_read_input_tokens(扁平结构)
   if (typeof usage.cache_read_input_tokens === "number") {
     result.cache_read_input_tokens = usage.cache_read_input_tokens;
@@ -1514,16 +1550,35 @@ async function finalizeRequestStats(
   }
 
   // 4. 更新成本
+  const resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null;
+  const cache5m =
+    usageMetrics.cache_creation_5m_input_tokens ??
+    (resolvedCacheTtl === "1h" ? undefined : usageMetrics.cache_creation_input_tokens);
+  const cache1h =
+    usageMetrics.cache_creation_1h_input_tokens ??
+    (resolvedCacheTtl === "1h" ? usageMetrics.cache_creation_input_tokens : undefined);
+  const cacheTotal =
+    usageMetrics.cache_creation_input_tokens ??
+    ((cache5m ?? 0) + (cache1h ?? 0) || undefined);
+
+  const normalizedUsage: UsageMetrics = {
+    ...usageMetrics,
+    cache_ttl: resolvedCacheTtl ?? usageMetrics.cache_ttl,
+    cache_creation_5m_input_tokens: cache5m,
+    cache_creation_1h_input_tokens: cache1h,
+    cache_creation_input_tokens: cacheTotal,
+  };
+
   await updateRequestCostFromUsage(
     messageContext.id,
     session.getOriginalModel(),
     session.getCurrentModel(),
-    usageMetrics,
+    normalizedUsage,
     provider.costMultiplier
   );
 
   // 5. 追踪消费到 Redis(用于限流)
-  await trackCostToRedis(session, usageMetrics);
+  await trackCostToRedis(session, normalizedUsage);
 
   // 6. 更新 session usage
   if (session.sessionId) {
@@ -1531,11 +1586,7 @@ async function finalizeRequestStats(
     if (session.request.model) {
       const priceData = await findLatestPriceByModel(session.request.model);
       if (priceData?.priceData) {
-        const cost = calculateRequestCost(
-          usageMetrics,
-          priceData.priceData,
-          provider.costMultiplier
-        );
+        const cost = calculateRequestCost(normalizedUsage, priceData.priceData, provider.costMultiplier);
         if (cost.gt(0)) {
           costUsdStr = cost.toString();
         }
@@ -1543,10 +1594,10 @@ async function finalizeRequestStats(
     }
 
     void SessionManager.updateSessionUsage(session.sessionId, {
-      inputTokens: usageMetrics.input_tokens,
-      outputTokens: usageMetrics.output_tokens,
-      cacheCreationInputTokens: usageMetrics.cache_creation_input_tokens,
-      cacheReadInputTokens: usageMetrics.cache_read_input_tokens,
+      inputTokens: normalizedUsage.input_tokens,
+      outputTokens: normalizedUsage.output_tokens,
+      cacheCreationInputTokens: normalizedUsage.cache_creation_input_tokens,
+      cacheReadInputTokens: normalizedUsage.cache_read_input_tokens,
       costUsd: costUsdStr,
       status: statusCode >= 200 && statusCode < 300 ? "completed" : "error",
       statusCode: statusCode,
@@ -1558,10 +1609,13 @@ async function finalizeRequestStats(
   // 7. 更新请求详情
   await updateMessageRequestDetails(messageContext.id, {
     statusCode: statusCode,
-    inputTokens: usageMetrics.input_tokens,
-    outputTokens: usageMetrics.output_tokens,
-    cacheCreationInputTokens: usageMetrics.cache_creation_input_tokens,
-    cacheReadInputTokens: usageMetrics.cache_read_input_tokens,
+    inputTokens: normalizedUsage.input_tokens,
+    outputTokens: normalizedUsage.output_tokens,
+    cacheCreationInputTokens: normalizedUsage.cache_creation_input_tokens,
+    cacheReadInputTokens: normalizedUsage.cache_read_input_tokens,
+    cacheCreation5mInputTokens: normalizedUsage.cache_creation_5m_input_tokens,
+    cacheCreation1hInputTokens: normalizedUsage.cache_creation_1h_input_tokens,
+    cacheTtlApplied: normalizedUsage.cache_ttl ?? null,
     providerChain: session.getProviderChain(),
     model: session.getCurrentModel() ?? undefined,
     providerId: session.provider?.id, // ⭐ 更新最终供应商ID(重试切换后)

+ 14 - 2
src/app/v1/_lib/proxy/session.ts

@@ -5,6 +5,7 @@ import type { ProviderChainItem } from "@/types/message";
 import type { Provider, ProviderType } from "@/types/provider";
 import type { User } from "@/types/user";
 import type { ClientFormat } from "./format-mapper";
+import type { CacheTtlResolved } from "@/types/cache";
 
 export interface AuthState {
   user: User | null;
@@ -69,6 +70,9 @@ export class ProxySession {
   // 上次选择的决策上下文(用于记录到 providerChain)
   private _lastSelectionContext?: ProviderChainItem["decisionContext"];
 
+  // Cache TTL override (resolved)
+  private cacheTtlResolved: CacheTtlResolved | null = null;
+
   private constructor(init: {
     startTime: number;
     method: string;
@@ -158,8 +162,16 @@ export class ProxySession {
   setProvider(provider: Provider | null): void {
     this.provider = provider;
     if (provider) {
-      this.providerType = provider.providerType as ProviderType;
-    }
+    this.providerType = provider.providerType as ProviderType;
+  }
+  }
+
+  setCacheTtlResolved(ttl: CacheTtlResolved | null): void {
+    this.cacheTtlResolved = ttl;
+  }
+
+  getCacheTtlResolved(): CacheTtlResolved | null {
+    return this.cacheTtlResolved;
   }
 
   /**

+ 9 - 0
src/drizzle/schema.ts

@@ -84,6 +84,9 @@ export const keys = pgTable('keys', {
   // Provider group override (null = inherit from user)
   providerGroup: varchar('provider_group', { length: 50 }),
 
+  // Cache TTL override:null/NULL 表示遵循供应商或客户端请求
+  cacheTtlPreference: varchar('cache_ttl_preference', { length: 10 }),
+
   createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
   updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
   deletedAt: timestamp('deleted_at', { withTimezone: true }),
@@ -202,6 +205,9 @@ export const providers = pgTable('providers', {
   websiteUrl: text('website_url'),
   faviconUrl: text('favicon_url'),
 
+  // Cache TTL override(null = 不覆写,沿用客户端请求)
+  cacheTtlPreference: varchar('cache_ttl_preference', { length: 10 }),
+
   // 废弃(保留向后兼容,但不再使用)
   tpm: integer('tpm').default(0),
   rpm: integer('rpm').default(0),
@@ -257,6 +263,9 @@ export const messageRequest = pgTable('message_request', {
   outputTokens: integer('output_tokens'),
   cacheCreationInputTokens: integer('cache_creation_input_tokens'),
   cacheReadInputTokens: integer('cache_read_input_tokens'),
+  cacheCreation5mInputTokens: integer('cache_creation_5m_input_tokens'),
+  cacheCreation1hInputTokens: integer('cache_creation_1h_input_tokens'),
+  cacheTtlApplied: varchar('cache_ttl_applied', { length: 10 }),
 
   // 错误信息
   errorMessage: text('error_message'),

+ 14 - 0
src/lib/provider-testing/parsers/anthropic-parser.ts

@@ -24,6 +24,10 @@ interface AnthropicResponse {
     output_tokens?: number;
     cache_creation_input_tokens?: number;
     cache_read_input_tokens?: number;
+    cache_creation?: {
+      ephemeral_5m_input_tokens?: number;
+      ephemeral_1h_input_tokens?: number;
+    };
   };
   error?: {
     type?: string;
@@ -67,7 +71,17 @@ export function parseAnthropicResponse(body: string, contentType?: string): Pars
         outputTokens: data.usage.output_tokens || 0,
         cacheCreationInputTokens: data.usage.cache_creation_input_tokens,
         cacheReadInputTokens: data.usage.cache_read_input_tokens,
+        cacheCreation5mInputTokens: data.usage.cache_creation?.ephemeral_5m_input_tokens,
+        cacheCreation1hInputTokens: data.usage.cache_creation?.ephemeral_1h_input_tokens,
       };
+
+      if (
+        usage.cacheCreationInputTokens === undefined &&
+        (usage.cacheCreation5mInputTokens || usage.cacheCreation1hInputTokens)
+      ) {
+        usage.cacheCreationInputTokens =
+          (usage.cacheCreation5mInputTokens || 0) + (usage.cacheCreation1hInputTokens || 0);
+      }
     }
 
     return {

+ 2 - 0
src/lib/provider-testing/types.ts

@@ -116,6 +116,8 @@ export interface TokenUsage {
   outputTokens: number;
   cacheCreationInputTokens?: number;
   cacheReadInputTokens?: number;
+  cacheCreation5mInputTokens?: number;
+  cacheCreation1hInputTokens?: number;
 }
 
 /**

+ 37 - 4
src/lib/utils/cost-calculation.ts

@@ -5,6 +5,9 @@ type UsageMetrics = {
   input_tokens?: number;
   output_tokens?: number;
   cache_creation_input_tokens?: number;
+  cache_creation_5m_input_tokens?: number;
+  cache_creation_1h_input_tokens?: number;
+  cache_ttl?: "5m" | "1h" | "mixed";
   cache_read_input_tokens?: number;
 };
 
@@ -36,17 +39,47 @@ export function calculateRequestCost(
   const inputCostPerToken = priceData.input_cost_per_token;
   const outputCostPerToken = priceData.output_cost_per_token;
 
-  const cacheCreationCost =
+  const cacheCreation5mCost =
     priceData.cache_creation_input_token_cost ??
-    (inputCostPerToken != null ? inputCostPerToken * 0.1 : undefined);
+    (inputCostPerToken != null ? inputCostPerToken * 1.25 : undefined);
+
+  const cacheCreation1hCost =
+    priceData.cache_creation_input_token_cost_above_1hr ??
+    (inputCostPerToken != null ? inputCostPerToken * 2 : undefined) ??
+    cacheCreation5mCost;
 
   const cacheReadCost =
     priceData.cache_read_input_token_cost ??
-    (outputCostPerToken != null ? outputCostPerToken * 0.1 : undefined);
+    (inputCostPerToken != null
+      ? inputCostPerToken * 0.1
+      : outputCostPerToken != null
+        ? outputCostPerToken * 0.1
+        : undefined);
+
+  // Derive cache creation tokens by TTL
+  let cache5mTokens = usage.cache_creation_5m_input_tokens;
+  let cache1hTokens = usage.cache_creation_1h_input_tokens;
+
+  if (typeof usage.cache_creation_input_tokens === "number") {
+    const remaining =
+      usage.cache_creation_input_tokens -
+      (cache5mTokens ?? 0) -
+      (cache1hTokens ?? 0);
+
+    if (remaining > 0) {
+      const target = usage.cache_ttl === "1h" ? "1h" : "5m";
+      if (target === "1h") {
+        cache1hTokens = (cache1hTokens ?? 0) + remaining;
+      } else {
+        cache5mTokens = (cache5mTokens ?? 0) + remaining;
+      }
+    }
+  }
 
   segments.push(multiplyCost(usage.input_tokens, inputCostPerToken));
   segments.push(multiplyCost(usage.output_tokens, outputCostPerToken));
-  segments.push(multiplyCost(usage.cache_creation_input_tokens, cacheCreationCost));
+  segments.push(multiplyCost(cache5mTokens, cacheCreation5mCost));
+  segments.push(multiplyCost(cache1hTokens, cacheCreation1hCost));
   segments.push(multiplyCost(usage.cache_read_input_tokens, cacheReadCost));
 
   const total = segments.reduce((acc, segment) => acc.plus(segment), new Decimal(0));

+ 22 - 17
src/lib/validation/schemas.ts

@@ -7,6 +7,8 @@ import {
 import { USER_DEFAULTS, USER_LIMITS } from "@/lib/constants/user.constants";
 import { CURRENCY_CONFIG } from "@/lib/utils/currency";
 
+const CACHE_TTL_PREFERENCE = z.enum(["inherit", "5m", "1h"]);
+
 /**
  * 用户创建数据验证schema
  */
@@ -199,6 +201,7 @@ export const KeyFormSchema = z.object({
     .optional()
     .default(0),
   providerGroup: z.string().max(50, "供应商分组不能超过50个字符").optional().default(""),
+  cacheTtlPreference: CACHE_TTL_PREFERENCE.optional().default("inherit"),
 });
 
 /**
@@ -309,6 +312,7 @@ export const CreateProviderSchema = z.object({
     .max(1000, "并发Session上限不能超过1000")
     .optional()
     .default(0),
+  cache_ttl_preference: CACHE_TTL_PREFERENCE.optional().default("inherit"),
   max_retry_attempts: z.coerce
     .number()
     .int("重试次数必须是整数")
@@ -484,23 +488,24 @@ export const UpdateProviderSchema = z
       .max(50000, "周消费上限不能超过50000美元")
       .nullable()
       .optional(),
-    limit_monthly_usd: z.coerce
-      .number()
-      .min(0, "月消费上限不能为负数")
-      .max(200000, "月消费上限不能超过200000美元")
-      .nullable()
-      .optional(),
-    limit_concurrent_sessions: z.coerce
-      .number()
-      .int("并发Session上限必须是整数")
-      .min(0, "并发Session上限不能为负数")
-      .max(1000, "并发Session上限不能超过1000")
-      .optional(),
-    max_retry_attempts: z.coerce
-      .number()
-      .int("重试次数必须是整数")
-      .min(PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS.MIN, "重试次数不能少于1次")
-      .max(PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS.MAX, "重试次数不能超过10次")
+  limit_monthly_usd: z.coerce
+    .number()
+    .min(0, "月消费上限不能为负数")
+    .max(200000, "月消费上限不能超过200000美元")
+    .nullable()
+    .optional(),
+  limit_concurrent_sessions: z.coerce
+    .number()
+    .int("并发Session上限必须是整数")
+    .min(0, "并发Session上限不能为负数")
+    .max(1000, "并发Session上限不能超过1000")
+    .optional(),
+  cache_ttl_preference: CACHE_TTL_PREFERENCE.optional(),
+  max_retry_attempts: z.coerce
+    .number()
+    .int("重试次数必须是整数")
+    .min(PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS.MIN, "重试次数不能少于1次")
+    .max(PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS.MAX, "重试次数不能超过10次")
       .nullable()
       .optional(),
     // 熔断器配置

+ 5 - 0
src/repository/_shared/transformers.ts

@@ -44,6 +44,7 @@ export function toKey(dbKey: any): Key {
         : null,
     limitConcurrentSessions: dbKey?.limitConcurrentSessions ?? 0,
     providerGroup: dbKey?.providerGroup ?? null,
+    cacheTtlPreference: dbKey?.cacheTtlPreference ?? null,
     createdAt: dbKey?.createdAt ? new Date(dbKey.createdAt) : new Date(),
     updatedAt: dbKey?.updatedAt ? new Date(dbKey.updatedAt) : new Date(),
   };
@@ -83,6 +84,7 @@ export function toProvider(dbProvider: any): Provider {
     requestTimeoutNonStreamingMs: dbProvider?.requestTimeoutNonStreamingMs ?? 600000,
     websiteUrl: dbProvider?.websiteUrl ?? null,
     faviconUrl: dbProvider?.faviconUrl ?? null,
+    cacheTtlPreference: dbProvider?.cacheTtlPreference ?? null,
     tpm: dbProvider?.tpm ?? null,
     rpm: dbProvider?.rpm ?? null,
     rpd: dbProvider?.rpd ?? null,
@@ -103,6 +105,9 @@ export function toMessageRequest(dbMessage: any): MessageRequest {
       const formatted = formatCostForStorage(dbMessage?.costUsd);
       return formatted ?? undefined;
     })(),
+    cacheCreation5mInputTokens: dbMessage?.cacheCreation5mInputTokens ?? undefined,
+    cacheCreation1hInputTokens: dbMessage?.cacheCreation1hInputTokens ?? undefined,
+    cacheTtlApplied: dbMessage?.cacheTtlApplied ?? null,
   };
 }
 

+ 9 - 0
src/repository/key.ts

@@ -27,6 +27,7 @@ export async function findKeyById(id: number): Promise<Key | null> {
       limitTotalUsd: keys.limitTotalUsd,
       limitConcurrentSessions: keys.limitConcurrentSessions,
       providerGroup: keys.providerGroup,
+      cacheTtlPreference: keys.cacheTtlPreference,
       createdAt: keys.createdAt,
       updatedAt: keys.updatedAt,
       deletedAt: keys.deletedAt,
@@ -57,6 +58,7 @@ export async function findKeyList(userId: number): Promise<Key[]> {
       limitTotalUsd: keys.limitTotalUsd,
       limitConcurrentSessions: keys.limitConcurrentSessions,
       providerGroup: keys.providerGroup,
+      cacheTtlPreference: keys.cacheTtlPreference,
       createdAt: keys.createdAt,
       updatedAt: keys.updatedAt,
       deletedAt: keys.deletedAt,
@@ -86,6 +88,7 @@ export async function createKey(keyData: CreateKeyData): Promise<Key> {
     limitTotalUsd: keyData.limit_total_usd != null ? keyData.limit_total_usd.toString() : null,
     limitConcurrentSessions: keyData.limit_concurrent_sessions,
     providerGroup: keyData.provider_group ?? null,
+    cacheTtlPreference: keyData.cache_ttl_preference ?? null,
   };
 
   const [key] = await db.insert(keys).values(dbData).returning({
@@ -145,6 +148,8 @@ export async function updateKey(id: number, keyData: UpdateKeyData): Promise<Key
   if (keyData.limit_concurrent_sessions !== undefined)
     dbData.limitConcurrentSessions = keyData.limit_concurrent_sessions;
   if (keyData.provider_group !== undefined) dbData.providerGroup = keyData.provider_group;
+  if (keyData.cache_ttl_preference !== undefined)
+    dbData.cacheTtlPreference = keyData.cache_ttl_preference ?? null;
 
   const [key] = await db
     .update(keys)
@@ -167,6 +172,7 @@ export async function updateKey(id: number, keyData: UpdateKeyData): Promise<Key
       limitTotalUsd: keys.limitTotalUsd,
       limitConcurrentSessions: keys.limitConcurrentSessions,
       providerGroup: keys.providerGroup,
+      cacheTtlPreference: keys.cacheTtlPreference,
       createdAt: keys.createdAt,
       updatedAt: keys.updatedAt,
       deletedAt: keys.deletedAt,
@@ -198,6 +204,7 @@ export async function findActiveKeyByUserIdAndName(
       limitTotalUsd: keys.limitTotalUsd,
       limitConcurrentSessions: keys.limitConcurrentSessions,
       providerGroup: keys.providerGroup,
+      cacheTtlPreference: keys.cacheTtlPreference,
       createdAt: keys.createdAt,
       updatedAt: keys.updatedAt,
       deletedAt: keys.deletedAt,
@@ -331,6 +338,7 @@ export async function validateApiKeyAndGetUser(
       keyLimitTotalUsd: keys.limitTotalUsd,
       keyLimitConcurrentSessions: keys.limitConcurrentSessions,
       keyProviderGroup: keys.providerGroup,
+      keyCacheTtlPreference: keys.cacheTtlPreference,
       keyCreatedAt: keys.createdAt,
       keyUpdatedAt: keys.updatedAt,
       keyDeletedAt: keys.deletedAt,
@@ -400,6 +408,7 @@ export async function validateApiKeyAndGetUser(
     limitTotalUsd: row.keyLimitTotalUsd,
     limitConcurrentSessions: row.keyLimitConcurrentSessions,
     providerGroup: row.keyProviderGroup,
+    cacheTtlPreference: row.keyCacheTtlPreference,
     createdAt: row.keyCreatedAt,
     updatedAt: row.keyUpdatedAt,
     deletedAt: row.keyDeletedAt,

+ 25 - 0
src/repository/message.ts

@@ -27,6 +27,11 @@ export async function createMessageRequest(
     userAgent: data.user_agent, // User-Agent
     endpoint: data.endpoint, // 请求端点(可为空)
     messagesCount: data.messages_count, // Messages 数量
+    cacheTtlApplied: data.cache_ttl_applied,
+    cacheCreationInputTokens: data.cache_creation_input_tokens,
+    cacheCreation5mInputTokens: data.cache_creation_5m_input_tokens,
+    cacheCreation1hInputTokens: data.cache_creation_1h_input_tokens,
+    cacheReadInputTokens: data.cache_read_input_tokens,
   };
 
   const [result] = await db.insert(messageRequest).values(dbData).returning({
@@ -43,6 +48,11 @@ export async function createMessageRequest(
     userAgent: messageRequest.userAgent, // 新增
     endpoint: messageRequest.endpoint, // 新增:返回端点
     messagesCount: messageRequest.messagesCount, // 新增
+    cacheTtlApplied: messageRequest.cacheTtlApplied,
+    cacheCreationInputTokens: messageRequest.cacheCreationInputTokens,
+    cacheCreation5mInputTokens: messageRequest.cacheCreation5mInputTokens,
+    cacheCreation1hInputTokens: messageRequest.cacheCreation1hInputTokens,
+    cacheReadInputTokens: messageRequest.cacheReadInputTokens,
     createdAt: messageRequest.createdAt,
     updatedAt: messageRequest.updatedAt,
     deletedAt: messageRequest.deletedAt,
@@ -96,6 +106,9 @@ export async function updateMessageRequestDetails(
     outputTokens?: number;
     cacheCreationInputTokens?: number;
     cacheReadInputTokens?: number;
+    cacheCreation5mInputTokens?: number;
+    cacheCreation1hInputTokens?: number;
+    cacheTtlApplied?: string | null;
     providerChain?: CreateMessageRequestData["provider_chain"];
     errorMessage?: string;
     model?: string; // ⭐ 新增:支持更新重定向后的模型名称
@@ -121,6 +134,15 @@ export async function updateMessageRequestDetails(
   if (details.cacheReadInputTokens !== undefined) {
     updateData.cacheReadInputTokens = details.cacheReadInputTokens;
   }
+  if (details.cacheCreation5mInputTokens !== undefined) {
+    updateData.cacheCreation5mInputTokens = details.cacheCreation5mInputTokens;
+  }
+  if (details.cacheCreation1hInputTokens !== undefined) {
+    updateData.cacheCreation1hInputTokens = details.cacheCreation1hInputTokens;
+  }
+  if (details.cacheTtlApplied !== undefined) {
+    updateData.cacheTtlApplied = details.cacheTtlApplied;
+  }
   if (details.providerChain !== undefined) {
     updateData.providerChain = details.providerChain;
   }
@@ -188,6 +210,9 @@ export async function findMessageRequestBySessionId(
       outputTokens: messageRequest.outputTokens,
       cacheCreationInputTokens: messageRequest.cacheCreationInputTokens,
       cacheReadInputTokens: messageRequest.cacheReadInputTokens,
+      cacheCreation5mInputTokens: messageRequest.cacheCreation5mInputTokens,
+      cacheCreation1hInputTokens: messageRequest.cacheCreation1hInputTokens,
+      cacheTtlApplied: messageRequest.cacheTtlApplied,
       errorMessage: messageRequest.errorMessage,
       providerChain: messageRequest.providerChain,
       blockedBy: messageRequest.blockedBy,

+ 8 - 0
src/repository/provider.ts

@@ -48,6 +48,7 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     requestTimeoutNonStreamingMs: providerData.request_timeout_non_streaming_ms ?? 600000,
     websiteUrl: providerData.website_url ?? null,
     faviconUrl: providerData.favicon_url ?? null,
+    cacheTtlPreference: providerData.cache_ttl_preference ?? null,
     tpm: providerData.tpm,
     rpm: providerData.rpm,
     rpd: providerData.rpd,
@@ -89,6 +90,7 @@ export async function createProvider(providerData: CreateProviderData): Promise<
     requestTimeoutNonStreamingMs: providers.requestTimeoutNonStreamingMs,
     websiteUrl: providers.websiteUrl,
     faviconUrl: providers.faviconUrl,
+    cacheTtlPreference: providers.cacheTtlPreference,
     tpm: providers.tpm,
     rpm: providers.rpm,
     rpd: providers.rpd,
@@ -141,6 +143,7 @@ export async function findProviderList(
       requestTimeoutNonStreamingMs: providers.requestTimeoutNonStreamingMs,
       websiteUrl: providers.websiteUrl,
       faviconUrl: providers.faviconUrl,
+      cacheTtlPreference: providers.cacheTtlPreference,
       tpm: providers.tpm,
       rpm: providers.rpm,
       rpd: providers.rpd,
@@ -204,6 +207,7 @@ export async function findAllProviders(): Promise<Provider[]> {
       requestTimeoutNonStreamingMs: providers.requestTimeoutNonStreamingMs,
       websiteUrl: providers.websiteUrl,
       faviconUrl: providers.faviconUrl,
+      cacheTtlPreference: providers.cacheTtlPreference,
       tpm: providers.tpm,
       rpm: providers.rpm,
       rpd: providers.rpd,
@@ -261,6 +265,7 @@ export async function findProviderById(id: number): Promise<Provider | null> {
       requestTimeoutNonStreamingMs: providers.requestTimeoutNonStreamingMs,
       websiteUrl: providers.websiteUrl,
       faviconUrl: providers.faviconUrl,
+      cacheTtlPreference: providers.cacheTtlPreference,
       tpm: providers.tpm,
       rpm: providers.rpm,
       rpd: providers.rpd,
@@ -348,6 +353,8 @@ export async function updateProvider(
     dbData.requestTimeoutNonStreamingMs = providerData.request_timeout_non_streaming_ms;
   if (providerData.website_url !== undefined) dbData.websiteUrl = providerData.website_url;
   if (providerData.favicon_url !== undefined) dbData.faviconUrl = providerData.favicon_url;
+  if (providerData.cache_ttl_preference !== undefined)
+    dbData.cacheTtlPreference = providerData.cache_ttl_preference ?? null;
   if (providerData.tpm !== undefined) dbData.tpm = providerData.tpm;
   if (providerData.rpm !== undefined) dbData.rpm = providerData.rpm;
   if (providerData.rpd !== undefined) dbData.rpd = providerData.rpd;
@@ -392,6 +399,7 @@ export async function updateProvider(
       requestTimeoutNonStreamingMs: providers.requestTimeoutNonStreamingMs,
       websiteUrl: providers.websiteUrl,
       faviconUrl: providers.faviconUrl,
+      cacheTtlPreference: providers.cacheTtlPreference,
       tpm: providers.tpm,
       rpm: providers.rpm,
       rpd: providers.rpd,

+ 15 - 0
src/repository/usage-logs.ts

@@ -39,6 +39,9 @@ export interface UsageLogRow {
   outputTokens: number | null;
   cacheCreationInputTokens: number | null;
   cacheReadInputTokens: number | null;
+  cacheCreation5mInputTokens: number | null;
+  cacheCreation1hInputTokens: number | null;
+  cacheTtlApplied: string | null;
   totalTokens: number;
   costUsd: string | null;
   costMultiplier: string | null; // 供应商倍率
@@ -59,6 +62,8 @@ export interface UsageLogSummary {
   totalOutputTokens: number;
   totalCacheCreationTokens: number;
   totalCacheReadTokens: number;
+  totalCacheCreation5mTokens: number;
+  totalCacheCreation1hTokens: number;
 }
 
 export interface UsageLogsResult {
@@ -195,6 +200,8 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
       totalOutputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens})::double precision, 0::double precision)`,
       totalCacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens})::double precision, 0::double precision)`,
       totalCacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens})::double precision, 0::double precision)`,
+      totalCacheCreation5mTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreation5mInputTokens})::double precision, 0::double precision)`,
+      totalCacheCreation1hTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreation1hInputTokens})::double precision, 0::double precision)`,
     })
     .from(messageRequest)
     .where(and(...conditions));
@@ -225,6 +232,9 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
       outputTokens: messageRequest.outputTokens,
       cacheCreationInputTokens: messageRequest.cacheCreationInputTokens,
       cacheReadInputTokens: messageRequest.cacheReadInputTokens,
+      cacheCreation5mInputTokens: messageRequest.cacheCreation5mInputTokens,
+      cacheCreation1hInputTokens: messageRequest.cacheCreation1hInputTokens,
+      cacheTtlApplied: messageRequest.cacheTtlApplied,
       costUsd: messageRequest.costUsd,
       costMultiplier: messageRequest.costMultiplier, // 供应商倍率
       durationMs: messageRequest.durationMs,
@@ -254,6 +264,9 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
     return {
       ...row,
       totalTokens: totalRowTokens,
+      cacheCreation5mInputTokens: row.cacheCreation5mInputTokens,
+      cacheCreation1hInputTokens: row.cacheCreation1hInputTokens,
+      cacheTtlApplied: row.cacheTtlApplied,
       costUsd: row.costUsd?.toString() ?? null,
       providerChain: row.providerChain as ProviderChainItem[] | null,
       endpoint: row.endpoint,
@@ -271,6 +284,8 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
       totalOutputTokens: summaryResult?.totalOutputTokens ?? 0,
       totalCacheCreationTokens: summaryResult?.totalCacheCreationTokens ?? 0,
       totalCacheReadTokens: summaryResult?.totalCacheReadTokens ?? 0,
+      totalCacheCreation5mTokens: summaryResult?.totalCacheCreation5mTokens ?? 0,
+      totalCacheCreation1hTokens: summaryResult?.totalCacheCreation1hTokens ?? 0,
     },
   };
 }

+ 5 - 0
src/types/cache.ts

@@ -0,0 +1,5 @@
+export type CacheTtlPreference = "inherit" | "5m" | "1h";
+
+export type CacheTtlResolved = Exclude<CacheTtlPreference, "inherit">;
+
+export type CacheTtlApplied = CacheTtlResolved | "mixed";

+ 11 - 0
src/types/key.ts

@@ -1,3 +1,5 @@
+import type { CacheTtlPreference } from "./cache";
+
 /**
  * 密钥数据库实体类型
  */
@@ -25,6 +27,9 @@ export interface Key {
   // Provider group override (null = inherit from user)
   providerGroup: string | null;
 
+  // Cache TTL override (inherit -> follow provider/client)
+  cacheTtlPreference: CacheTtlPreference | null;
+
   createdAt: Date;
   updatedAt: Date;
   deletedAt?: Date;
@@ -52,6 +57,9 @@ export interface CreateKeyData {
   limit_concurrent_sessions?: number;
   // Provider group override (null = inherit from user)
   provider_group?: string | null;
+
+  // Cache TTL override
+  cache_ttl_preference?: CacheTtlPreference;
 }
 
 /**
@@ -74,4 +82,7 @@ export interface UpdateKeyData {
   limit_concurrent_sessions?: number;
   // Provider group override (null = inherit from user)
   provider_group?: string | null;
+
+  // Cache TTL override
+  cache_ttl_preference?: CacheTtlPreference;
 }

+ 7 - 0
src/types/message.ts

@@ -1,4 +1,5 @@
 import type { Numeric } from "decimal.js-light";
+import type { CacheTtlApplied } from "./cache";
 
 /**
  * 供应商信息(用于决策链)
@@ -179,6 +180,9 @@ export interface MessageRequest {
   outputTokens?: number;
   cacheCreationInputTokens?: number;
   cacheReadInputTokens?: number;
+  cacheCreation5mInputTokens?: number;
+  cacheCreation1hInputTokens?: number;
+  cacheTtlApplied?: CacheTtlApplied | null;
 
   // 错误信息
   errorMessage?: string;
@@ -228,6 +232,9 @@ export interface CreateMessageRequestData {
   output_tokens?: number;
   cache_creation_input_tokens?: number;
   cache_read_input_tokens?: number;
+  cache_creation_5m_input_tokens?: number;
+  cache_creation_1h_input_tokens?: number;
+  cache_ttl_applied?: CacheTtlApplied | null;
 
   // 错误信息
   error_message?: string;

+ 8 - 0
src/types/provider.ts

@@ -1,4 +1,6 @@
 // 供应商类型枚举
+import type { CacheTtlPreference } from "./cache";
+
 export type ProviderType =
   | "claude"
   | "claude-auth"
@@ -85,6 +87,9 @@ export interface Provider {
   websiteUrl: string | null;
   faviconUrl: string | null;
 
+  // Cache TTL override(inherit 表示不强制覆写)
+  cacheTtlPreference: CacheTtlPreference | null;
+
   // 废弃(保留向后兼容,但不再使用)
   // TPM (Tokens Per Minute): 每分钟可处理的文本总量
   tpm: number | null;
@@ -148,6 +153,7 @@ export interface ProviderDisplay {
   // 供应商官网地址
   websiteUrl: string | null;
   faviconUrl: string | null;
+  cacheTtlPreference: CacheTtlPreference | null;
   // 废弃字段(保留向后兼容)
   tpm: number | null;
   rpm: number | null;
@@ -212,6 +218,7 @@ export interface CreateProviderData {
   // 供应商官网地址
   website_url?: string | null;
   favicon_url?: string | null;
+  cache_ttl_preference?: CacheTtlPreference;
 
   // 废弃字段(保留向后兼容)
   // TPM (Tokens Per Minute): 每分钟可处理的文本总量
@@ -274,6 +281,7 @@ export interface UpdateProviderData {
   // 供应商官网地址
   website_url?: string | null;
   favicon_url?: string | null;
+  cache_ttl_preference?: CacheTtlPreference;
 
   // 废弃字段(保留向后兼容)
   // TPM (Tokens Per Minute): 每分钟可处理的文本总量