From f3aace7b60d006540a7cf0e79923c696e55bd15f Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 23:57:53 +0300 Subject: [PATCH 01/11] feat(provider): add swap cache TTL billing option to invert 1h/5min cost calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enabled on a provider, swaps the 5m and 1h token buckets for cost calculation only — the log badge remains unchanged. This addresses cases where a provider reports 1h cache but actually bills at the 5min rate. Co-Authored-By: Claude Opus 4.6 --- drizzle/0069_special_squirrel_girl.sql | 1 + drizzle/meta/0069_snapshot.json | 3244 +++++++++++++++++ drizzle/meta/_journal.json | 7 + .../en/settings/providers/form/sections.json | 4 + .../ja/settings/providers/form/sections.json | 4 + .../ru/settings/providers/form/sections.json | 4 + .../settings/providers/form/sections.json | 4 + .../settings/providers/form/sections.json | 4 + src/actions/providers.ts | 1 + .../_components/forms/provider-form/index.tsx | 1 + .../provider-form/provider-form-context.tsx | 3 + .../provider-form/provider-form-types.ts | 2 + .../sections/routing-section.tsx | 15 + src/app/v1/_lib/proxy/response-handler.ts | 10 +- src/drizzle/schema.ts | 3 + src/lib/validation/schemas.ts | 2 + src/repository/_shared/transformers.ts | 1 + src/repository/provider.ts | 8 + src/types/provider.ts | 6 + .../cost-calculation-swap-cache-ttl.test.ts | 129 + 20 files changed, 3451 insertions(+), 2 deletions(-) create mode 100644 drizzle/0069_special_squirrel_girl.sql create mode 100644 drizzle/meta/0069_snapshot.json create mode 100644 tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts diff --git a/drizzle/0069_special_squirrel_girl.sql b/drizzle/0069_special_squirrel_girl.sql new file mode 100644 index 000000000..4e56dbb7c --- /dev/null +++ b/drizzle/0069_special_squirrel_girl.sql @@ -0,0 +1 @@ +ALTER TABLE "providers" ADD COLUMN "swap_cache_ttl_billing" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0069_snapshot.json b/drizzle/meta/0069_snapshot.json new file mode 100644 index 000000000..02001f725 --- /dev/null +++ b/drizzle/meta/0069_snapshot.json @@ -0,0 +1,3244 @@ +{ + "id": "b80dfcb9-be78-4c4f-a483-842cea7cefb4", + "prevId": "81847b3d-5ce4-4fb0-bad1-9e4570f3c5fb", + "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 with time zone", + "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(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "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_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "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 + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "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": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "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_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "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_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "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_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "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 + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'litellm'" + }, + "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": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "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 + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "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.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "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": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "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()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_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.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 + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "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 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "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'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "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 + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "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_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "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": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "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 + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "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()" + } + }, + "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": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "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'" + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "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()" + } + }, + "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 + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "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 + }, + "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'" + }, + "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 + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "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 + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6a74178de..861ccd714 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -484,6 +484,13 @@ "when": 1771164248361, "tag": "0068_flaky_swarm", "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1771188568762, + "tag": "0069_special_squirrel_girl", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index becef1d2b..5e582c3d4 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -173,6 +173,10 @@ "inherit": "No override (follow client)" } }, + "swapCacheTtlBilling": { + "label": "Swap Cache TTL Billing", + "desc": "Invert cache TTL for cost calculation: 1h tokens billed as 5min rate and vice versa. Display badge remains unchanged." + }, "codexOverrides": { "title": "Codex Parameter Overrides", "desc": "Override Codex (Responses API) request parameters at the provider level", diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 6cf30359e..5a85d1412 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -174,6 +174,10 @@ "inherit": "オーバーライドしない(クライアントに従う)" } }, + "swapCacheTtlBilling": { + "label": "キャッシュTTL課金スワップ", + "desc": "コスト計算でキャッシュTTLを反転:1hトークンを5分レートで課金し、その逆も同様。ログのバッジは変更されません。" + }, "codexOverrides": { "title": "Codex パラメータオーバーライド", "desc": "プロバイダーレベルで Codex (Responses API) リクエストパラメータをオーバーライド", diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index 4f651411f..4d3bd254c 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -174,6 +174,10 @@ "inherit": "Не переопределять (следовать клиенту)" } }, + "swapCacheTtlBilling": { + "label": "Инверсия тарификации Cache TTL", + "desc": "Инвертировать TTL кэша при расчете стоимости: токены 1h тарифицируются по ставке 5 мин и наоборот. Бейдж в логах остается без изменений." + }, "codexOverrides": { "title": "Переопределение параметров Codex", "desc": "Переопределение параметров запросов Codex (Responses API) на уровне провайдера", diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index b9694b837..b101467a3 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -87,6 +87,10 @@ }, "desc": "强制设置 prompt cache TTL;仅影响包含 cache_control 的请求。" }, + "swapCacheTtlBilling": { + "label": "Cache TTL 计费互换", + "desc": "反转缓存 TTL 的计费方式:1h 令牌按 5 分钟费率计费,反之亦然。日志中的标记保持不变。" + }, "context1m": { "label": "1M 上下文窗口", "options": { diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index 62e56b9a0..a965a7d24 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -174,6 +174,10 @@ "inherit": "不覆寫(跟隨客戶端)" } }, + "swapCacheTtlBilling": { + "label": "Cache TTL 計費互換", + "desc": "反轉快取 TTL 的計費方式:1h 令牌按 5 分鐘費率計費,反之亦然。日誌中的標記保持不變。" + }, "codexOverrides": { "title": "Codex 參數覆寫", "desc": "在供應商級別覆寫 Codex (Responses API) 請求參數", diff --git a/src/actions/providers.ts b/src/actions/providers.ts index e056df2a0..9c76ba3ad 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -277,6 +277,7 @@ export async function getProviders(): Promise { websiteUrl: provider.websiteUrl, faviconUrl: provider.faviconUrl, cacheTtlPreference: provider.cacheTtlPreference, + swapCacheTtlBilling: provider.swapCacheTtlBilling, context1mPreference: provider.context1mPreference, codexReasoningEffortPreference: provider.codexReasoningEffortPreference, codexReasoningSummaryPreference: provider.codexReasoningSummaryPreference, diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx index cc6eaa87b..2942d5be3 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx @@ -319,6 +319,7 @@ function ProviderFormContent({ cost_multiplier: state.routing.costMultiplier, group_tag: state.routing.groupTag.length > 0 ? state.routing.groupTag.join(",") : null, cache_ttl_preference: state.routing.cacheTtlPreference, + swap_cache_ttl_billing: state.routing.swapCacheTtlBilling, context_1m_preference: state.routing.context1mPreference, codex_reasoning_effort_preference: state.routing.codexReasoningEffortPreference, codex_reasoning_summary_preference: state.routing.codexReasoningSummaryPreference, diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx index 9f9c890a2..facc525c9 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx @@ -52,6 +52,7 @@ export function createInitialState( weight: sourceProvider?.weight ?? 1, costMultiplier: sourceProvider?.costMultiplier ?? 1.0, cacheTtlPreference: sourceProvider?.cacheTtlPreference ?? "inherit", + swapCacheTtlBilling: sourceProvider?.swapCacheTtlBilling ?? false, context1mPreference: (sourceProvider?.context1mPreference as "inherit" | "force_enable" | "disabled") ?? "inherit", @@ -152,6 +153,8 @@ export function providerFormReducer( return { ...state, routing: { ...state.routing, costMultiplier: action.payload } }; case "SET_CACHE_TTL_PREFERENCE": return { ...state, routing: { ...state.routing, cacheTtlPreference: action.payload } }; + case "SET_SWAP_CACHE_TTL_BILLING": + return { ...state, routing: { ...state.routing, swapCacheTtlBilling: action.payload } }; case "SET_CONTEXT_1M_PREFERENCE": return { ...state, routing: { ...state.routing, context1mPreference: action.payload } }; case "SET_CODEX_REASONING_EFFORT": diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts index e4bf0eae6..60355dd9e 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts @@ -47,6 +47,7 @@ export interface RoutingState { weight: number; costMultiplier: number; cacheTtlPreference: "inherit" | "5m" | "1h"; + swapCacheTtlBilling: boolean; context1mPreference: "inherit" | "force_enable" | "disabled"; // Codex-specific codexReasoningEffortPreference: CodexReasoningEffortPreference; @@ -127,6 +128,7 @@ export type ProviderFormAction = | { type: "SET_WEIGHT"; payload: number } | { type: "SET_COST_MULTIPLIER"; payload: number } | { type: "SET_CACHE_TTL_PREFERENCE"; payload: "inherit" | "5m" | "1h" } + | { type: "SET_SWAP_CACHE_TTL_BILLING"; payload: boolean } | { type: "SET_CONTEXT_1M_PREFERENCE"; payload: "inherit" | "force_enable" | "disabled" } | { type: "SET_CODEX_REASONING_EFFORT"; payload: CodexReasoningEffortPreference } | { type: "SET_CODEX_REASONING_SUMMARY"; payload: CodexReasoningSummaryPreference } diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx index e9c399c0f..5e43c4d65 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx @@ -370,6 +370,21 @@ export function RoutingSection() { + {/* Swap Cache TTL Billing */} + + + dispatch({ type: "SET_SWAP_CACHE_TTL_BILLING", payload: checked }) + } + disabled={state.ui.isPending} + /> + + {/* 1M Context Window - Claude type only */} {state.routing.providerType === "claude" && ( 1h token buckets for billing when provider option is enabled + // Badge (cache_ttl) stays unchanged - only cost calculation is affected + const swap = provider.swapCacheTtlBilling === true; + const billing5m = swap ? cache1h : cache5m; + const billing1h = swap ? cache5m : cache1h; + const normalizedUsage: UsageMetrics = { ...usageMetrics, cache_ttl: resolvedCacheTtl ?? usageMetrics.cache_ttl, - cache_creation_5m_input_tokens: cache5m, - cache_creation_1h_input_tokens: cache1h, + cache_creation_5m_input_tokens: billing5m, + cache_creation_1h_input_tokens: billing1h, cache_creation_input_tokens: cacheTotal, }; diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index ce9165886..65deea818 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -268,6 +268,9 @@ export const providers = pgTable('providers', { // Cache TTL override(null = 不覆写,沿用客户端请求) cacheTtlPreference: varchar('cache_ttl_preference', { length: 10 }), + // Cache TTL billing swap: when true, invert 1h<->5m for cost calculation only + swapCacheTtlBilling: boolean('swap_cache_ttl_billing').notNull().default(false), + // 1M Context Window 偏好配置(仅对 Anthropic 类型供应商有效) // - 'inherit' (默认): 遵循客户端请求,客户端带 1M header 则启用 // - 'force_enable': 强制启用 1M 上下文(仅对支持的模型生效) diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 18cd87244..ebfbf1321 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -490,6 +490,7 @@ export const CreateProviderSchema = z .optional() .default(0), cache_ttl_preference: CACHE_TTL_PREFERENCE.optional().default("inherit"), + swap_cache_ttl_billing: z.boolean().optional().default(false), context_1m_preference: CONTEXT_1M_PREFERENCE.nullable().optional(), codex_reasoning_effort_preference: CODEX_REASONING_EFFORT_PREFERENCE.optional().default("inherit"), @@ -693,6 +694,7 @@ export const UpdateProviderSchema = z .max(1000, "并发Session上限不能超过1000") .optional(), cache_ttl_preference: CACHE_TTL_PREFERENCE.optional(), + swap_cache_ttl_billing: z.boolean().optional(), context_1m_preference: CONTEXT_1M_PREFERENCE.nullable().optional(), codex_reasoning_effort_preference: CODEX_REASONING_EFFORT_PREFERENCE.optional(), codex_reasoning_summary_preference: CODEX_REASONING_SUMMARY_PREFERENCE.optional(), diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 7f9edb56b..ef9d97e6c 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -119,6 +119,7 @@ export function toProvider(dbProvider: any): Provider { websiteUrl: dbProvider?.websiteUrl ?? null, faviconUrl: dbProvider?.faviconUrl ?? null, cacheTtlPreference: dbProvider?.cacheTtlPreference ?? null, + swapCacheTtlBilling: dbProvider?.swapCacheTtlBilling ?? false, context1mPreference: dbProvider?.context1mPreference ?? null, codexReasoningEffortPreference: dbProvider?.codexReasoningEffortPreference ?? null, codexReasoningSummaryPreference: dbProvider?.codexReasoningSummaryPreference ?? null, diff --git a/src/repository/provider.ts b/src/repository/provider.ts index be6c7f4b1..4d6b24fad 100644 --- a/src/repository/provider.ts +++ b/src/repository/provider.ts @@ -59,6 +59,7 @@ export async function createProvider(providerData: CreateProviderData): Promise< websiteUrl: providerData.website_url ?? null, faviconUrl: providerData.favicon_url ?? null, cacheTtlPreference: providerData.cache_ttl_preference ?? null, + swapCacheTtlBilling: providerData.swap_cache_ttl_billing ?? false, context1mPreference: providerData.context_1m_preference ?? null, codexReasoningEffortPreference: providerData.codex_reasoning_effort_preference ?? null, codexReasoningSummaryPreference: providerData.codex_reasoning_summary_preference ?? null, @@ -129,6 +130,7 @@ export async function createProvider(providerData: CreateProviderData): Promise< websiteUrl: providers.websiteUrl, faviconUrl: providers.faviconUrl, cacheTtlPreference: providers.cacheTtlPreference, + swapCacheTtlBilling: providers.swapCacheTtlBilling, context1mPreference: providers.context1mPreference, codexReasoningEffortPreference: providers.codexReasoningEffortPreference, codexReasoningSummaryPreference: providers.codexReasoningSummaryPreference, @@ -208,6 +210,7 @@ export async function findProviderList( websiteUrl: providers.websiteUrl, faviconUrl: providers.faviconUrl, cacheTtlPreference: providers.cacheTtlPreference, + swapCacheTtlBilling: providers.swapCacheTtlBilling, context1mPreference: providers.context1mPreference, codexReasoningEffortPreference: providers.codexReasoningEffortPreference, codexReasoningSummaryPreference: providers.codexReasoningSummaryPreference, @@ -287,6 +290,7 @@ export async function findAllProvidersFresh(): Promise { websiteUrl: providers.websiteUrl, faviconUrl: providers.faviconUrl, cacheTtlPreference: providers.cacheTtlPreference, + swapCacheTtlBilling: providers.swapCacheTtlBilling, context1mPreference: providers.context1mPreference, codexReasoningEffortPreference: providers.codexReasoningEffortPreference, codexReasoningSummaryPreference: providers.codexReasoningSummaryPreference, @@ -370,6 +374,7 @@ export async function findProviderById(id: number): Promise { websiteUrl: providers.websiteUrl, faviconUrl: providers.faviconUrl, cacheTtlPreference: providers.cacheTtlPreference, + swapCacheTtlBilling: providers.swapCacheTtlBilling, context1mPreference: providers.context1mPreference, codexReasoningEffortPreference: providers.codexReasoningEffortPreference, codexReasoningSummaryPreference: providers.codexReasoningSummaryPreference, @@ -471,6 +476,8 @@ export async function updateProvider( 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.swap_cache_ttl_billing !== undefined) + dbData.swapCacheTtlBilling = providerData.swap_cache_ttl_billing; if (providerData.context_1m_preference !== undefined) dbData.context1mPreference = providerData.context_1m_preference ?? null; if (providerData.codex_reasoning_effort_preference !== undefined) @@ -590,6 +597,7 @@ export async function updateProvider( websiteUrl: providers.websiteUrl, faviconUrl: providers.faviconUrl, cacheTtlPreference: providers.cacheTtlPreference, + swapCacheTtlBilling: providers.swapCacheTtlBilling, context1mPreference: providers.context1mPreference, codexReasoningEffortPreference: providers.codexReasoningEffortPreference, codexReasoningSummaryPreference: providers.codexReasoningSummaryPreference, diff --git a/src/types/provider.ts b/src/types/provider.ts index 3ab55cacf..aed85a685 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -131,6 +131,9 @@ export interface Provider { // Cache TTL override(inherit 表示不强制覆写) cacheTtlPreference: CacheTtlPreference | null; + // Cache TTL billing swap: invert 1h<->5m for cost calculation + swapCacheTtlBilling: boolean; + // 1M Context Window 偏好配置(仅对 Anthropic 类型供应商有效) context1mPreference: Context1mPreference | null; @@ -214,6 +217,7 @@ export interface ProviderDisplay { websiteUrl: string | null; faviconUrl: string | null; cacheTtlPreference: CacheTtlPreference | null; + swapCacheTtlBilling: boolean; context1mPreference: Context1mPreference | null; codexReasoningEffortPreference: CodexReasoningEffortPreference | null; codexReasoningSummaryPreference: CodexReasoningSummaryPreference | null; @@ -305,6 +309,7 @@ export interface CreateProviderData { website_url?: string | null; favicon_url?: string | null; cache_ttl_preference?: CacheTtlPreference | null; + swap_cache_ttl_billing?: boolean; context_1m_preference?: Context1mPreference | null; codex_reasoning_effort_preference?: CodexReasoningEffortPreference | null; codex_reasoning_summary_preference?: CodexReasoningSummaryPreference | null; @@ -378,6 +383,7 @@ export interface UpdateProviderData { website_url?: string | null; favicon_url?: string | null; cache_ttl_preference?: CacheTtlPreference | null; + swap_cache_ttl_billing?: boolean; context_1m_preference?: Context1mPreference | null; codex_reasoning_effort_preference?: CodexReasoningEffortPreference | null; codex_reasoning_summary_preference?: CodexReasoningSummaryPreference | null; diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts new file mode 100644 index 000000000..3c7f372ad --- /dev/null +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, test } from "vitest"; +import { calculateRequestCostBreakdown } from "@/lib/utils/cost-calculation"; +import type { ModelPriceData } from "@/types/model-price"; + +function makePriceData(overrides: Partial = {}): ModelPriceData { + return { + input_cost_per_token: 0.000003, // $3/MTok + output_cost_per_token: 0.000015, // $15/MTok + cache_creation_input_token_cost: 0.00000375, // 1.25x input (5m rate) + cache_read_input_token_cost: 0.0000003, // 0.1x input + cache_creation_input_token_cost_above_1hr: 0.000006, // 2x input (1h rate) + ...overrides, + }; +} + +describe("swap cache TTL billing", () => { + // Simulates the swap logic from response-handler.ts: + // When provider.swapCacheTtlBilling is true, the 5m and 1h token counts + // are exchanged before cost calculation, while the badge remains unchanged. + function applySwap( + usage: { cache_creation_5m_input_tokens?: number; cache_creation_1h_input_tokens?: number }, + swap: boolean + ) { + const billing5m = swap ? usage.cache_creation_1h_input_tokens : usage.cache_creation_5m_input_tokens; + const billing1h = swap ? usage.cache_creation_5m_input_tokens : usage.cache_creation_1h_input_tokens; + return { + cache_creation_5m_input_tokens: billing5m, + cache_creation_1h_input_tokens: billing1h, + }; + } + + test("swap=false: normal billing (5m tokens at 5m rate, 1h tokens at 1h rate)", () => { + const tokens = { cache_creation_5m_input_tokens: 1000, cache_creation_1h_input_tokens: 0 }; + const swapped = applySwap(tokens, false); + + const result = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...swapped }, + makePriceData() + ); + + // 1000 * 0.00000375 (5m rate) + expect(result.cache_creation).toBeCloseTo(0.00375, 6); + }); + + test("swap=true: 1h tokens billed at 5m rate (cheaper)", () => { + // Provider reports 1h, but actually bills at 5m rate + const tokens = { cache_creation_5m_input_tokens: 0, cache_creation_1h_input_tokens: 1000 }; + const swapped = applySwap(tokens, true); + + const result = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...swapped }, + makePriceData() + ); + + // After swap: 1h tokens (1000) moved to 5m bucket -> 1000 * 0.00000375 + expect(result.cache_creation).toBeCloseTo(0.00375, 6); + }); + + test("swap=true: 5m tokens billed at 1h rate (more expensive)", () => { + // Provider reports 5m, but actually bills at 1h rate + const tokens = { cache_creation_5m_input_tokens: 1000, cache_creation_1h_input_tokens: 0 }; + const swapped = applySwap(tokens, true); + + const result = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...swapped }, + makePriceData() + ); + + // After swap: 5m tokens (1000) moved to 1h bucket -> 1000 * 0.000006 + expect(result.cache_creation).toBeCloseTo(0.006, 6); + }); + + test("swap inverts both buckets when both have tokens", () => { + const tokens = { cache_creation_5m_input_tokens: 200, cache_creation_1h_input_tokens: 800 }; + + const normalResult = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...applySwap(tokens, false) }, + makePriceData() + ); + + const swappedResult = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...applySwap(tokens, true) }, + makePriceData() + ); + + // Normal: 200 * 0.00000375 + 800 * 0.000006 = 0.00075 + 0.0048 = 0.00555 + expect(normalResult.cache_creation).toBeCloseTo(0.00555, 6); + + // Swapped: 800 * 0.00000375 + 200 * 0.000006 = 0.003 + 0.0012 = 0.0042 + expect(swappedResult.cache_creation).toBeCloseTo(0.0042, 6); + + // Swapped is cheaper because more tokens went to the cheaper 5m rate + expect(swappedResult.cache_creation).toBeLessThan(normalResult.cache_creation); + }); + + test("swap has no effect when only one bucket has tokens and other is zero", () => { + const tokens5mOnly = { cache_creation_5m_input_tokens: 500, cache_creation_1h_input_tokens: 0 }; + const tokens1hOnly = { cache_creation_5m_input_tokens: 0, cache_creation_1h_input_tokens: 500 }; + + // 5m-only normal = 5m-only swapped (0 goes to 1h, 500 stays) + // Actually no - swap exchanges: 5m=0, 1h=500 becomes 5m=500, 1h=0 + const normal5m = applySwap(tokens5mOnly, false); + const swapped5m = applySwap(tokens5mOnly, true); + + // Normal: 500 at 5m rate + expect(normal5m.cache_creation_5m_input_tokens).toBe(500); + expect(normal5m.cache_creation_1h_input_tokens).toBe(0); + + // Swapped: 500 at 1h rate, 0 at 5m rate + expect(swapped5m.cache_creation_5m_input_tokens).toBe(0); + expect(swapped5m.cache_creation_1h_input_tokens).toBe(500); + }); + + test("swap with undefined tokens treats them as undefined (no crash)", () => { + const tokens = { cache_creation_5m_input_tokens: undefined, cache_creation_1h_input_tokens: 1000 }; + const swapped = applySwap(tokens, true); + + expect(swapped.cache_creation_5m_input_tokens).toBe(1000); + expect(swapped.cache_creation_1h_input_tokens).toBeUndefined(); + + // Should not crash when passed to cost calculation + const result = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, ...swapped }, + makePriceData() + ); + // 1000 at 5m rate + expect(result.cache_creation).toBeCloseTo(0.00375, 6); + }); +}); From d65bcd601174a9ce05bc2f2935e5548a7527e411 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 02:51:54 +0300 Subject: [PATCH 02/11] fix(provider): swap cache TTL at data entry so badge/cost/metrics are consistent Move swap logic from post-resolution (billing5m/billing1h) to data entry point, inverting both bucket values and cache_ttl before downstream processing. Also swap session fallback TTL when usageMetrics.cache_ttl is absent. Update i18n descriptions and add tests for new behavior. Co-Authored-By: Claude Opus 4.6 --- .../en/settings/providers/form/sections.json | 2 +- .../ja/settings/providers/form/sections.json | 2 +- .../ru/settings/providers/form/sections.json | 2 +- .../settings/providers/form/sections.json | 2 +- .../settings/providers/form/sections.json | 2 +- src/app/v1/_lib/proxy/response-handler.ts | 29 +++++--- .../cost-calculation-swap-cache-ttl.test.ts | 73 ++++++++++++++++--- 7 files changed, 86 insertions(+), 26 deletions(-) diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index 5e582c3d4..135d192bc 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -175,7 +175,7 @@ }, "swapCacheTtlBilling": { "label": "Swap Cache TTL Billing", - "desc": "Invert cache TTL for cost calculation: 1h tokens billed as 5min rate and vice versa. Display badge remains unchanged." + "desc": "Invert cache TTL for incoming data: 1h tokens treated as 5min and vice versa. Affects badge, cost, and all stored metrics." }, "codexOverrides": { "title": "Codex Parameter Overrides", diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 5a85d1412..1356c87e4 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -176,7 +176,7 @@ }, "swapCacheTtlBilling": { "label": "キャッシュTTL課金スワップ", - "desc": "コスト計算でキャッシュTTLを反転:1hトークンを5分レートで課金し、その逆も同様。ログのバッジは変更されません。" + "desc": "受信データのキャッシュTTLを反転:1hトークンを5分として扱い、その逆も同様。バッジ、コスト、保存メトリクスすべてに影響します。" }, "codexOverrides": { "title": "Codex パラメータオーバーライド", diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index 4d3bd254c..b59ec1a4f 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -176,7 +176,7 @@ }, "swapCacheTtlBilling": { "label": "Инверсия тарификации Cache TTL", - "desc": "Инвертировать TTL кэша при расчете стоимости: токены 1h тарифицируются по ставке 5 мин и наоборот. Бейдж в логах остается без изменений." + "desc": "Инвертировать TTL кэша на входе: токены 1h обрабатываются как 5 мин и наоборот. Влияет на бейдж, стоимость и все сохраняемые метрики." }, "codexOverrides": { "title": "Переопределение параметров Codex", diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index b101467a3..3e54ef4ab 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -89,7 +89,7 @@ }, "swapCacheTtlBilling": { "label": "Cache TTL 计费互换", - "desc": "反转缓存 TTL 的计费方式:1h 令牌按 5 分钟费率计费,反之亦然。日志中的标记保持不变。" + "desc": "反转传入数据的缓存 TTL:1h 令牌视为 5 分钟,反之亦然。影响标记、成本及所有存储指标。" }, "context1m": { "label": "1M 上下文窗口", diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index a965a7d24..c007c3941 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -176,7 +176,7 @@ }, "swapCacheTtlBilling": { "label": "Cache TTL 計費互換", - "desc": "反轉快取 TTL 的計費方式:1h 令牌按 5 分鐘費率計費,反之亦然。日誌中的標記保持不變。" + "desc": "反轉傳入資料的快取 TTL:1h 令牌視為 5 分鐘,反之亦然。影響標記、成本及所有儲存指標。" }, "codexOverrides": { "title": "Codex 參數覆寫", diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index f43ad5a8a..220ef523c 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -2821,7 +2821,24 @@ export async function finalizeRequestStats( } // 4. 更新成本 - const resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; + // Invert cache TTL at data entry when provider option is enabled + // All downstream (badge, cost, DB, logs) will see inverted values + if (provider.swapCacheTtlBilling) { + const orig5m = usageMetrics.cache_creation_5m_input_tokens; + usageMetrics.cache_creation_5m_input_tokens = usageMetrics.cache_creation_1h_input_tokens; + usageMetrics.cache_creation_1h_input_tokens = orig5m; + if (usageMetrics.cache_ttl === "5m") usageMetrics.cache_ttl = "1h"; + else if (usageMetrics.cache_ttl === "1h") usageMetrics.cache_ttl = "5m"; + } + + let resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; + + // When usageMetrics.cache_ttl is absent, session fallback wasn't swapped - handle it + if (provider.swapCacheTtlBilling && !usageMetrics.cache_ttl) { + if (resolvedCacheTtl === "5m") resolvedCacheTtl = "1h"; + else if (resolvedCacheTtl === "1h") resolvedCacheTtl = "5m"; + } + const cache5m = usageMetrics.cache_creation_5m_input_tokens ?? (resolvedCacheTtl === "1h" ? undefined : usageMetrics.cache_creation_input_tokens); @@ -2831,17 +2848,11 @@ export async function finalizeRequestStats( const cacheTotal = usageMetrics.cache_creation_input_tokens ?? ((cache5m ?? 0) + (cache1h ?? 0) || undefined); - // Swap 5m<->1h token buckets for billing when provider option is enabled - // Badge (cache_ttl) stays unchanged - only cost calculation is affected - const swap = provider.swapCacheTtlBilling === true; - const billing5m = swap ? cache1h : cache5m; - const billing1h = swap ? cache5m : cache1h; - const normalizedUsage: UsageMetrics = { ...usageMetrics, cache_ttl: resolvedCacheTtl ?? usageMetrics.cache_ttl, - cache_creation_5m_input_tokens: billing5m, - cache_creation_1h_input_tokens: billing1h, + cache_creation_5m_input_tokens: cache5m, + cache_creation_1h_input_tokens: cache1h, cache_creation_input_tokens: cacheTotal, }; diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts index 3c7f372ad..7513167c0 100644 --- a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -15,17 +15,30 @@ function makePriceData(overrides: Partial = {}): ModelPriceData describe("swap cache TTL billing", () => { // Simulates the swap logic from response-handler.ts: - // When provider.swapCacheTtlBilling is true, the 5m and 1h token counts - // are exchanged before cost calculation, while the badge remains unchanged. + // When provider.swapCacheTtlBilling is true, bucket values AND cache_ttl + // are swapped at data entry so all downstream sees inverted values. function applySwap( - usage: { cache_creation_5m_input_tokens?: number; cache_creation_1h_input_tokens?: number }, + usage: { + cache_creation_5m_input_tokens?: number; + cache_creation_1h_input_tokens?: number; + cache_ttl?: "5m" | "1h"; + }, swap: boolean ) { - const billing5m = swap ? usage.cache_creation_1h_input_tokens : usage.cache_creation_5m_input_tokens; - const billing1h = swap ? usage.cache_creation_5m_input_tokens : usage.cache_creation_1h_input_tokens; + if (!swap) { + return { + cache_creation_5m_input_tokens: usage.cache_creation_5m_input_tokens, + cache_creation_1h_input_tokens: usage.cache_creation_1h_input_tokens, + cache_ttl: usage.cache_ttl, + }; + } + let swappedTtl = usage.cache_ttl; + if (swappedTtl === "5m") swappedTtl = "1h"; + else if (swappedTtl === "1h") swappedTtl = "5m"; return { - cache_creation_5m_input_tokens: billing5m, - cache_creation_1h_input_tokens: billing1h, + cache_creation_5m_input_tokens: usage.cache_creation_1h_input_tokens, + cache_creation_1h_input_tokens: usage.cache_creation_5m_input_tokens, + cache_ttl: swappedTtl, }; } @@ -93,12 +106,9 @@ describe("swap cache TTL billing", () => { expect(swappedResult.cache_creation).toBeLessThan(normalResult.cache_creation); }); - test("swap has no effect when only one bucket has tokens and other is zero", () => { + test("swap exchanges buckets when only one bucket has tokens", () => { const tokens5mOnly = { cache_creation_5m_input_tokens: 500, cache_creation_1h_input_tokens: 0 }; - const tokens1hOnly = { cache_creation_5m_input_tokens: 0, cache_creation_1h_input_tokens: 500 }; - // 5m-only normal = 5m-only swapped (0 goes to 1h, 500 stays) - // Actually no - swap exchanges: 5m=0, 1h=500 becomes 5m=500, 1h=0 const normal5m = applySwap(tokens5mOnly, false); const swapped5m = applySwap(tokens5mOnly, true); @@ -106,7 +116,7 @@ describe("swap cache TTL billing", () => { expect(normal5m.cache_creation_5m_input_tokens).toBe(500); expect(normal5m.cache_creation_1h_input_tokens).toBe(0); - // Swapped: 500 at 1h rate, 0 at 5m rate + // Swapped: 500 moved to 1h bucket, 0 moved to 5m bucket expect(swapped5m.cache_creation_5m_input_tokens).toBe(0); expect(swapped5m.cache_creation_1h_input_tokens).toBe(500); }); @@ -126,4 +136,43 @@ describe("swap cache TTL billing", () => { // 1000 at 5m rate expect(result.cache_creation).toBeCloseTo(0.00375, 6); }); + + test("swap also inverts cache_ttl value", () => { + const usage5m = { cache_creation_5m_input_tokens: 100, cache_creation_1h_input_tokens: 0, cache_ttl: "5m" as const }; + const usage1h = { cache_creation_5m_input_tokens: 0, cache_creation_1h_input_tokens: 100, cache_ttl: "1h" as const }; + + const swapped5m = applySwap(usage5m, true); + const swapped1h = applySwap(usage1h, true); + + expect(swapped5m.cache_ttl).toBe("1h"); + expect(swapped1h.cache_ttl).toBe("5m"); + }); + + test("swap with only cache_creation_input_tokens (total) and cache_ttl=1h routes total to 5m bucket", () => { + // Upstream sends total without explicit buckets + cache_ttl: "1h" + // After swap: cache_ttl becomes "5m", so total should go to 5m bucket (not 1h) + const usage = { cache_ttl: "1h" as const }; + const swapped = applySwap(usage, true); + + // cache_ttl should be inverted + expect(swapped.cache_ttl).toBe("5m"); + + // Simulate how response-handler resolves buckets after swap: + // resolvedCacheTtl = "5m" (swapped), cache_creation_input_tokens = 1000 (total) + const resolvedCacheTtl = swapped.cache_ttl; + const totalTokens = 1000; + const cache5m = resolvedCacheTtl === "1h" ? undefined : totalTokens; + const cache1h = resolvedCacheTtl === "1h" ? totalTokens : undefined; + + // Total should land in 5m bucket (cheaper), not 1h + expect(cache5m).toBe(1000); + expect(cache1h).toBeUndefined(); + + const result = calculateRequestCostBreakdown( + { input_tokens: 0, output_tokens: 0, cache_creation_5m_input_tokens: cache5m, cache_creation_1h_input_tokens: cache1h }, + makePriceData() + ); + // 1000 * 0.00000375 (5m rate) + expect(result.cache_creation).toBeCloseTo(0.00375, 6); + }); }); From c7ac039d21083b6bf488c049f3dec761dd93606e Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 10:47:58 +0300 Subject: [PATCH 03/11] refactor(provider): extract applySwapCacheTtlBilling and fix Langfuse usage consistency Extract swap logic into reusable applySwapCacheTtlBilling() function, apply it in all response paths (non-streaming, SSE), and return finalized usage from finalizeRequestStats so Langfuse traces receive already-swapped metrics instead of re-parsing raw response text. Co-Authored-By: Claude Opus 4.6 --- src/app/v1/_lib/proxy/response-handler.ts | 50 +++++-- .../cost-calculation-swap-cache-ttl.test.ts | 141 ++++++++++++++---- 2 files changed, 147 insertions(+), 44 deletions(-) diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index 220ef523c..fcd1c6929 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -585,7 +585,7 @@ export class ProxyResponseHandler { // 使用共享的统计处理方法 const duration = Date.now() - session.startTime; - await finalizeRequestStats( + const finalizedUsage = await finalizeRequestStats( session, responseText, statusCode, @@ -596,8 +596,7 @@ export class ProxyResponseHandler { emitLangfuseTrace(session, { responseHeaders: response.headers, responseText, - usageMetrics: parseUsageFromResponseText(responseText, provider.providerType) - .usageMetrics, + usageMetrics: finalizedUsage, costUsd: undefined, statusCode, durationMs: duration, @@ -727,6 +726,10 @@ export class ProxyResponseHandler { usageRecord = usageResult.usageRecord; usageMetrics = usageResult.usageMetrics; + if (usageMetrics) { + applySwapCacheTtlBilling(usageMetrics, provider.swapCacheTtlBilling); + } + // Codex: Extract prompt_cache_key and update session binding if (provider.providerType === "codex" && session.sessionId && provider.id) { try { @@ -1335,7 +1338,7 @@ export class ProxyResponseHandler { clientAborted, abortReason ); - await finalizeRequestStats( + const finalizedUsage = await finalizeRequestStats( session, allContent, finalized.effectiveStatusCode, @@ -1347,8 +1350,7 @@ export class ProxyResponseHandler { emitLangfuseTrace(session, { responseHeaders: response.headers, responseText: allContent, - usageMetrics: parseUsageFromResponseText(allContent, provider.providerType) - .usageMetrics, + usageMetrics: finalizedUsage, costUsd: undefined, statusCode: finalized.effectiveStatusCode, durationMs: duration, @@ -1685,6 +1687,10 @@ export class ProxyResponseHandler { const usageResult = parseUsageFromResponseText(allContent, provider.providerType); usageForCost = usageResult.usageMetrics; + if (usageForCost) { + applySwapCacheTtlBilling(usageForCost, provider.swapCacheTtlBilling); + } + // Codex: Extract prompt_cache_key from SSE events and update session binding if (provider.providerType === "codex" && session.sessionId && provider.id) { try { @@ -2640,6 +2646,22 @@ function adjustUsageForProviderType( }; } +/** + * Swap 5m/1h cache buckets and cache_ttl when provider.swapCacheTtlBilling is enabled. + * Mutates in-place. + */ +export function applySwapCacheTtlBilling( + usage: UsageMetrics, + swap: boolean | undefined +): void { + if (!swap) return; + const orig5m = usage.cache_creation_5m_input_tokens; + usage.cache_creation_5m_input_tokens = usage.cache_creation_1h_input_tokens; + usage.cache_creation_1h_input_tokens = orig5m; + if (usage.cache_ttl === "5m") usage.cache_ttl = "1h"; + else if (usage.cache_ttl === "1h") usage.cache_ttl = "5m"; +} + async function updateRequestCostFromUsage( messageId: number, originalModel: string | null, @@ -2789,10 +2811,10 @@ export async function finalizeRequestStats( duration: number, errorMessage?: string, providerIdOverride?: number -): Promise { +): Promise { const { messageContext, provider } = session; if (!provider || !messageContext) { - return; + return null; } const providerIdForPersistence = providerIdOverride ?? session.provider?.id; @@ -2817,19 +2839,13 @@ export async function finalizeRequestStats( providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), }); - return; + return null; } // 4. 更新成本 // Invert cache TTL at data entry when provider option is enabled // All downstream (badge, cost, DB, logs) will see inverted values - if (provider.swapCacheTtlBilling) { - const orig5m = usageMetrics.cache_creation_5m_input_tokens; - usageMetrics.cache_creation_5m_input_tokens = usageMetrics.cache_creation_1h_input_tokens; - usageMetrics.cache_creation_1h_input_tokens = orig5m; - if (usageMetrics.cache_ttl === "5m") usageMetrics.cache_ttl = "1h"; - else if (usageMetrics.cache_ttl === "1h") usageMetrics.cache_ttl = "5m"; - } + applySwapCacheTtlBilling(usageMetrics, provider.swapCacheTtlBilling); let resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; @@ -2923,6 +2939,8 @@ export async function finalizeRequestStats( providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), }); + + return normalizedUsage; } /** diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts index 7513167c0..18a24bc05 100644 --- a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "vitest"; import { calculateRequestCostBreakdown } from "@/lib/utils/cost-calculation"; +import { applySwapCacheTtlBilling } from "@/app/v1/_lib/proxy/response-handler"; +import type { UsageMetrics } from "@/app/v1/_lib/proxy/response-handler"; import type { ModelPriceData } from "@/types/model-price"; function makePriceData(overrides: Partial = {}): ModelPriceData { @@ -13,35 +15,28 @@ function makePriceData(overrides: Partial = {}): ModelPriceData }; } -describe("swap cache TTL billing", () => { - // Simulates the swap logic from response-handler.ts: - // When provider.swapCacheTtlBilling is true, bucket values AND cache_ttl - // are swapped at data entry so all downstream sees inverted values. - function applySwap( - usage: { - cache_creation_5m_input_tokens?: number; - cache_creation_1h_input_tokens?: number; - cache_ttl?: "5m" | "1h"; - }, - swap: boolean - ) { - if (!swap) { - return { - cache_creation_5m_input_tokens: usage.cache_creation_5m_input_tokens, - cache_creation_1h_input_tokens: usage.cache_creation_1h_input_tokens, - cache_ttl: usage.cache_ttl, - }; - } - let swappedTtl = usage.cache_ttl; - if (swappedTtl === "5m") swappedTtl = "1h"; - else if (swappedTtl === "1h") swappedTtl = "5m"; - return { - cache_creation_5m_input_tokens: usage.cache_creation_1h_input_tokens, - cache_creation_1h_input_tokens: usage.cache_creation_5m_input_tokens, - cache_ttl: swappedTtl, - }; - } +/** + * Wrapper around the real applySwapCacheTtlBilling that returns a new object + * (the production function mutates in-place). + */ +function applySwap( + usage: { + cache_creation_5m_input_tokens?: number; + cache_creation_1h_input_tokens?: number; + cache_ttl?: "5m" | "1h"; + }, + swap: boolean +) { + const copy = { ...usage } as UsageMetrics; + applySwapCacheTtlBilling(copy, swap); + return { + cache_creation_5m_input_tokens: copy.cache_creation_5m_input_tokens, + cache_creation_1h_input_tokens: copy.cache_creation_1h_input_tokens, + cache_ttl: copy.cache_ttl, + }; +} +describe("swap cache TTL billing", () => { test("swap=false: normal billing (5m tokens at 5m rate, 1h tokens at 1h rate)", () => { const tokens = { cache_creation_5m_input_tokens: 1000, cache_creation_1h_input_tokens: 0 }; const swapped = applySwap(tokens, false); @@ -176,3 +171,93 @@ describe("swap cache TTL billing", () => { expect(result.cache_creation).toBeCloseTo(0.00375, 6); }); }); + +describe("applySwapCacheTtlBilling (direct)", () => { + test("swap=false is a no-op", () => { + const usage: UsageMetrics = { + input_tokens: 100, + output_tokens: 50, + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 300, + cache_ttl: "5m", + }; + const before = { ...usage }; + applySwapCacheTtlBilling(usage, false); + expect(usage).toEqual(before); + }); + + test("swap=undefined is a no-op", () => { + const usage: UsageMetrics = { + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 300, + cache_ttl: "1h", + }; + const before = { ...usage }; + applySwapCacheTtlBilling(usage, undefined); + expect(usage).toEqual(before); + }); + + test("swap=true swaps bucket values", () => { + const usage: UsageMetrics = { + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 300, + }; + applySwapCacheTtlBilling(usage, true); + expect(usage.cache_creation_5m_input_tokens).toBe(300); + expect(usage.cache_creation_1h_input_tokens).toBe(200); + }); + + test("swap=true inverts cache_ttl 5m->1h", () => { + const usage: UsageMetrics = { cache_ttl: "5m" }; + applySwapCacheTtlBilling(usage, true); + expect(usage.cache_ttl).toBe("1h"); + }); + + test("swap=true inverts cache_ttl 1h->5m", () => { + const usage: UsageMetrics = { cache_ttl: "1h" }; + applySwapCacheTtlBilling(usage, true); + expect(usage.cache_ttl).toBe("5m"); + }); + + test("swap=true leaves undefined cache_ttl as undefined", () => { + const usage: UsageMetrics = { cache_creation_5m_input_tokens: 100 }; + applySwapCacheTtlBilling(usage, true); + expect(usage.cache_ttl).toBeUndefined(); + }); + + test("swap=true with undefined bucket values does not crash", () => { + const usage: UsageMetrics = {}; + applySwapCacheTtlBilling(usage, true); + expect(usage.cache_creation_5m_input_tokens).toBeUndefined(); + expect(usage.cache_creation_1h_input_tokens).toBeUndefined(); + }); + + test("swap=true preserves non-cache fields", () => { + const usage: UsageMetrics = { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: 75, + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 300, + cache_ttl: "5m", + }; + applySwapCacheTtlBilling(usage, true); + expect(usage.input_tokens).toBe(100); + expect(usage.output_tokens).toBe(50); + expect(usage.cache_read_input_tokens).toBe(75); + }); + + test("swap=true does not touch mixed cache_ttl", () => { + const usage: UsageMetrics = { + cache_creation_5m_input_tokens: 100, + cache_creation_1h_input_tokens: 200, + cache_ttl: "mixed", + }; + applySwapCacheTtlBilling(usage, true); + // Buckets swap + expect(usage.cache_creation_5m_input_tokens).toBe(200); + expect(usage.cache_creation_1h_input_tokens).toBe(100); + // "mixed" is not "5m" or "1h", so stays unchanged + expect(usage.cache_ttl).toBe("mixed"); + }); +}); From ba5e333936e2800488991474d74647abe7d91375 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 12:18:32 +0300 Subject: [PATCH 04/11] fix(provider): address code review findings for swap cache TTL billing - Record swapCacheTtlApplied on error/abort paths so NULL unambiguously means pre-migration - Add .default(false) to schema column and regenerate migration for query consistency - Remove redundant ?? false where provider is guaranteed non-null - Document in-place mutation in normalizeUsageWithSwap JSDoc - Include swapCacheTtlApplied in audit query for session detail page Co-Authored-By: Claude Opus 4.6 --- drizzle/0070_stormy_exiles.sql | 1 + drizzle/meta/0070_snapshot.json | 3251 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/v1/_lib/proxy/error-handler.ts | 1 + src/app/v1/_lib/proxy/response-handler.ts | 77 +- src/drizzle/schema.ts | 3 + src/repository/_shared/transformers.ts | 1 + src/repository/message-write-buffer.ts | 2 + src/repository/message.ts | 7 + src/types/message.ts | 5 +- .../cost-calculation-swap-cache-ttl.test.ts | 28 + 11 files changed, 3354 insertions(+), 29 deletions(-) create mode 100644 drizzle/0070_stormy_exiles.sql create mode 100644 drizzle/meta/0070_snapshot.json diff --git a/drizzle/0070_stormy_exiles.sql b/drizzle/0070_stormy_exiles.sql new file mode 100644 index 000000000..9ed323cc7 --- /dev/null +++ b/drizzle/0070_stormy_exiles.sql @@ -0,0 +1 @@ +ALTER TABLE "message_request" ADD COLUMN "swap_cache_ttl_applied" boolean DEFAULT false; \ No newline at end of file diff --git a/drizzle/meta/0070_snapshot.json b/drizzle/meta/0070_snapshot.json new file mode 100644 index 000000000..a97618979 --- /dev/null +++ b/drizzle/meta/0070_snapshot.json @@ -0,0 +1,3251 @@ +{ + "id": "36940835-849c-47f5-9cbf-15a6c250499a", + "prevId": "b80dfcb9-be78-4c4f-a483-842cea7cefb4", + "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 with time zone", + "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(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "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_key": { + "name": "idx_keys_key", + "columns": [ + { + "expression": "key", + "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 + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "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": "bigint", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "swap_cache_ttl_applied": { + "name": "swap_cache_ttl_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "special_settings": { + "name": "special_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "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_session_id_prefix": { + "name": "idx_message_request_session_id_prefix", + "columns": [ + { + "expression": "\"session_id\" varchar_pattern_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "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_blocked_by": { + "name": "idx_message_request_blocked_by", + "columns": [ + { + "expression": "blocked_by", + "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_key_created_at_id": { + "name": "idx_message_request_key_created_at_id", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_model_active": { + "name": "idx_message_request_key_model_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_key_endpoint_active": { + "name": "idx_message_request_key_endpoint_active", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"endpoint\" IS NOT NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_created_at_id_active": { + "name": "idx_message_request_created_at_id_active", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_model_active": { + "name": "idx_message_request_model_active", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"model\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_message_request_status_code_active": { + "name": "idx_message_request_status_code_active", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"message_request\".\"deleted_at\" IS NULL AND \"message_request\".\"status_code\" IS NOT NULL", + "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 + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'litellm'" + }, + "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": {} + }, + "idx_model_prices_source": { + "name": "idx_model_prices_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "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 + }, + "use_legacy_mode": { + "name": "use_legacy_mode", + "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.notification_target_bindings": { + "name": "notification_target_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "schedule_cron": { + "name": "schedule_cron", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "schedule_timezone": { + "name": "schedule_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "template_override": { + "name": "template_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_notification_target_binding": { + "name": "unique_notification_target_binding", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_type": { + "name": "idx_notification_bindings_type", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_bindings_target": { + "name": "idx_notification_bindings_target", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_target_bindings_target_id_webhook_targets_id_fk": { + "name": "notification_target_bindings_target_id_webhook_targets_id_fk", + "tableFrom": "notification_target_bindings", + "tableTo": "webhook_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoint_probe_logs": { + "name": "provider_endpoint_probe_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_type": { + "name": "error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_provider_endpoint_probe_logs_endpoint_created_at": { + "name": "idx_provider_endpoint_probe_logs_endpoint_created_at", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoint_probe_logs_created_at": { + "name": "idx_provider_endpoint_probe_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": { + "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk", + "tableFrom": "provider_endpoint_probe_logs", + "tableTo": "provider_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_endpoints": { + "name": "provider_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vendor_id": { + "name": "vendor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'claude'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_probed_at": { + "name": "last_probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_probe_ok": { + "name": "last_probe_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_probe_status_code": { + "name": "last_probe_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_latency_ms": { + "name": "last_probe_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_type": { + "name": "last_probe_error_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "last_probe_error_message": { + "name": "last_probe_error_message", + "type": "text", + "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": { + "uniq_provider_endpoints_vendor_type_url": { + "name": "uniq_provider_endpoints_vendor_type_url", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_vendor_type": { + "name": "idx_provider_endpoints_vendor_type", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_enabled": { + "name": "idx_provider_endpoints_enabled", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_pick_enabled": { + "name": "idx_provider_endpoints_pick_enabled", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"provider_endpoints\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_created_at": { + "name": "idx_provider_endpoints_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_endpoints_deleted_at": { + "name": "idx_provider_endpoints_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_endpoints_vendor_id_provider_vendors_id_fk": { + "name": "provider_endpoints_vendor_id_provider_vendors_id_fk", + "tableFrom": "provider_endpoints", + "tableTo": "provider_vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_vendors": { + "name": "provider_vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "website_domain": { + "name": "website_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "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()" + } + }, + "indexes": { + "uniq_provider_vendors_website_domain": { + "name": "uniq_provider_vendors_website_domain", + "columns": [ + { + "expression": "website_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_provider_vendors_created_at": { + "name": "idx_provider_vendors_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.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 + }, + "provider_vendor_id": { + "name": "provider_vendor_id", + "type": "integer", + "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 + }, + "group_priorities": { + "name": "group_priorities", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "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'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "total_cost_reset_at": { + "name": "total_cost_reset_at", + "type": "timestamp with time zone", + "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 + }, + "swap_cache_ttl_billing": { + "name": "swap_cache_ttl_billing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_effort_preference": { + "name": "codex_reasoning_effort_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_reasoning_summary_preference": { + "name": "codex_reasoning_summary_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "codex_text_verbosity_preference": { + "name": "codex_text_verbosity_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "codex_parallel_tool_calls_preference": { + "name": "codex_parallel_tool_calls_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "anthropic_max_tokens_preference": { + "name": "anthropic_max_tokens_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_thinking_budget_preference": { + "name": "anthropic_thinking_budget_preference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "anthropic_adaptive_thinking": { + "name": "anthropic_adaptive_thinking", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "gemini_google_search_preference": { + "name": "gemini_google_search_preference", + "type": "varchar(20)", + "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_vendor_type_url_active": { + "name": "idx_providers_vendor_type_url_active", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "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": {} + }, + "idx_providers_vendor_type": { + "name": "idx_providers_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_providers_enabled_vendor_type": { + "name": "idx_providers_enabled_vendor_type", + "columns": [ + { + "expression": "provider_vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"providers\".\"deleted_at\" IS NULL AND \"providers\".\"is_enabled\" = true AND \"providers\".\"provider_vendor_id\" IS NOT NULL AND \"providers\".\"provider_vendor_id\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "providers_provider_vendor_id_provider_vendors_id_fk": { + "name": "providers_provider_vendor_id_provider_vendors_id_fk", + "tableFrom": "providers", + "tableTo": "provider_vendors", + "columnsFrom": [ + "provider_vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "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 + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "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()" + } + }, + "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": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "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'" + }, + "timezone": { + "name": "timezone", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "intercept_anthropic_warmup_requests": { + "name": "intercept_anthropic_warmup_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_thinking_signature_rectifier": { + "name": "enable_thinking_signature_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_thinking_budget_rectifier": { + "name": "enable_thinking_budget_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_billing_header_rectifier": { + "name": "enable_billing_header_rectifier", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_codex_session_id_completion": { + "name": "enable_codex_session_id_completion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_claude_metadata_user_id_injection": { + "name": "enable_claude_metadata_user_id_injection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_response_fixer": { + "name": "enable_response_fixer", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "response_fixer_config": { + "name": "response_fixer_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb" + }, + "quota_db_refresh_interval_seconds": { + "name": "quota_db_refresh_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "quota_lease_percent_5h": { + "name": "quota_lease_percent_5h", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_daily": { + "name": "quota_lease_percent_daily", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_weekly": { + "name": "quota_lease_percent_weekly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_percent_monthly": { + "name": "quota_lease_percent_monthly", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.05'" + }, + "quota_lease_cap_usd": { + "name": "quota_lease_cap_usd", + "type": "numeric(10, 2)", + "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()" + } + }, + "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 + }, + "daily_limit_usd": { + "name": "daily_limit_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "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 + }, + "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'" + }, + "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 + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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_tags_gin": { + "name": "idx_users_tags_gin", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "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 + }, + "public.webhook_targets": { + "name": "webhook_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "webhook_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "telegram_bot_token": { + "name": "telegram_bot_token", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "dingtalk_secret": { + "name": "dingtalk_secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "custom_template": { + "name": "custom_template", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_result": { + "name": "last_test_result", + "type": "jsonb", + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.daily_reset_mode": { + "name": "daily_reset_mode", + "schema": "public", + "values": [ + "fixed", + "rolling" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "circuit_breaker", + "daily_leaderboard", + "cost_alert" + ] + }, + "public.webhook_provider_type": { + "name": "webhook_provider_type", + "schema": "public", + "values": [ + "wechat", + "feishu", + "dingtalk", + "telegram", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 861ccd714..9be36aa80 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -491,6 +491,13 @@ "when": 1771188568762, "tag": "0069_special_squirrel_girl", "breakpoints": true + }, + { + "idx": 70, + "version": "7", + "when": 1771233193254, + "tag": "0070_stormy_exiles", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/v1/_lib/proxy/error-handler.ts b/src/app/v1/_lib/proxy/error-handler.ts index 0b457a3b3..ddeb9d4dd 100644 --- a/src/app/v1/_lib/proxy/error-handler.ts +++ b/src/app/v1/_lib/proxy/error-handler.ts @@ -414,6 +414,7 @@ export class ProxyErrorHandler { model: session.getCurrentModel() ?? undefined, providerId: session.provider?.id, // ⭐ 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false, }); // 记录请求结束 diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index fcd1c6929..fffc5e0a5 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -671,6 +671,7 @@ export class ProxyResponseHandler { model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 providerId: session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false, }); const tracker = ProxyStatusTracker.getInstance(); tracker.endRequest(messageContext.user.id, messageContext.id); @@ -727,7 +728,7 @@ export class ProxyResponseHandler { usageMetrics = usageResult.usageMetrics; if (usageMetrics) { - applySwapCacheTtlBilling(usageMetrics, provider.swapCacheTtlBilling); + usageMetrics = normalizeUsageWithSwap(usageMetrics, session, provider.swapCacheTtlBilling); } // Codex: Extract prompt_cache_key and update session binding @@ -884,6 +885,7 @@ export class ProxyResponseHandler { model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 providerId: session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: provider.swapCacheTtlBilling, }); // 记录请求结束 @@ -1688,7 +1690,7 @@ export class ProxyResponseHandler { usageForCost = usageResult.usageMetrics; if (usageForCost) { - applySwapCacheTtlBilling(usageForCost, provider.swapCacheTtlBilling); + usageForCost = normalizeUsageWithSwap(usageForCost, session, provider.swapCacheTtlBilling); } // Codex: Extract prompt_cache_key from SSE events and update session binding @@ -1819,6 +1821,7 @@ export class ProxyResponseHandler { model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 providerId: providerIdForPersistence ?? session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: provider.swapCacheTtlBilling, }); emitLangfuseTrace(session, { @@ -2662,6 +2665,46 @@ export function applySwapCacheTtlBilling( else if (usage.cache_ttl === "1h") usage.cache_ttl = "5m"; } +/** + * Apply swap + resolve session fallback cache_ttl + normalize cache buckets. + * Returns a new UsageMetrics object with consistent bucket routing. + * + * WARNING: Mutates `usageMetrics` in-place (via applySwapCacheTtlBilling) + * before creating the normalized copy. + */ +function normalizeUsageWithSwap( + usageMetrics: UsageMetrics, + session: ProxySession, + swapCacheTtlBilling?: boolean +): UsageMetrics { + applySwapCacheTtlBilling(usageMetrics, swapCacheTtlBilling); + + let resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; + + // When usageMetrics.cache_ttl is absent, session fallback wasn't swapped - handle it + if (swapCacheTtlBilling && !usageMetrics.cache_ttl) { + if (resolvedCacheTtl === "5m") resolvedCacheTtl = "1h"; + else if (resolvedCacheTtl === "1h") resolvedCacheTtl = "5m"; + } + + 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); + + return { + ...usageMetrics, + cache_ttl: resolvedCacheTtl ?? usageMetrics.cache_ttl, + cache_creation_5m_input_tokens: cache5m, + cache_creation_1h_input_tokens: cache1h, + cache_creation_input_tokens: cacheTotal, + }; +} + async function updateRequestCostFromUsage( messageId: number, originalModel: string | null, @@ -2838,6 +2881,7 @@ export async function finalizeRequestStats( model: session.getCurrentModel() ?? undefined, providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: provider.swapCacheTtlBilling, }); return null; } @@ -2845,32 +2889,7 @@ export async function finalizeRequestStats( // 4. 更新成本 // Invert cache TTL at data entry when provider option is enabled // All downstream (badge, cost, DB, logs) will see inverted values - applySwapCacheTtlBilling(usageMetrics, provider.swapCacheTtlBilling); - - let resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; - - // When usageMetrics.cache_ttl is absent, session fallback wasn't swapped - handle it - if (provider.swapCacheTtlBilling && !usageMetrics.cache_ttl) { - if (resolvedCacheTtl === "5m") resolvedCacheTtl = "1h"; - else if (resolvedCacheTtl === "1h") resolvedCacheTtl = "5m"; - } - - 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, - }; + const normalizedUsage = normalizeUsageWithSwap(usageMetrics, session, provider.swapCacheTtlBilling); await updateRequestCostFromUsage( messageContext.id, @@ -2938,6 +2957,7 @@ export async function finalizeRequestStats( model: session.getCurrentModel() ?? undefined, providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: provider.swapCacheTtlBilling, }); return normalizedUsage; @@ -3096,6 +3116,7 @@ async function persistRequestFailure(options: { model: session.getCurrentModel() ?? undefined, providerId: session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), + swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false, }); const isAsyncWrite = getEnvConfig().MESSAGE_REQUEST_WRITE_MODE !== "sync"; diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 65deea818..3b4f31c75 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -456,6 +456,9 @@ export const messageRequest = pgTable('message_request', { // 1M Context Window 应用状态 context1mApplied: boolean('context_1m_applied').default(false), + // Swap Cache TTL Billing: whether cache TTL inversion was active for this request + swapCacheTtlApplied: boolean('swap_cache_ttl_applied').default(false), + // 特殊设置(用于记录各类“特殊行为/覆写”的命中与生效情况,便于审计与展示) specialSettings: jsonb('special_settings').$type(), diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index ef9d97e6c..d3773b713 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -154,6 +154,7 @@ export function toMessageRequest(dbMessage: any): MessageRequest { cacheCreation1hInputTokens: dbMessage?.cacheCreation1hInputTokens ?? undefined, cacheTtlApplied: dbMessage?.cacheTtlApplied ?? null, context1mApplied: dbMessage?.context1mApplied ?? false, + swapCacheTtlApplied: dbMessage?.swapCacheTtlApplied ?? false, specialSettings: dbMessage?.specialSettings ?? null, }; } diff --git a/src/repository/message-write-buffer.ts b/src/repository/message-write-buffer.ts index e7f946bb1..d2f690189 100644 --- a/src/repository/message-write-buffer.ts +++ b/src/repository/message-write-buffer.ts @@ -26,6 +26,7 @@ export type MessageRequestUpdatePatch = { model?: string; providerId?: number; context1mApplied?: boolean; + swapCacheTtlApplied?: boolean; specialSettings?: CreateMessageRequestData["special_settings"]; }; @@ -59,6 +60,7 @@ const COLUMN_MAP: Record = { model: "model", providerId: "provider_id", context1mApplied: "context_1m_applied", + swapCacheTtlApplied: "swap_cache_ttl_applied", specialSettings: "special_settings", }; diff --git a/src/repository/message.ts b/src/repository/message.ts index a90d0e0ec..c22ace55a 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -135,6 +135,7 @@ export async function updateMessageRequestDetails( model?: string; // ⭐ 新增:支持更新重定向后的模型名称 providerId?: number; // ⭐ 新增:支持更新最终供应商ID(重试切换后) context1mApplied?: boolean; // 是否应用了1M上下文窗口 + swapCacheTtlApplied?: boolean; // Swap Cache TTL Billing active at request time specialSettings?: CreateMessageRequestData["special_settings"]; // 特殊设置(审计/展示) } ): Promise { @@ -195,6 +196,9 @@ export async function updateMessageRequestDetails( if (details.context1mApplied !== undefined) { updateData.context1mApplied = details.context1mApplied; } + if (details.swapCacheTtlApplied !== undefined) { + updateData.swapCacheTtlApplied = details.swapCacheTtlApplied; + } if (details.specialSettings !== undefined) { updateData.specialSettings = details.specialSettings; } @@ -285,6 +289,7 @@ export async function findMessageRequestAuditBySessionIdAndSequence( blockedReason: string | null; cacheTtlApplied: string | null; context1mApplied: boolean | null; + swapCacheTtlApplied: boolean | null; specialSettings: SpecialSetting[] | null; } | null> { const [row] = await db @@ -294,6 +299,7 @@ export async function findMessageRequestAuditBySessionIdAndSequence( blockedReason: messageRequest.blockedReason, cacheTtlApplied: messageRequest.cacheTtlApplied, context1mApplied: messageRequest.context1mApplied, + swapCacheTtlApplied: messageRequest.swapCacheTtlApplied, specialSettings: messageRequest.specialSettings, }) .from(messageRequest) @@ -313,6 +319,7 @@ export async function findMessageRequestAuditBySessionIdAndSequence( blockedReason: row.blockedReason, cacheTtlApplied: row.cacheTtlApplied, context1mApplied: row.context1mApplied, + swapCacheTtlApplied: row.swapCacheTtlApplied, specialSettings: Array.isArray(row.specialSettings) ? (row.specialSettings as SpecialSetting[]) : null, diff --git a/src/types/message.ts b/src/types/message.ts index faa2e3f6f..c6833d290 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -256,7 +256,10 @@ export interface MessageRequest { // 1M 上下文窗口是否已应用 context1mApplied?: boolean; - // 特殊设置(用于记录各类“特殊行为/覆写”的命中与生效情况,便于审计与展示) + // Swap Cache TTL Billing: whether cache TTL inversion was active for this request + swapCacheTtlApplied?: boolean; + + // 特殊设置(用于记录各类"特殊行为/覆写"的命中与生效情况,便于审计与展示) specialSettings?: SpecialSetting[] | null; createdAt: Date; diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts index 18a24bc05..d15d61acf 100644 --- a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -260,4 +260,32 @@ describe("applySwapCacheTtlBilling (direct)", () => { // "mixed" is not "5m" or "1h", so stays unchanged expect(usage.cache_ttl).toBe("mixed"); }); + + test("double swap returns to original values (idempotency)", () => { + const usage: UsageMetrics = { + input_tokens: 500, + output_tokens: 250, + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 800, + cache_read_input_tokens: 150, + cache_ttl: "5m", + }; + const original = { ...usage }; + + applySwapCacheTtlBilling(usage, true); + // After first swap, values are inverted + expect(usage.cache_creation_5m_input_tokens).toBe(800); + expect(usage.cache_creation_1h_input_tokens).toBe(200); + expect(usage.cache_ttl).toBe("1h"); + + applySwapCacheTtlBilling(usage, true); + // After second swap, values return to original + expect(usage.cache_creation_5m_input_tokens).toBe(original.cache_creation_5m_input_tokens); + expect(usage.cache_creation_1h_input_tokens).toBe(original.cache_creation_1h_input_tokens); + expect(usage.cache_ttl).toBe(original.cache_ttl); + // Non-cache fields unchanged throughout + expect(usage.input_tokens).toBe(original.input_tokens); + expect(usage.output_tokens).toBe(original.output_tokens); + expect(usage.cache_read_input_tokens).toBe(original.cache_read_input_tokens); + }); }); From 55e64c1e95148e0083906aaed9a514d2ee033e22 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 13:18:27 +0300 Subject: [PATCH 05/11] fix(provider): clone usageMetrics in normalizeUsageWithSwap to prevent mutation side-effects The function was mutating the caller's object in-place via applySwapCacheTtlBilling, risking double-swap and inconsistent state. Now clones before swapping. Also adds swap_cache_ttl_billing to editProvider schema and a caller-isolation test. Co-Authored-By: Claude Opus 4.6 --- src/actions/providers.ts | 1 + src/app/v1/_lib/proxy/response-handler.ts | 26 +++++++++---------- .../cost-calculation-swap-cache-ttl.test.ts | 23 ++++++++++++++++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 9c76ba3ad..89cb72f06 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -638,6 +638,7 @@ export async function editProvider( limit_total_usd?: number | null; limit_concurrent_sessions?: number | null; cache_ttl_preference?: "inherit" | "5m" | "1h"; + swap_cache_ttl_billing?: boolean; context_1m_preference?: Context1mPreference | null; codex_reasoning_effort_preference?: CodexReasoningEffortPreference | null; codex_reasoning_summary_preference?: CodexReasoningSummaryPreference | null; diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index fffc5e0a5..bc011216c 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -2668,37 +2668,37 @@ export function applySwapCacheTtlBilling( /** * Apply swap + resolve session fallback cache_ttl + normalize cache buckets. * Returns a new UsageMetrics object with consistent bucket routing. - * - * WARNING: Mutates `usageMetrics` in-place (via applySwapCacheTtlBilling) - * before creating the normalized copy. + * The input object is NOT mutated -- swap is applied to an internal clone. */ function normalizeUsageWithSwap( usageMetrics: UsageMetrics, session: ProxySession, swapCacheTtlBilling?: boolean ): UsageMetrics { - applySwapCacheTtlBilling(usageMetrics, swapCacheTtlBilling); + // Clone before mutating to prevent caller side-effects and double-swap risks + const swapped = { ...usageMetrics }; + applySwapCacheTtlBilling(swapped, swapCacheTtlBilling); - let resolvedCacheTtl = usageMetrics.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; + let resolvedCacheTtl = swapped.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; - // When usageMetrics.cache_ttl is absent, session fallback wasn't swapped - handle it + // When original cache_ttl is absent, session fallback wasn't swapped - handle it if (swapCacheTtlBilling && !usageMetrics.cache_ttl) { if (resolvedCacheTtl === "5m") resolvedCacheTtl = "1h"; else if (resolvedCacheTtl === "1h") resolvedCacheTtl = "5m"; } const cache5m = - usageMetrics.cache_creation_5m_input_tokens ?? - (resolvedCacheTtl === "1h" ? undefined : usageMetrics.cache_creation_input_tokens); + swapped.cache_creation_5m_input_tokens ?? + (resolvedCacheTtl === "1h" ? undefined : swapped.cache_creation_input_tokens); const cache1h = - usageMetrics.cache_creation_1h_input_tokens ?? - (resolvedCacheTtl === "1h" ? usageMetrics.cache_creation_input_tokens : undefined); + swapped.cache_creation_1h_input_tokens ?? + (resolvedCacheTtl === "1h" ? swapped.cache_creation_input_tokens : undefined); const cacheTotal = - usageMetrics.cache_creation_input_tokens ?? ((cache5m ?? 0) + (cache1h ?? 0) || undefined); + swapped.cache_creation_input_tokens ?? ((cache5m ?? 0) + (cache1h ?? 0) || undefined); return { - ...usageMetrics, - cache_ttl: resolvedCacheTtl ?? usageMetrics.cache_ttl, + ...swapped, + cache_ttl: resolvedCacheTtl ?? swapped.cache_ttl, cache_creation_5m_input_tokens: cache5m, cache_creation_1h_input_tokens: cache1h, cache_creation_input_tokens: cacheTotal, diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts index d15d61acf..7ec605ef7 100644 --- a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -261,6 +261,29 @@ describe("applySwapCacheTtlBilling (direct)", () => { expect(usage.cache_ttl).toBe("mixed"); }); + test("applySwapCacheTtlBilling does not affect a pre-cloned copy (caller isolation)", () => { + const original: UsageMetrics = { + cache_creation_5m_input_tokens: 200, + cache_creation_1h_input_tokens: 800, + cache_ttl: "5m", + }; + const snapshot = { ...original }; + + // Clone then swap (mimics the fixed normalizeUsageWithSwap pattern) + const clone = { ...original }; + applySwapCacheTtlBilling(clone, true); + + // Original must be untouched + expect(original.cache_creation_5m_input_tokens).toBe(snapshot.cache_creation_5m_input_tokens); + expect(original.cache_creation_1h_input_tokens).toBe(snapshot.cache_creation_1h_input_tokens); + expect(original.cache_ttl).toBe(snapshot.cache_ttl); + + // Clone should have swapped values + expect(clone.cache_creation_5m_input_tokens).toBe(800); + expect(clone.cache_creation_1h_input_tokens).toBe(200); + expect(clone.cache_ttl).toBe("1h"); + }); + test("double swap returns to original values (idempotency)", () => { const usage: UsageMetrics = { input_tokens: 500, From dddff24b228d5b8dc9e114a3ad234c210b982a75 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 14:09:04 +0300 Subject: [PATCH 06/11] feat(ui): add swap indicator on cacheTtlApplied badge in logs Thread swapCacheTtlApplied from DB through repository SELECT queries, error-details-dialog props, and all 3 badge render locations. When swap is active, badge turns amber with "~" suffix and i18n tooltip. Co-Authored-By: Claude Opus 4.6 --- messages/en/dashboard.json | 2 + messages/ja/dashboard.json | 2 + messages/ru/dashboard.json | 2 + messages/zh-CN/dashboard.json | 2 + messages/zh-TW/dashboard.json | 2 + .../components/MetadataTab.tsx | 17 ++++++++- .../components/SummaryTab.tsx | 17 ++++++++- .../error-details-dialog/index.tsx | 3 ++ .../_components/error-details-dialog/types.ts | 2 + .../_components/usage-logs-table.test.tsx | 38 +++++++++++++++++++ .../logs/_components/usage-logs-table.tsx | 17 ++++++++- .../virtualized-logs-table.test.tsx | 34 +++++++++++++++++ .../_components/virtualized-logs-table.tsx | 14 ++++++- src/repository/usage-logs.ts | 3 ++ 14 files changed, 151 insertions(+), 4 deletions(-) diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index ece1301d6..8b056aaf4 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -280,6 +280,7 @@ "cacheWrite1h": "Cache Write (1h)", "cacheRead": "Cache Read", "cacheTtl": "Cache TTL", + "cacheTtlSwapped": "Billing TTL (swapped)", "multiplier": "Provider Multiplier", "totalCost": "Total Cost", "context1m": "1M Context", @@ -365,6 +366,7 @@ "cacheWrite1h": "Cache Write (1h)", "cacheRead": "Cache Read", "cacheTtl": "Cache TTL", + "cacheTtlSwapped": "Billing TTL (swapped)", "multiplier": "Provider Multiplier", "totalCost": "Total Cost", "context1m": "1M Context", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index a609b1c13..1a6fada76 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -280,6 +280,7 @@ "cacheWrite1h": "キャッシュ書き込み (1h)", "cacheRead": "キャッシュ読み取り", "cacheTtl": "キャッシュ TTL", + "cacheTtlSwapped": "課金 TTL (スワップ済み)", "multiplier": "プロバイダー倍率", "totalCost": "総コスト", "context1m": "1M コンテキスト", @@ -365,6 +366,7 @@ "cacheWrite1h": "キャッシュ書き込み (1h)", "cacheRead": "キャッシュ読み取り", "cacheTtl": "キャッシュ TTL", + "cacheTtlSwapped": "課金 TTL (スワップ済み)", "multiplier": "プロバイダー倍率", "totalCost": "合計費用", "context1m": "1M コンテキスト", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 0204a1430..1ea2d9cc7 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -280,6 +280,7 @@ "cacheWrite1h": "Запись кэша (1h)", "cacheRead": "Чтение кэша", "cacheTtl": "TTL кэша", + "cacheTtlSwapped": "TTL биллинга (инверсия)", "multiplier": "Множитель поставщика", "totalCost": "Общая стоимость", "context1m": "1M контекст", @@ -365,6 +366,7 @@ "cacheWrite1h": "Запись кэша (1h)", "cacheRead": "Чтение кэша", "cacheTtl": "TTL кэша", + "cacheTtlSwapped": "TTL биллинга (инверсия)", "multiplier": "Множитель поставщика", "totalCost": "Общая стоимость", "context1m": "1M Контекст", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 25354fe2d..924f895af 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -280,6 +280,7 @@ "cacheWrite1h": "缓存写入 (1h)", "cacheRead": "缓存读取", "cacheTtl": "缓存 TTL", + "cacheTtlSwapped": "计费 TTL (已互换)", "multiplier": "供应商倍率", "totalCost": "总费用", "context1m": "1M 上下文", @@ -365,6 +366,7 @@ "cacheWrite1h": "缓存写入 (1h)", "cacheRead": "缓存读取", "cacheTtl": "缓存 TTL", + "cacheTtlSwapped": "计费 TTL (已互换)", "multiplier": "供应商倍率", "totalCost": "总费用", "context1m": "1M 上下文", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 7f30dcc9b..abdafbccf 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -280,6 +280,7 @@ "cacheWrite1h": "快取寫入(1h)", "cacheRead": "快取讀取", "cacheTtl": "快取 TTL", + "cacheTtlSwapped": "計費 TTL (已互換)", "multiplier": "供應商倍率", "totalCost": "總費用", "context1m": "1M 上下文", @@ -365,6 +366,7 @@ "cacheWrite1h": "快取寫入(1h)", "cacheRead": "快取讀取", "cacheTtl": "快取 TTL", + "cacheTtlSwapped": "計費 TTL (已互換)", "multiplier": "供應商倍率", "totalCost": "總費用", "context1m": "1M 上下文長度", diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx index 1d6d1364a..623d44dfd 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx @@ -35,6 +35,7 @@ export function MetadataTab({ cacheCreation1hInputTokens, cacheReadInputTokens, cacheTtlApplied, + swapCacheTtlApplied, costUsd, costMultiplier, context1mApplied, @@ -205,8 +206,22 @@ export function MetadataTab({ {cacheTtlApplied && (
{t("billingDetails.cacheTtl")}: - + {cacheTtlApplied} + {swapCacheTtlApplied ? " ~" : ""}
)} diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx index d502d9637..a6fec668f 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx @@ -42,6 +42,7 @@ export function SummaryTab({ cacheCreation1hInputTokens, cacheReadInputTokens, cacheTtlApplied, + swapCacheTtlApplied, costUsd, costMultiplier, context1mApplied, @@ -316,8 +317,22 @@ export function SummaryTab({ {cacheTtlApplied && (
{t("billingDetails.cacheTtl")}: - + {cacheTtlApplied} + {swapCacheTtlApplied ? " ~" : ""}
)} diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx index 70c75cbe7..3ecff9dfb 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx @@ -36,6 +36,7 @@ interface ErrorDetailsDialogProps { cacheCreation1hInputTokens?: number | null; cacheReadInputTokens?: number | null; cacheTtlApplied?: string | null; + swapCacheTtlApplied?: boolean | null; costUsd?: string | null; costMultiplier?: string | null; context1mApplied?: boolean | null; @@ -74,6 +75,7 @@ export function ErrorDetailsDialog({ cacheCreation1hInputTokens, cacheReadInputTokens, cacheTtlApplied, + swapCacheTtlApplied, costUsd, costMultiplier, context1mApplied, @@ -217,6 +219,7 @@ export function ErrorDetailsDialog({ cacheCreation1hInputTokens, cacheReadInputTokens, cacheTtlApplied, + swapCacheTtlApplied, costUsd, costMultiplier, context1mApplied, diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts index 7c1384d4a..26bd8eca2 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/types.ts @@ -48,6 +48,8 @@ export interface TabSharedProps { cacheReadInputTokens?: number | null; /** Cache TTL applied */ cacheTtlApplied?: string | null; + /** Whether swap cache TTL billing was applied */ + swapCacheTtlApplied?: boolean | null; /** Total cost in USD */ costUsd?: string | null; /** Cost multiplier */ diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx index 34fef1749..d45313e5c 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx @@ -82,6 +82,7 @@ function makeLog(overrides: Partial): UsageLogRow { userAgent: null, messagesCount: null, context1mApplied: null, + swapCacheTtlApplied: null, specialSettings: null, ...overrides, }; @@ -232,6 +233,43 @@ describe("usage-logs-table multiplier badge", () => { expect(html).toContain("TTFB"); }); + test("renders swap indicator on cacheTtl badge when swapCacheTtlApplied is true", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + /> + ); + + // Should contain the swap indicator "~" + expect(html).toContain("5m ~"); + // Should contain amber styling + expect(html).toContain("bg-amber-50"); + }); + + test("does not render swap indicator when swapCacheTtlApplied is false", () => { + const html = renderToStaticMarkup( + {}} + isPending={false} + /> + ); + + // Should contain the TTL value without swap indicator + expect(html).toContain("5m"); + expect(html).not.toContain("5m ~"); + // Should not contain amber styling + expect(html).not.toContain("bg-amber-50"); + }); + test("copies sessionId on click and shows toast", async () => { const writeText = vi.fn(async () => {}); Object.defineProperty(navigator, "clipboard", { diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx index 9b643ac85..06241c052 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx @@ -299,8 +299,22 @@ export function UsageLogsTable({
{log.cacheTtlApplied ? ( - + {log.cacheTtlApplied} + {log.swapCacheTtlApplied ? " ~" : ""} ) : null} @@ -509,6 +523,7 @@ export function UsageLogsTable({ cacheCreation1hInputTokens={log.cacheCreation1hInputTokens} cacheReadInputTokens={log.cacheReadInputTokens} cacheTtlApplied={log.cacheTtlApplied} + swapCacheTtlApplied={log.swapCacheTtlApplied} costUsd={log.costUsd} costMultiplier={log.costMultiplier} context1mApplied={log.context1mApplied} diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx index e39583be8..404501fa6 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx @@ -128,6 +128,7 @@ function makeLog(overrides: Partial): UsageLogRow { userAgent: null, messagesCount: null, context1mApplied: null, + swapCacheTtlApplied: null, specialSettings: null, ...overrides, }; @@ -340,4 +341,37 @@ describe("virtualized-logs-table multiplier badge", () => { // TTFB should also appear expect(html).toContain("TTFB"); }); + + test("renders swap indicator on cacheTtl badge when swapCacheTtlApplied is true", () => { + mockIsLoading = false; + mockIsError = false; + mockError = null; + mockHasNextPage = false; + mockIsFetchingNextPage = false; + + mockLogs = [makeLog({ id: 1, cacheTtlApplied: "5m", swapCacheTtlApplied: true })]; + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("5m ~"); + expect(html).toContain("bg-amber-50"); + }); + + test("does not render swap indicator when swapCacheTtlApplied is false", () => { + mockIsLoading = false; + mockIsError = false; + mockError = null; + mockHasNextPage = false; + mockIsFetchingNextPage = false; + + mockLogs = [makeLog({ id: 1, cacheTtlApplied: "5m", swapCacheTtlApplied: false })]; + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("5m"); + expect(html).not.toContain("5m ~"); + expect(html).not.toContain("bg-amber-50"); + }); }); diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx index af4a980ec..7b24cc03b 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -562,9 +562,20 @@ export function VirtualizedLogsTable({ {log.cacheTtlApplied ? ( {log.cacheTtlApplied} + {log.swapCacheTtlApplied ? " ~" : ""} ) : null} @@ -739,6 +750,7 @@ export function VirtualizedLogsTable({ cacheCreation1hInputTokens={log.cacheCreation1hInputTokens} cacheReadInputTokens={log.cacheReadInputTokens} cacheTtlApplied={log.cacheTtlApplied} + swapCacheTtlApplied={log.swapCacheTtlApplied} costUsd={log.costUsd} costMultiplier={log.costMultiplier} context1mApplied={log.context1mApplied} diff --git a/src/repository/usage-logs.ts b/src/repository/usage-logs.ts index d965b7d3e..410025958 100644 --- a/src/repository/usage-logs.ts +++ b/src/repository/usage-logs.ts @@ -63,6 +63,7 @@ export interface UsageLogRow { userAgent: string | null; // User-Agent(客户端信息) messagesCount: number | null; // Messages 数量 context1mApplied: boolean | null; // 是否应用了1M上下文窗口 + swapCacheTtlApplied: boolean | null; // 是否启用了swap cache TTL billing specialSettings: SpecialSetting[] | null; // 特殊设置(审计/展示) } @@ -178,6 +179,7 @@ export async function findUsageLogsBatch( userAgent: messageRequest.userAgent, messagesCount: messageRequest.messagesCount, context1mApplied: messageRequest.context1mApplied, + swapCacheTtlApplied: messageRequest.swapCacheTtlApplied, specialSettings: messageRequest.specialSettings, }) .from(messageRequest) @@ -529,6 +531,7 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis userAgent: messageRequest.userAgent, // User-Agent messagesCount: messageRequest.messagesCount, // Messages 数量 context1mApplied: messageRequest.context1mApplied, // 1M上下文窗口 + swapCacheTtlApplied: messageRequest.swapCacheTtlApplied, // swap cache TTL billing specialSettings: messageRequest.specialSettings, // 特殊设置(审计/展示) }) .from(messageRequest) From 485ab1f36a0c9f7650cd229d1e9c9dd0cbdaba89 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 14:32:20 +0300 Subject: [PATCH 07/11] fix(ui): reorder swap toggle and add missing DialogTitle for a11y Move Swap Cache TTL Billing toggle directly after Forward client IP for logical grouping. Add hidden DialogTitle to all provider dialogs missing it (add, edit, clone, vendor key) to satisfy Radix a11y check. Co-Authored-By: Claude Opus 4.6 --- .../_components/add-provider-dialog.tsx | 6 +++- .../sections/routing-section.tsx | 30 +++++++++---------- .../_components/provider-rich-list-item.tsx | 8 +++++ .../_components/vendor-keys-compact-list.tsx | 7 +++++ 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx index 07fb2b7e6..e8d944292 100644 --- a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx +++ b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx @@ -4,7 +4,8 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import { FormErrorBoundary } from "@/components/form-error-boundary"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { ProviderForm } from "./forms/provider-form"; interface AddProviderDialogProps { @@ -22,6 +23,9 @@ export function AddProviderDialog({ enableMultiProviderTypes }: AddProviderDialo + + {t("addProvider")} + + {/* Swap Cache TTL Billing */} + + + dispatch({ type: "SET_SWAP_CACHE_TTL_BILLING", payload: checked }) + } + disabled={state.ui.isPending} + /> + + {/* Cache TTL */} - {/* Swap Cache TTL Billing */} - - - dispatch({ type: "SET_SWAP_CACHE_TTL_BILLING", payload: checked }) - } - disabled={state.ui.isPending} - /> - - {/* 1M Context Window - Claude type only */} {state.routing.providerType === "claude" && ( + + {t("editProvider")} + + + {t("clone")} + + + {t("addVendorKey")} + + + {t("editProvider")} + Date: Mon, 16 Feb 2026 14:53:50 +0300 Subject: [PATCH 08/11] fix(ui): prevent duplicate "default" key in provider group tabs When a provider has groupTag="default", the value already exists in the Set. Adding "default" again for the hasDefaultGroup case produced a duplicate React key warning. Deleting it from the Set before spreading eliminates the duplication. Co-Authored-By: Claude Opus 4.6 --- .../[locale]/settings/providers/_components/provider-manager.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/[locale]/settings/providers/_components/provider-manager.tsx b/src/app/[locale]/settings/providers/_components/provider-manager.tsx index 0b373c4db..4a40a6453 100644 --- a/src/app/[locale]/settings/providers/_components/provider-manager.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-manager.tsx @@ -156,6 +156,7 @@ export function ProviderManager({ }); // Sort groups: "default" first, then alphabetically + groups.delete("default"); const sortedGroups = Array.from(groups).sort(); if (hasDefaultGroup) { return ["default", ...sortedGroups]; From 34e7286b89e741ed51f96b4a31d9db604de3c0e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 12:20:12 +0000 Subject: [PATCH 09/11] chore: format code (feature-swap-cache-ttl-billing-00fa88c) --- .../components/MetadataTab.tsx | 6 +---- .../components/SummaryTab.tsx | 6 +---- src/app/v1/_lib/proxy/response-handler.ts | 23 ++++++++++++------ .../cost-calculation-swap-cache-ttl.test.ts | 24 +++++++++++++++---- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx index 623d44dfd..83178a60e 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx @@ -214,11 +214,7 @@ export function MetadataTab({ ? "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/30 dark:text-amber-300 dark:border-amber-800" : "" )} - title={ - swapCacheTtlApplied - ? t("billingDetails.cacheTtlSwapped") - : undefined - } + title={swapCacheTtlApplied ? t("billingDetails.cacheTtlSwapped") : undefined} > {cacheTtlApplied} {swapCacheTtlApplied ? " ~" : ""} diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx index a6fec668f..1873d23ce 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx @@ -325,11 +325,7 @@ export function SummaryTab({ ? "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/30 dark:text-amber-300 dark:border-amber-800" : "" )} - title={ - swapCacheTtlApplied - ? t("billingDetails.cacheTtlSwapped") - : undefined - } + title={swapCacheTtlApplied ? t("billingDetails.cacheTtlSwapped") : undefined} > {cacheTtlApplied} {swapCacheTtlApplied ? " ~" : ""} diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index bc011216c..04c79c320 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -728,7 +728,11 @@ export class ProxyResponseHandler { usageMetrics = usageResult.usageMetrics; if (usageMetrics) { - usageMetrics = normalizeUsageWithSwap(usageMetrics, session, provider.swapCacheTtlBilling); + usageMetrics = normalizeUsageWithSwap( + usageMetrics, + session, + provider.swapCacheTtlBilling + ); } // Codex: Extract prompt_cache_key and update session binding @@ -1690,7 +1694,11 @@ export class ProxyResponseHandler { usageForCost = usageResult.usageMetrics; if (usageForCost) { - usageForCost = normalizeUsageWithSwap(usageForCost, session, provider.swapCacheTtlBilling); + usageForCost = normalizeUsageWithSwap( + usageForCost, + session, + provider.swapCacheTtlBilling + ); } // Codex: Extract prompt_cache_key from SSE events and update session binding @@ -2653,10 +2661,7 @@ function adjustUsageForProviderType( * Swap 5m/1h cache buckets and cache_ttl when provider.swapCacheTtlBilling is enabled. * Mutates in-place. */ -export function applySwapCacheTtlBilling( - usage: UsageMetrics, - swap: boolean | undefined -): void { +export function applySwapCacheTtlBilling(usage: UsageMetrics, swap: boolean | undefined): void { if (!swap) return; const orig5m = usage.cache_creation_5m_input_tokens; usage.cache_creation_5m_input_tokens = usage.cache_creation_1h_input_tokens; @@ -2889,7 +2894,11 @@ export async function finalizeRequestStats( // 4. 更新成本 // Invert cache TTL at data entry when provider option is enabled // All downstream (badge, cost, DB, logs) will see inverted values - const normalizedUsage = normalizeUsageWithSwap(usageMetrics, session, provider.swapCacheTtlBilling); + const normalizedUsage = normalizeUsageWithSwap( + usageMetrics, + session, + provider.swapCacheTtlBilling + ); await updateRequestCostFromUsage( messageContext.id, diff --git a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts index 7ec605ef7..6fbda3f3b 100644 --- a/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts +++ b/tests/unit/lib/cost-calculation-swap-cache-ttl.test.ts @@ -117,7 +117,10 @@ describe("swap cache TTL billing", () => { }); test("swap with undefined tokens treats them as undefined (no crash)", () => { - const tokens = { cache_creation_5m_input_tokens: undefined, cache_creation_1h_input_tokens: 1000 }; + const tokens = { + cache_creation_5m_input_tokens: undefined, + cache_creation_1h_input_tokens: 1000, + }; const swapped = applySwap(tokens, true); expect(swapped.cache_creation_5m_input_tokens).toBe(1000); @@ -133,8 +136,16 @@ describe("swap cache TTL billing", () => { }); test("swap also inverts cache_ttl value", () => { - const usage5m = { cache_creation_5m_input_tokens: 100, cache_creation_1h_input_tokens: 0, cache_ttl: "5m" as const }; - const usage1h = { cache_creation_5m_input_tokens: 0, cache_creation_1h_input_tokens: 100, cache_ttl: "1h" as const }; + const usage5m = { + cache_creation_5m_input_tokens: 100, + cache_creation_1h_input_tokens: 0, + cache_ttl: "5m" as const, + }; + const usage1h = { + cache_creation_5m_input_tokens: 0, + cache_creation_1h_input_tokens: 100, + cache_ttl: "1h" as const, + }; const swapped5m = applySwap(usage5m, true); const swapped1h = applySwap(usage1h, true); @@ -164,7 +175,12 @@ describe("swap cache TTL billing", () => { expect(cache1h).toBeUndefined(); const result = calculateRequestCostBreakdown( - { input_tokens: 0, output_tokens: 0, cache_creation_5m_input_tokens: cache5m, cache_creation_1h_input_tokens: cache1h }, + { + input_tokens: 0, + output_tokens: 0, + cache_creation_5m_input_tokens: cache5m, + cache_creation_1h_input_tokens: cache1h, + }, makePriceData() ); // 1000 * 0.00000375 (5m rate) From f4924aa38c77d680e3d36fdf7040f17f32e073cb Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 15:55:13 +0300 Subject: [PATCH 10/11] fix(ui): handle explicit "default" groupTag in provider tabs When a provider has an explicit "default" groupTag, treat it the same as having no tag rather than adding a literal "default" entry to the groups Set, which caused a duplicate tab. Co-Authored-By: Claude Opus 4.6 --- .../settings/providers/_components/provider-manager.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/settings/providers/_components/provider-manager.tsx b/src/app/[locale]/settings/providers/_components/provider-manager.tsx index 4a40a6453..d756e03bf 100644 --- a/src/app/[locale]/settings/providers/_components/provider-manager.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-manager.tsx @@ -151,12 +151,17 @@ export function ProviderManager({ if (!tags || tags.length === 0) { hasDefaultGroup = true; } else { - tags.forEach((g) => groups.add(g)); + tags.forEach((g) => { + if (g === "default") { + hasDefaultGroup = true; + } else { + groups.add(g); + } + }); } }); // Sort groups: "default" first, then alphabetically - groups.delete("default"); const sortedGroups = Array.from(groups).sort(); if (hasDefaultGroup) { return ["default", ...sortedGroups]; From dfbca74322b14acd36aab55a647e81da5a8005ee Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 16 Feb 2026 15:55:20 +0300 Subject: [PATCH 11/11] refactor(provider): address code review nitpicks - Reorder VisuallyHidden import to third-party block in two files - Expand comment on session fallback cache_ttl inversion - Use ?? false for swapCacheTtlApplied to guarantee boolean type - Use destructuring swap instead of temp variable in applySwapCacheTtlBilling Co-Authored-By: Claude Opus 4.6 --- .../_components/provider-rich-list-item.tsx | 2 +- .../_components/vendor-keys-compact-list.tsx | 2 +- src/app/v1/_lib/proxy/response-handler.ts | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx index 771cf5e70..6ff5e67e6 100644 --- a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx @@ -15,6 +15,7 @@ import { import { useTranslations } from "next-intl"; import { useEffect, useState, useTransition } from "react"; import { toast } from "sonner"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { editProvider, getUnmaskedProviderKey, @@ -43,7 +44,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { DropdownMenu, DropdownMenuContent, diff --git a/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx b/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx index 84ae87008..acc70cfa4 100644 --- a/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx +++ b/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx @@ -5,6 +5,7 @@ import { CheckCircle, Copy, Edit2, Loader2, Plus, Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { getProviderEndpoints } from "@/actions/provider-endpoints"; import { editProvider, getUnmaskedProviderKey, removeProvider } from "@/actions/providers"; import { FormErrorBoundary } from "@/components/form-error-boundary"; @@ -28,7 +29,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { Switch } from "@/components/ui/switch"; import { Table, diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index 04c79c320..a50d4bb6b 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -889,7 +889,7 @@ export class ProxyResponseHandler { model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 providerId: session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), - swapCacheTtlApplied: provider.swapCacheTtlBilling, + swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false, }); // 记录请求结束 @@ -1829,7 +1829,7 @@ export class ProxyResponseHandler { model: session.getCurrentModel() ?? undefined, // 更新重定向后的模型 providerId: providerIdForPersistence ?? session.provider?.id, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), - swapCacheTtlApplied: provider.swapCacheTtlBilling, + swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false, }); emitLangfuseTrace(session, { @@ -2663,9 +2663,10 @@ function adjustUsageForProviderType( */ export function applySwapCacheTtlBilling(usage: UsageMetrics, swap: boolean | undefined): void { if (!swap) return; - const orig5m = usage.cache_creation_5m_input_tokens; - usage.cache_creation_5m_input_tokens = usage.cache_creation_1h_input_tokens; - usage.cache_creation_1h_input_tokens = orig5m; + [usage.cache_creation_5m_input_tokens, usage.cache_creation_1h_input_tokens] = [ + usage.cache_creation_1h_input_tokens, + usage.cache_creation_5m_input_tokens, + ]; if (usage.cache_ttl === "5m") usage.cache_ttl = "1h"; else if (usage.cache_ttl === "1h") usage.cache_ttl = "5m"; } @@ -2686,7 +2687,9 @@ function normalizeUsageWithSwap( let resolvedCacheTtl = swapped.cache_ttl ?? session.getCacheTtlResolved?.() ?? null; - // When original cache_ttl is absent, session fallback wasn't swapped - handle it + // When the upstream response had no cache_ttl, we fell through to the session-level + // getCacheTtlResolved() fallback which reflects the *original* (un-swapped) value. + // We must invert it here to stay consistent with the already-swapped bucket tokens. if (swapCacheTtlBilling && !usageMetrics.cache_ttl) { if (resolvedCacheTtl === "5m") resolvedCacheTtl = "1h"; else if (resolvedCacheTtl === "1h") resolvedCacheTtl = "5m"; @@ -2886,7 +2889,7 @@ export async function finalizeRequestStats( model: session.getCurrentModel() ?? undefined, providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), - swapCacheTtlApplied: provider.swapCacheTtlBilling, + swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false, }); return null; } @@ -2966,7 +2969,7 @@ export async function finalizeRequestStats( model: session.getCurrentModel() ?? undefined, providerId: providerIdForPersistence, // 更新最终供应商ID(重试切换后) context1mApplied: session.getContext1mApplied(), - swapCacheTtlApplied: provider.swapCacheTtlBilling, + swapCacheTtlApplied: provider.swapCacheTtlBilling ?? false, }); return normalizedUsage;