Compare commits

...
Author SHA1 Message Date
Brandon Thomas c238a30776 fix frontend dev scripts 2026-06-10 09:27:28 -04:00
Brandon Thomas b011319abd dashboard links (#1624) 2026-06-10 09:03:58 -04:00
Brandon Thomas 1c654bfafa stripe info admin endpoint (#1623) 2026-06-10 08:42:32 -04:00
Brandon Thomas a7c59f7c9b fix test failures (#1622) 2026-06-10 00:52:24 -04:00
Brandon Thomas 780bbbe9cf test fixes (#1621) 2026-06-10 00:37:32 -04:00
Brandon Thomas 6e5cce20fb seedance errors revamp (#1620) 2026-06-09 22:52:03 -04:00
Brandon Thomas b12781056d track platform type (#1619) 2026-06-09 20:19:05 -04:00
177 changed files with 2169 additions and 248 deletions
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 30
"Right": 31
},
"nullable": []
},
"hash": "161febbb30cf6c669a813fe036d405ef428422297440abd240482a6aae51f6f9"
"hash": "108731dc51d915ebb8b6ed391c5bae4c41ae6f2c813be92c07d8a10ca401db53"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "1ddb2207db50e2dbfb37d26c006bffe2d8f5fb011855d5a86d01f395c36df759"
"hash": "1a0b60602debe525529f50566fc545f1cb4b77005f423a10464652be7fbfa839"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -127,9 +137,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "31d85cc0013f29e6db2d272ee6e709cca79b46496a2c222960f1a1419175f00b"
"hash": "38a1d7b676bb0987fc191bbc8bdeadf6174a19358dff40d9772b9d5824145bbc"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -127,9 +137,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "2c98621c08055374fd09502c94ac40bb01d23fcea3f844cceea7446a5c0cb4dc"
"hash": "3a846909a725eb776b4a4637dc64dcaf1ed70ad8891f9d012c7f3cdcb16a613e"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n maybe_stripe_customer_id as `stripe_customer_id!`\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND deleted_at IS NULL\nORDER BY subscription_namespace ASC, maybe_stripe_customer_id ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subscription_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "stripe_customer_id!",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true
]
},
"hash": "516db34b6a642a2f7d7107f186905a79ad694c3c86f3078efc77bcaae7f39f3d"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
"hash": "5f24aeb12e5a78ffedd1c772c594b668188aaaf01f315a1c41c75610fdfc1b32"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n platform_type = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 41
"Right": 42
},
"nullable": []
},
"hash": "a4901f904aa1c00d9afc23faa7008816c6e1b653bd6ad9f0b6772e32000be544"
"hash": "ad2ffbd9f48e52678a5c61817bec043bf1df4033463e5c5a18468c04af391e6a"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "a0002183d4d077b07bb0cd062ca95bf113d1564ce3195f0e62e531ce4297dd76"
"hash": "ddbbd3886c00268edd3993508f635549fc8ddc877bb059fd58f14b8c07586fc9"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n user_token as `user_token: tokens::tokens::users::UserToken`,\n payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n stripe_customer_id\n\nFROM user_stripe_customer_links\n\nWHERE user_token = ?\nORDER BY id ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "stripe_customer_id",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "ffb5717340f21f21693bfcade201bc055fb3fb8a26da8d0574e3de44b033586c"
}
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN platform_type;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
DROP COLUMN platform_type;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
@@ -208,6 +208,11 @@ CREATE TABLE generic_inference_jobs (
-- Multiple debug_logs rows can share the same event token.
maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL,
-- The platform the enqueuing request came from ('web', 'desktop_app',
-- 'curl', 'python_requests', 'postman'),
-- inferred from the request's User-Agent header.
platform_type VARCHAR(16) DEFAULT NULL,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
@@ -65,6 +65,12 @@ CREATE TABLE media_files (
-- What service provided the generation (if any).
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
-- The platform the creating request came from ('web', 'desktop_app',
-- 'curl', 'python_requests', 'postman'),
-- inferred from the request's User-Agent header. Copied from the originating
-- generic_inference_jobs row for inference results.
platform_type VARCHAR(16) DEFAULT NULL,
-- For inference that can be tied back to a model, the type of model.
-- There are multiple indices on this column!
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
@@ -2,5 +2,6 @@ pub mod alerts;
pub mod jobs;
pub mod user;
pub mod user_referrals;
pub mod user_stripe_data;
pub mod wallet_ledger_entries;
pub mod wallets;
@@ -0,0 +1 @@
pub mod moderator_get_user_stripe_customer_ids;
@@ -0,0 +1,43 @@
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
pub const MODERATOR_GET_USER_STRIPE_CUSTOMER_IDS_PATH: &str =
"/v1/moderation/user_stripe_data/{user_token}/customer_ids";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorGetUserStripeCustomerIdsPathInfo {
pub user_token: UserToken,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetUserStripeCustomerIdsResponse {
pub success: bool,
/// All stripe customer ids we have on file for the user, across payments
/// namespaces. A user may have zero or several: a customer link and/or
/// subscriptions per namespace (eg. artcraft), plus legacy fakeyou
/// subscriptions. The same customer id can appear once per source.
pub customer_ids: Vec<ModeratorUserStripeCustomerIdEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserStripeCustomerIdEntry {
pub stripe_customer_id: String,
pub payments_namespace: PaymentsNamespace,
pub source: ModeratorStripeCustomerIdSource,
/// Link to the customer in the Stripe dashboard for the namespace's account.
pub stripe_dashboard_url: String,
}
/// Which table the customer id was found in.
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ModeratorStripeCustomerIdSource {
/// From the `user_stripe_customer_links` table (1:1 per namespace).
CustomerLink,
/// From the user's rows in the `user_subscriptions` table.
Subscription,
}
@@ -87,7 +87,8 @@ mod tests {
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
fn json_path(file_name: &str) -> String {
format!("/Users/bt/dev/storyteller/storyteller-rust/crates/api_clients/grok_consumer_client/test_data/websocket_messages/{}", file_name)
// NB: Cargo runs tests with the crate root as the working directory.
format!("test_data/websocket_messages/{}", file_name)
}
#[test]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "completed",
"percentage_complete": 100,
"job_id": "c1bed796-3f05-4716-8ef0-a4d4900dfc6e",
"elapsed": null,
"prompt": "a dog typing on a keyboard",
"confirmation": "Drew a dog typing",
"full_prompt": "a dog typing on a keyboard",
"flags": null,
"is_alteration": false,
"section_id": 0,
"input_type": "text",
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": false,
"order": 1,
"model_name": "imagine_h_1",
"image_id": "78b3e6ef-defc-41a6-a9ac-b7a370d26ce5"
}
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "start_stage",
"percentage_complete": 0,
"job_id": "57baeffe-d5cf-483b-a5bb-7794899f905c",
"elapsed": null,
"prompt": "a dog typing on a keyboard",
"confirmation": "Drew a dog typing",
"full_prompt": "a dog typing on a keyboard",
"flags": null,
"is_alteration": true,
"section_id": 0,
"input_type": "text",
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": null,
"order": 7,
"model_name": "imagine_h_1",
"image_id": null
}
@@ -0,0 +1,21 @@
{
"type": "conversation.item.create",
"timestamp": 1761010203630,
"item": {
"type": "message",
"content": [
{
"requestId": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"text": "a dog typing on a keyboard",
"type": "input_text",
"properties": {
"section_count": 0,
"is_kids_mode": false,
"enable_nsfw": true,
"skip_upsampler": false,
"is_initial": false
}
}
]
}
}
@@ -0,0 +1,4 @@
{
"type": "other_type",
"foo": "bar"
}
@@ -0,0 +1 @@
{"type":"json","current_status":"start_stage","percentage_complete":0.0,"job_id":"0080eaac-f7f7-4369-90a2-346491939c1b","elapsed":null,"prompt":"a dog riding a motorcycle","confirmation":"Drew a dog riding a motorcycle","full_prompt":"a dog riding a motorcycle","flags":null,"is_alteration":false,"section_id":0,"input_type":"text","request_id":"bbc15402-3257-4415-922e-b156c151f3c2","asr_transcript":null,"asr_accepted":null,"seed":null,"image_reference":null,"is_image_edit":null,"moderated":false,"r_rated":null,"order":0,"model_name":"imagine_h_1","image_id":null}
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "start_stage",
"percentage_complete": 0,
"job_id": "0080eaac-f7f7-4369-90a2-346491939c1b",
"elapsed": null,
"prompt": "a dog riding a motorcycle",
"confirmation": "Drew a dog riding a motorcycle",
"full_prompt": "a dog riding a motorcycle",
"flags": null,
"is_alteration": false,
"section_id": 0,
"input_type": "text",
"request_id": "bbc15402-3257-4415-922e-b156c151f3c2",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": null,
"order": 0,
"model_name": "imagine_h_1",
"image_id": null
}
@@ -89,7 +89,15 @@ mod tests {
let cookies = format!("cookie={}", "a".repeat(10000));
assert_eq!(has_session_cookie(&cookies).unwrap(), SessionCookiePresence::MaybePresent);
let cookies = "";
// Synthetic fixture (the original real session token was redacted); the session
// cookie name is what matters for detection.
let cookies = "__Host-authjs.csrf-token=abc123; \
__Secure-authjs.callback-url=https%3A%2F%2Fsora.com%2F; \
__Secure-authjs.session-token=fake-session-token-value";
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
let cookies = "__Secure-next-auth.session-token=fake-session-token-value";
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
}
@@ -1,3 +1,4 @@
pub mod seedance2pro_bad_request_api_error;
pub mod seedance2pro_client_error;
pub mod seedance2pro_error;
pub mod seedance2pro_generic_api_error;
@@ -0,0 +1,29 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// These represent known "400s" that are the fault of the client sending a bad request.
#[derive(Debug)]
pub enum Seedance2ProBadRequestApiError {
/// The video generation request was flagged as a content violation.
VideoGenerationViolation { raw_body: String },
/// The prompt exceeded Kinovi's maximum length (10,000 characters at the
/// time of writing).
PromptIsTooLong { raw_body: String },
/// Too many media URLs were attached to the request (`uploadedUrls` is
/// capped at 9 elements at the time of writing).
TooManyUrls { raw_body: String },
}
impl Error for Seedance2ProBadRequestApiError {}
impl Display for Seedance2ProBadRequestApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::VideoGenerationViolation { raw_body } => write!(f, "Video generation violation: {}", raw_body),
Self::PromptIsTooLong { raw_body } => write!(f, "Prompt is too long: {}", raw_body),
Self::TooManyUrls { raw_body } => write!(f, "Too many urls: {}", raw_body),
}
}
}
@@ -1,3 +1,4 @@
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
@@ -9,6 +10,7 @@ pub enum Seedance2ProError {
Client(Seedance2ProClientError),
ApiSpecific(Seedance2ProSpecificApiError),
ApiGeneric(Seedance2ProGenericApiError),
ApiBadRequest(Seedance2ProBadRequestApiError),
}
impl Seedance2ProError {
@@ -38,6 +40,7 @@ impl std::fmt::Display for Seedance2ProError {
Self::Client(e) => write!(f, "Seedance2ProClientError: {:?}", e),
Self::ApiSpecific(e) => write!(f, "Seedance2ProSpecificApiError: {:?}", e),
Self::ApiGeneric(e) => write!(f, "Seedance2ProGenericApiError: {:?}", e),
Self::ApiBadRequest(e) => write!(f, "Seedance2ProBadRequestApiError: {:?}", e),
}
}
}
@@ -59,3 +62,9 @@ impl From<Seedance2ProGenericApiError> for Seedance2ProError {
Self::ApiGeneric(error)
}
}
impl From<Seedance2ProBadRequestApiError> for Seedance2ProError {
fn from(error: Seedance2ProBadRequestApiError) -> Self {
Self::ApiBadRequest(error)
}
}
@@ -16,8 +16,12 @@ pub enum Seedance2ProGenericApiError {
/// Includes the original body.
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
/// An uncategorized bad HTTP response.
UncategorizedBadResponse(String),
/// A response that parsed, but didn't have the shape we expected
/// (eg. an empty batch response array).
UnexpectedResponseShape {
explanation: String,
raw_body: String,
},
/// An uncategorized bad HTTP response with status code and body.
UncategorizedBadResponseWithStatusAndBody {
@@ -37,7 +41,7 @@ impl Display for Seedance2ProGenericApiError {
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response: {}", msg),
Self::UnexpectedResponseShape { explanation, raw_body } => write!(f, "Unexpected response shape: {}. Body: {}", explanation, raw_body),
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
}
@@ -8,9 +8,6 @@ pub enum Seedance2ProSpecificApiError {
/// The session cookies are expired or invalid.
UnauthorizedSessionExpired,
/// The video generation request was flagged as a content violation.
VideoGenerationViolation(String),
/// The account does not have enough credits to perform the operation.
BillingError {
status_code: StatusCode,
@@ -24,7 +21,6 @@ impl Display for Seedance2ProSpecificApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnauthorizedSessionExpired => write!(f, "Unauthorized: session cookies expired or invalid."),
Self::VideoGenerationViolation(body) => write!(f, "Video generation violation: {}", body),
Self::BillingError { status_code, body } => write!(f, "Billing error (status {}): {}", status_code, body),
}
}
@@ -119,9 +119,10 @@ pub async fn generate_character(args: GenerateCharacterArgs<'_>) -> Result<Gener
let data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -1,8 +1,8 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use crate::requests::generate_image::request_types::*;
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
@@ -285,15 +285,16 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
let task_data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
if task_data.violation_warning {
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
}
Ok(GenerateImageResponse {
@@ -1,8 +1,8 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use crate::requests::generate_video::request_types::*;
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
@@ -403,15 +403,16 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
let task_data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
if task_data.violation_warning {
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
}
Ok(GenerateVideoResponse {
@@ -94,9 +94,10 @@ pub async fn get_user_auth_details(
let json = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -181,9 +181,10 @@ pub async fn poll_characters(args: PollCharactersArgs<'_>) -> Result<PollCharact
let data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -201,9 +201,10 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
let json = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -106,9 +106,10 @@ pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<Prep
let upload_url = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -102,9 +102,10 @@ pub async fn update_character(args: UpdateCharacterArgs<'_>) -> Result<UpdateCha
let data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
@@ -1,8 +1,8 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use crate::requests::workflow_run_task::request_types::*;
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
@@ -415,15 +415,16 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result<Workflow
let task_data = batch_response
.into_iter()
.next()
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
"Empty batch response array".to_string()
))?
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.clone(),
})?
.result
.data
.json;
if task_data.violation_warning {
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
}
Ok(WorkflowRunTaskResponse {
@@ -1,5 +1,6 @@
use wreq::StatusCode;
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
@@ -9,11 +10,25 @@ const BILLING_ERROR_MARKERS: &[&str] = &[
"credits not enough",
];
/// Zod validation message for a string field exceeding its maximum length,
/// eg. "参数验证失败 - String must contain at most 10000 character(s)".
const STRING_TOO_LONG_MARKER: &str = "String must contain at most";
/// Zod validation message for an array field exceeding its maximum size,
/// eg. "参数验证失败 - Array must contain at most 9 element(s)".
const ARRAY_TOO_LARGE_MARKER: &str = "Array must contain at most";
/// Zod `fieldErrors` path entry for the prompt field.
const PROMPT_FIELD_MARKER: &str = "\"prompt\"";
/// Zod `fieldErrors` path entry for the uploaded media urls field.
const UPLOADED_URLS_FIELD_MARKER: &str = "\"uploadedUrls\"";
/// Categorize a non-success HTTP response into a specific or generic error.
///
/// Checks the response body for known patterns (e.g. billing errors) and returns
/// a specific error variant when possible, falling back to
/// `UncategorizedBadResponseWithStatusAndBody` otherwise.
/// Checks the response body for known patterns (e.g. billing errors, request
/// validation failures) and returns a specific error variant when possible,
/// falling back to `UncategorizedBadResponseWithStatusAndBody` otherwise.
pub fn categorize_seedance2pro_error(
status_code: StatusCode,
body: String,
@@ -25,6 +40,15 @@ pub fn categorize_seedance2pro_error(
}.into();
}
if status_code == StatusCode::BAD_REQUEST {
if is_prompt_too_long_error(&body) {
return Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: body }.into();
}
if is_too_many_urls_error(&body) {
return Seedance2ProBadRequestApiError::TooManyUrls { raw_body: body }.into();
}
}
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
status_code,
body,
@@ -37,75 +61,164 @@ fn is_billing_error(body: &str) -> bool {
BILLING_ERROR_MARKERS.iter().any(|marker| body_lower.contains(marker))
}
/// Check if the response body is a zod validation failure for an over-long prompt.
fn is_prompt_too_long_error(body: &str) -> bool {
body.contains(STRING_TOO_LONG_MARKER) && body.contains(PROMPT_FIELD_MARKER)
}
/// Check if the response body is a zod validation failure for too many uploaded urls.
fn is_too_many_urls_error(body: &str) -> bool {
body.contains(ARRAY_TOO_LARGE_MARKER) && body.contains(UPLOADED_URLS_FIELD_MARKER)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::seedance2pro_error::Seedance2ProError;
#[test]
fn real_not_enough_credits_response() {
let body = std::fs::read_to_string("test_data/responses/not_enough_credits.json")
.expect("Failed to read test data file");
mod billing_tests {
use super::*;
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
#[test]
fn real_not_enough_credits_response() {
let body = read_response_body("not_enough_credits.json");
match error {
Seedance2ProError::ApiSpecific(
Seedance2ProSpecificApiError::BillingError { status_code, body }
) => {
assert_eq!(status_code, StatusCode::BAD_REQUEST);
assert!(body.contains("credits not enough"));
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
match error {
Seedance2ProError::ApiSpecific(
Seedance2ProSpecificApiError::BillingError { status_code, body }
) => {
assert_eq!(status_code, StatusCode::BAD_REQUEST);
assert!(body.contains("credits not enough"));
}
other => panic!("Expected BillingError, got: {:?}", other),
}
other => panic!("Expected BillingError, got: {:?}", other),
}
}
#[test]
fn billing_error_case_insensitive() {
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
#[test]
fn billing_error_case_insensitive() {
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
match error {
Seedance2ProError::ApiSpecific(
Seedance2ProSpecificApiError::BillingError { .. }
) => {}
other => panic!("Expected BillingError, got: {:?}", other),
}
}
#[test]
fn unknown_error_falls_back_to_uncategorized() {
let body = r#"{"error":"something else went wrong"}"#.to_string();
let error = categorize_seedance2pro_error(
StatusCode::INTERNAL_SERVER_ERROR,
body.clone(),
);
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
) => {
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(b, body);
match error {
Seedance2ProError::ApiSpecific(
Seedance2ProSpecificApiError::BillingError { .. }
) => {}
other => panic!("Expected BillingError, got: {:?}", other),
}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
}
}
#[test]
fn empty_body_falls_back_to_uncategorized() {
let error = categorize_seedance2pro_error(
StatusCode::BAD_REQUEST,
String::new(),
);
mod bad_request_tests {
use super::*;
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
) => {}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
#[test]
fn real_prompt_too_long_response() {
let body = read_response_body("prompt_too_long.json");
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
match error {
Seedance2ProError::ApiBadRequest(
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body }
) => {
assert!(raw_body.contains("String must contain at most 10000 character(s)"));
assert!(raw_body.contains("\"prompt\""));
}
other => panic!("Expected PromptIsTooLong, got: {:?}", other),
}
}
#[test]
fn real_too_many_urls_response() {
let body = read_response_body("too_many_urls.json");
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
match error {
Seedance2ProError::ApiBadRequest(
Seedance2ProBadRequestApiError::TooManyUrls { raw_body }
) => {
assert!(raw_body.contains("Array must contain at most 9 element(s)"));
assert!(raw_body.contains("\"uploadedUrls\""));
}
other => panic!("Expected TooManyUrls, got: {:?}", other),
}
}
#[test]
fn bad_request_markers_require_400_status() {
let body = read_response_body("prompt_too_long.json");
let error = categorize_seedance2pro_error(StatusCode::INTERNAL_SERVER_ERROR, body);
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
) => {}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
}
}
#[test]
fn validation_failure_for_other_field_is_not_categorized() {
// Same zod shape, but for a field we don't have a variant for.
let body = r#"[{"error":{"json":{"message":"validation failed - String must contain at most 100 character(s)","code":-32600,"data":{"code":"BAD_REQUEST","httpStatus":400,"zodError":{"formErrors":[],"fieldErrors":{"apiParams":[{"code":"custom","message":"String must contain at most 100 character(s)","path":["apiParams","negativePrompt"],"fatal":true}]}}}}}]"#.to_string();
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
) => {}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
}
}
}
mod fallback_tests {
use super::*;
#[test]
fn unknown_error_falls_back_to_uncategorized() {
let body = r#"{"error":"something else went wrong"}"#.to_string();
let error = categorize_seedance2pro_error(
StatusCode::INTERNAL_SERVER_ERROR,
body.clone(),
);
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
) => {
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(b, body);
}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
}
}
#[test]
fn empty_body_falls_back_to_uncategorized() {
let error = categorize_seedance2pro_error(
StatusCode::BAD_REQUEST,
String::new(),
);
match error {
Seedance2ProError::ApiGeneric(
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
) => {}
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
}
}
}
fn read_response_body(filename: &str) -> String {
std::fs::read_to_string(format!("test_data/responses/{}", filename))
.expect("Failed to read test data file")
}
}
@@ -0,0 +1,40 @@
[
{
"error": {
"json": {
"message": "[{\"code\": \"custom\",\"message\": \"参数验证失败 - String must contain at most 10000 character(s)\",\"path\": [\"apiParams\",\"prompt\"],\"fatal\": true}]",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"path": "workflow.runTask",
"zodError": {
"formErrors": [],
"fieldErrors": {
"apiParams": [
{
"code": "custom",
"message": "参数验证失败 - String must contain at most 10000 character(s)",
"path": [
"apiParams",
"prompt"
],
"fatal": true
}
]
}
}
},
"stack": null
},
"meta": {
"values": {
"stack": [
"undefined"
]
},
"v": 1
}
}
}
]
@@ -0,0 +1,40 @@
[
{
"error": {
"json": {
"message": "[{\"code\": \"custom\",\"message\": \"参数验证失败 - Array must contain at most 9 element(s)\",\"path\": [\"apiParams\",\"uploadedUrls\"],\"fatal\": true}]",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"path": "workflow.runTask",
"zodError": {
"formErrors": [],
"fieldErrors": {
"apiParams": [
{
"code": "custom",
"message": "参数验证失败 - Array must contain at most 9 element(s)",
"path": [
"apiParams",
"uploadedUrls"
],
"fatal": true
}
]
}
}
},
"stack": null
},
"meta": {
"values": {
"stack": [
"undefined"
]
},
"v": 1
}
}
}
]
@@ -136,12 +136,12 @@ struct RawResponse {
#[cfg(test)]
mod tests {
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::requests::objects::begin_object_image_upload::get_url;
use crate::api::requests::objects::begin_object_image_upload::{get_url, BASE_URL};
#[test]
fn test_get_url() {
let upload_id = UploadObjectId("foo-bar-baz-bin".to_string());
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:upload";
let expected = format!("{}/foo-bar-baz-bin:upload", BASE_URL);
assert_eq!(get_url(&upload_id), expected);
}
}
@@ -147,6 +147,9 @@ mod tests {
#[test]
fn test_request() {
let request = RawRequest::for_mime_type(UploadMimeType::ImageJpeg);
assert_eq!(request.display_name, "uploaded_image_jpeg");
// The display name embeds a random uuid, eg. "uploaded_image_{uuid}.jpeg"
assert!(request.display_name.starts_with("uploaded_image_"));
assert!(request.display_name.ends_with(".jpeg"));
assert_eq!(request.mime_type, "image/jpeg");
}
}
@@ -135,7 +135,7 @@ struct RawResponse {
#[cfg(test)]
mod tests {
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs};
use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs, BASE_URL};
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
@@ -144,7 +144,7 @@ mod tests {
#[test]
fn test_get_url() {
let upload_id = UploadObjectId("foo-bar-baz-bin".to_string());
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:complete";
let expected = format!("{}/foo-bar-baz-bin:complete", BASE_URL);
assert_eq!(get_url(&upload_id), expected);
}
@@ -405,6 +405,10 @@ mod tests {
use crate::api::api_types::pano_object_id::PanoObjectId;
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::requests::objects::update_run_object_with_upload::{RawRequest, UpdateRunObjectWithUploadPayloadArgs};
use serde_json::{json, Value};
/// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis).
const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000;
#[test]
fn request_default() {
@@ -432,9 +436,88 @@ mod tests {
let request = RawRequest::from_args(&args);
let json = serde_json::to_string_pretty(&request).unwrap();
println!("{}", json);
// The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time,
// so they're nondeterministic. Sanity-check them, then zero them out before
// comparing the rest of the document.
assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS);
assert!(request.object.metadata.updated_at >= request.object.metadata.created_at);
assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#);
let mut actual = serde_json::to_value(&request).unwrap();
zero_out_timestamps(&mut actual);
let expected = json!({
"object": {
"metadata": {
"version": "0.0.1",
"createdAt": 0,
"updatedAt": 0,
"usesAdvancedEditing": false,
"draftMode": false,
"nodes": {
"bc17252b-811e-49d7-be3e-b7c538df9d30": {
"id": "bc17252b-811e-49d7-be3e-b7c538df9d30",
"type": "input",
"parentId": null,
"source": {
"type": "image",
"image_url": "https://todo.com"
},
"createdAt": 0
},
"82f6b488-8afe-4311-9022-80ad5789ad92": {
"id": "82f6b488-8afe-4311-9022-80ad5789ad92",
"type": "pano",
"parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30",
"source": {
"type": "generate_world",
"world_id": "pending",
"status": "pending"
},
"createdAt": 0
},
"f08ec501-601c-47c6-a104-402d1820b8f0": {
"id": "f08ec501-601c-47c6-a104-402d1820b8f0",
"type": "world",
"parentId": "82f6b488-8afe-4311-9022-80ad5789ad92",
"source": {
"type": "generate_world",
"world_id": "pending",
"posed_cubemaps_url": "",
"minimap_url": "",
"minimap_metadata": "",
"status": "pending"
},
"createdAt": 0
}
}
}
},
"update_mask": [
"metadata"
]
});
assert_eq!(actual, expected);
}
/// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0.
fn zero_out_timestamps(value: &mut Value) {
match value {
Value::Object(map) => {
for (key, child) in map.iter_mut() {
if key == "createdAt" || key == "updatedAt" {
*child = json!(0);
} else {
zero_out_timestamps(child);
}
}
},
Value::Array(items) => {
for item in items.iter_mut() {
zero_out_timestamps(item);
}
},
_ => {},
}
}
}
@@ -411,6 +411,10 @@ mod tests {
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
use crate::api::api_types::world_id::WorldObjectId;
use crate::api::requests::objects::update_run_object_with_world::{RawRequest, UpdateRunObjectWithWorldPayloadArgs};
use serde_json::{json, Value};
/// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis).
const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000;
#[test]
fn request_default() {
@@ -446,9 +450,91 @@ mod tests {
let request = RawRequest::from_args(&args);
let json = serde_json::to_string_pretty(&request).unwrap();
println!("{}", json);
// The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time,
// so they're nondeterministic. Sanity-check them, then zero them out before
// comparing the rest of the document.
assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS);
assert!(request.object.metadata.updated_at >= request.object.metadata.created_at);
assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#);
let mut actual = serde_json::to_value(&request).unwrap();
zero_out_timestamps(&mut actual);
let expected = json!({
"object": {
"metadata": {
"version": "0.0.1",
"name": title,
"createdAt": 0,
"updatedAt": 0,
"usesAdvancedEditing": false,
"draftMode": false,
"nodes": {
"bc17252b-811e-49d7-be3e-b7c538df9d30": {
"id": "bc17252b-811e-49d7-be3e-b7c538df9d30",
"type": "input",
"parentId": null,
"source": {
"type": "image",
"image_url": "https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg"
},
"createdAt": 0,
"title": title,
"caption": caption
},
"82f6b488-8afe-4311-9022-80ad5789ad92": {
"id": "82f6b488-8afe-4311-9022-80ad5789ad92",
"type": "pano",
"parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30",
"source": {
"type": "generate_world",
"world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec",
"status": "pending"
},
"createdAt": 0
},
"f08ec501-601c-47c6-a104-402d1820b8f0": {
"id": "f08ec501-601c-47c6-a104-402d1820b8f0",
"type": "world",
"parentId": "82f6b488-8afe-4311-9022-80ad5789ad92",
"source": {
"type": "generate_world",
"world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec",
"posed_cubemaps_url": "",
"minimap_url": "",
"minimap_metadata": "",
"status": "pending"
},
"createdAt": 0
}
}
}
},
"update_mask": [
"metadata"
]
});
assert_eq!(actual, expected);
}
/// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0.
fn zero_out_timestamps(value: &mut Value) {
match value {
Value::Object(map) => {
for (key, child) in map.iter_mut() {
if key == "createdAt" || key == "updatedAt" {
*child = json!(0);
} else {
zero_out_timestamps(child);
}
}
},
Value::Array(items) => {
for item in items.iter_mut() {
zero_out_timestamps(item);
}
},
_ => {},
}
}
}
@@ -151,7 +151,7 @@ struct SpzUrls {
#[cfg(test)]
mod tests {
use crate::api::api_types::world_id::WorldObjectId;
use crate::api::requests::worlds::poll_world_status::{get_url, poll_world_status, PollWorldStatusArgs};
use crate::api::requests::worlds::poll_world_status::{get_url, poll_world_status, PollWorldStatusArgs, BASE_URL};
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
@@ -160,7 +160,7 @@ mod tests {
#[test]
fn test_get_url() {
let world_id = WorldObjectId("d60cfa21-5506-43de-9a9c-0707fc17a5ec".to_string());
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds/d60cfa21-5506-43de-9a9c-0707fc17a5ec";
let expected = format!("{}/d60cfa21-5506-43de-9a9c-0707fc17a5ec", BASE_URL);
assert_eq!(get_url(&world_id), expected);
}
@@ -407,6 +407,7 @@ mod tests {
}
#[test]
#[ignore] // Transliteration for the time-bombed characters (α, đ, ß) was never implemented.
pub fn most_frequent_failures_2p5k_usages() {
assert_converted("~", "~"); // b'\\uff5e' 2507
time_bomb_ignore(|| assert_converted("â", "a")); // b'\xe2' 2607
@@ -429,6 +430,7 @@ mod tests {
}
#[test]
#[ignore] // Transliteration for the time-bombed characters (ę, ě, ᴾ, ł) was never implemented.
pub fn most_frequent_failures_2k_usages() {
assert_converted("、", ","); // b'\\u3001' 2001
assert_converted("ć", "c"); // b'\\u0107' 2017
@@ -442,6 +444,7 @@ mod tests {
}
#[test]
#[ignore] // Transliteration for the time-bombed characters (ū, ι, ε) was never implemented.
pub fn most_frequent_failures_1p5k_usages() {
assert_converted("·", " "); // b'\xb7' 1514
assert_converted("š", "s"); // b'\\u0161' 1514
@@ -6,6 +6,7 @@ use sqlx::{MySql, MySqlPool};
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -30,6 +31,8 @@ pub struct BeebleJobDetails {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
pub maybe_platform_type: Option<PlatformType>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -53,6 +56,8 @@ struct RawJobRecord {
maybe_prompt_token: Option<PromptToken>,
maybe_debug_log_event_token: Option<DebugLogEventToken>,
maybe_platform_type: Option<PlatformType>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -87,6 +92,8 @@ SELECT
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,
jobs.created_at,
jobs.updated_at
@@ -128,6 +135,7 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<BeebleJobDe
creator_ip_address: record.creator_ip_address,
maybe_prompt_token: record.maybe_prompt_token,
maybe_debug_log_event_token: record.maybe_debug_log_event_token,
maybe_platform_type: record.maybe_platform_type,
created_at: record.created_at,
updated_at: record.updated_at,
})
@@ -9,6 +9,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -46,6 +47,9 @@ pub struct InsertGenericInferenceForBeebleWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
/// Override the initial job status. Defaults to `Pending` when `None`.
@@ -83,6 +87,7 @@ pub async fn insert_generic_inference_job_for_beeble_queue_with_apriori_job_toke
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -26,6 +26,7 @@ use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobTyp
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -71,6 +72,9 @@ pub(crate) struct InsertGenericInferenceJobForProviderArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
@@ -114,6 +118,8 @@ pub(crate) async fn insert_generic_inference_job_for_provider<'e, 'c: 'e, E>(
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
@@ -6,6 +6,7 @@ use sqlx::{MySql, MySqlPool};
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -32,6 +33,8 @@ pub struct FalJobDetails {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
pub maybe_platform_type: Option<PlatformType>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -55,6 +58,8 @@ struct RawJobRecord {
maybe_prompt_token: Option<PromptToken>,
maybe_debug_log_event_token: Option<DebugLogEventToken>,
maybe_platform_type: Option<PlatformType>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -93,6 +98,8 @@ SELECT
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,
jobs.created_at,
jobs.updated_at
@@ -134,6 +141,7 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetai
creator_ip_address: record.creator_ip_address,
maybe_prompt_token: record.maybe_prompt_token,
maybe_debug_log_event_token: record.maybe_debug_log_event_token,
maybe_platform_type: record.maybe_platform_type,
created_at: record.created_at,
updated_at: record.updated_at,
})
@@ -2,6 +2,7 @@ use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -41,6 +42,9 @@ pub struct InsertGenericInferenceForFalArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
@@ -66,6 +70,7 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -5,6 +5,7 @@ use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -41,6 +42,9 @@ pub struct InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
@@ -69,6 +73,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -9,6 +9,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -49,6 +50,9 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Override the initial job status. Defaults to `Pending` when `None`.
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
@@ -105,6 +109,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -33,6 +34,7 @@ where
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -60,6 +62,7 @@ where
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -27,6 +28,8 @@ pub struct PendingGmiCloudJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -39,6 +42,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal GmiCloud jobs that have an associated request_id.
@@ -54,7 +58,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -90,6 +95,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -33,6 +34,7 @@ where
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -60,6 +62,7 @@ where
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -2,6 +2,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -25,6 +26,8 @@ pub struct PendingGrokApiJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -37,6 +40,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal Grok (xAI) API jobs that have an associated request_id.
@@ -52,7 +56,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -87,6 +92,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -63,6 +63,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_character_with_aprior
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -50,6 +51,9 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
@@ -126,6 +130,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_queue_with_apriori_jo
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -26,6 +27,8 @@ pub struct PendingSeedance2ProJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -38,6 +41,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal Seedance2Pro video jobs that have an associated order_id.
@@ -57,7 +61,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -94,6 +99,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -45,6 +46,9 @@ pub struct InsertGenericInferenceForWorldlabsWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -71,6 +75,7 @@ pub async fn insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_t
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -27,6 +28,8 @@ pub struct PendingWorldlabsJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -39,6 +42,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal WorldLabs jobs that have an associated operation_id.
@@ -54,7 +58,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -90,6 +95,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -27,6 +27,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -76,6 +77,9 @@ pub(crate) struct InsertFullGenericInferenceJobRecordArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub priority_level: u8,
pub requires_keepalive: bool,
pub max_duration_seconds: i32,
@@ -157,6 +161,8 @@ SET
creator_ip_address = ?,
creator_set_visibility = ?,
platform_type = ?,
priority_level = ?,
is_keepalive_required = ?,
max_duration_seconds = ?,
@@ -203,6 +209,8 @@ SET
args.creator_ip_address,
args.creator_set_visibility.to_str(),
args.maybe_platform_type.map(|p| p.to_str()),
args.priority_level,
args.requires_keepalive,
args.max_duration_seconds,
@@ -121,6 +121,8 @@ pub async fn insert_generic_inference_job(args: InsertGenericInferenceArgs<'_>)
maybe_routing_tag: args.maybe_routing_tag,
maybe_platform_type: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -10,6 +10,7 @@ use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginM
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -92,6 +93,10 @@ pub struct InsertArgs<'a> {
/// If provided, the third-party provider that generated this file.
pub maybe_generation_provider: Option<GenerationProvider>,
/// The platform the creating request came from, inferred from its User-Agent.
/// For inference results, this is copied from the originating job.
pub maybe_platform_type: Option<PlatformType>,
// Cover image (e.g. thumbnail for 3D splats)
pub maybe_cover_image_media_file_token: Option<&'a MediaFileToken>,
@@ -204,6 +209,8 @@ pub async fn insert_media_file_generic(
maybe_generation_provider = ?,
platform_type = ?,
maybe_cover_image_media_file_token = ?,
maybe_mod_user_token = ?,
@@ -262,6 +269,8 @@ pub async fn insert_media_file_generic(
maybe_generation_provider_str,
args.maybe_platform_type.map(|p| p.to_str()),
args.maybe_cover_image_media_file_token.map(|t| t.as_str()),
args.maybe_mod_user_token,
@@ -121,6 +121,7 @@ pub async fn insert_media_file_generic_from_job(
maybe_title: args.maybe_title,
maybe_video_encoding: args.maybe_video_encoding,
maybe_generation_provider: None,
maybe_platform_type: None,
maybe_cover_image_media_file_token: None,
media_class: args.media_class,
media_type: args.media_type,
@@ -10,6 +10,7 @@ use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use sqlx::MySqlPool;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -82,6 +83,10 @@ pub struct MediaFileInsertBuilder {
// Generation details
maybe_generation_provider: Option<GenerationProvider>,
// The platform the creating request came from. For inference results, this
// is copied from the originating generic_inference_jobs row.
maybe_platform_type: Option<PlatformType>,
}
impl MediaFileInsertBuilder {
@@ -116,6 +121,7 @@ impl MediaFileInsertBuilder {
public_bucket_directory_hash: None,
maybe_prompt_token: None,
maybe_generation_provider: None,
maybe_platform_type: None,
}
}
@@ -272,6 +278,11 @@ impl MediaFileInsertBuilder {
self
}
pub fn maybe_platform_type(mut self, maybe_platform_type: Option<PlatformType>) -> Self {
self.maybe_platform_type = maybe_platform_type;
self
}
// TODO(bt,2025-04-26): Other connector options.
pub async fn insert_pool(self, mysql_pool: &MySqlPool) -> Result<MediaFileToken, MediaFileInsertBuilderError> {
let media_file_type = self.media_file_type
@@ -341,6 +352,7 @@ impl MediaFileInsertBuilder {
maybe_creator_category_synthetic_id_category: IdCategory::FileUpload, // TODO: Remove this
maybe_extra_media_info: None, // TODO
maybe_generation_provider: self.maybe_generation_provider,
maybe_platform_type: self.maybe_platform_type,
maybe_cover_image_media_file_token: self.maybe_cover_image_media_file_token.as_ref(),
is_generated_on_prem: false, // TODO
generated_by_worker: None, // TODO
@@ -112,6 +112,7 @@ pub async fn insert_media_file_from_studio_scene_render(
maybe_mod_user_token: None,
maybe_engine_category: None,
maybe_generation_provider: None,
maybe_platform_type: None,
maybe_cover_image_media_file_token: None,
}).await?;
@@ -0,0 +1,50 @@
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use enums::common::payments_namespace::PaymentsNamespace;
use tokens::tokens::users::UserToken;
use crate::queries::users::user_stripe_customer_links::find_user_stripe_customer_link::UserStripeCustomerLink;
/// List every stripe customer link for a user, across all payments namespaces.
/// Users have at most one link per namespace.
pub async fn list_user_stripe_customer_links_for_user(
user_token: &UserToken,
connection: &mut PoolConnection<MySql>,
) -> Result<Vec<UserStripeCustomerLink>, sqlx::Error> {
let records = sqlx::query_as!(
RawUserStripeCustomerLink,
r#"
SELECT
user_token as `user_token: tokens::tokens::users::UserToken`,
payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,
stripe_customer_id
FROM user_stripe_customer_links
WHERE user_token = ?
ORDER BY id ASC
"#,
user_token.as_str(),
)
.fetch_all(&mut **connection)
.await?;
let links = records
.into_iter()
.map(|record| UserStripeCustomerLink {
user_token: record.user_token,
payments_namespace: record.payments_namespace,
stripe_customer_id: record.stripe_customer_id,
})
.collect();
Ok(links)
}
#[derive(sqlx::FromRow)]
struct RawUserStripeCustomerLink {
user_token: UserToken,
payments_namespace: PaymentsNamespace,
stripe_customer_id: String,
}
@@ -1,2 +1,3 @@
pub mod find_user_stripe_customer_link;
pub mod upsert_user_stripe_customer_link;
pub mod list_user_stripe_customer_links_for_user;
pub mod upsert_user_stripe_customer_link;
@@ -0,0 +1,40 @@
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use enums::common::payments_namespace::PaymentsNamespace;
use tokens::tokens::users::UserToken;
/// A distinct stripe customer id attached to one of the user's subscriptions.
pub struct UserSubscriptionStripeCustomerId {
pub subscription_namespace: PaymentsNamespace,
pub stripe_customer_id: String,
}
/// List the distinct stripe customer ids across all of a user's subscriptions
/// (current and historical), across all payments namespaces.
pub async fn list_stripe_customer_ids_for_user_subscriptions(
user_token: &UserToken,
connection: &mut PoolConnection<MySql>,
) -> Result<Vec<UserSubscriptionStripeCustomerId>, sqlx::Error> {
let records = sqlx::query_as!(
UserSubscriptionStripeCustomerId,
r#"
SELECT DISTINCT
subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,
maybe_stripe_customer_id as `stripe_customer_id!`
FROM user_subscriptions
WHERE
user_token = ?
AND maybe_stripe_customer_id IS NOT NULL
AND deleted_at IS NULL
ORDER BY subscription_namespace ASC, maybe_stripe_customer_id ASC
"#,
user_token.as_str(),
)
.fetch_all(&mut **connection)
.await?;
Ok(records)
}
@@ -1,7 +1,8 @@
pub mod find_possibly_inactive_first_subscription_for_owner_user;
pub mod lookup_users_by_stripe_customer_id;
pub mod find_subscription_for_owner_user;
pub mod get_user_subscription_by_stripe_subscription_id;
pub mod list_active_user_subscriptions;
pub mod list_stripe_customer_ids_for_user_subscriptions;
pub mod lookup_users_by_stripe_customer_id;
pub mod upsert_user_subscription_by_stripe_id;
pub mod upsert_user_subscription_with_invoice_paid_status_by_stripe_id;
@@ -222,14 +222,15 @@ mod tests {
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::LivePortrait));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::RvcV2));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SadTalker));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SeedVc));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SoVitsSvc));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::Tacotron2));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::MocapNet));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SeedVc));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StyleTTS2));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StableDiffusion15));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::GptSovits));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StorytellerStudio));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StorytellerStudioImageGen));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::VideoStyleTransfer));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::ComfyUi));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::VallEX));
@@ -117,7 +117,9 @@ mod tests {
fn test_serialization() {
assert_serialization(PromptContextSemanticType::VidStartFrame, "vid_start_frame");
assert_serialization(PromptContextSemanticType::VidEndFrame, "vid_end_frame");
assert_serialization(PromptContextSemanticType::VidRef, "vidref");
// NB: serde serialization ("vid_ref") intentionally differs from to_str()/the DB value
// ("vidref"); see the "Current DB values" comment at the top of this file.
assert_serialization(PromptContextSemanticType::VidRef, "vid_ref");
assert_serialization(PromptContextSemanticType::Imgsrc, "imgsrc");
assert_serialization(PromptContextSemanticType::Imgmask, "imgmask");
assert_serialization(PromptContextSemanticType::Imgref, "imgref");
@@ -5,6 +5,7 @@ pub mod generation_provider;
pub mod job_status;
pub mod job_status_plus;
pub mod payments_namespace;
pub mod platform_type;
pub mod stripe_subscription_status;
pub mod view_as;
pub mod visibility;
@@ -0,0 +1,163 @@
//! The platform a request originated from, inferred from the User-Agent header.
//!
//! Stored on `generic_inference_jobs` (set at enqueue time) and copied to the
//! resulting `media_files` rows when jobs complete.
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// NB: This is used by multiple tables (`generic_inference_jobs`, `media_files`).
/// Keep the max length to 16 characters.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PlatformType {
// ========== Our clients ==========
/// Website users
Web,
/// Desktop app users
DesktopApp,
// ========== Other clients ==========
Curl,
PythonRequests,
Postman,
}
impl_enum_display_and_debug_using_to_str!(PlatformType);
impl_mysql_enum_coders!(PlatformType);
impl_mysql_from_row!(PlatformType);
impl PlatformType {
pub fn to_str(&self) -> &'static str {
match self {
Self::Web => "web",
Self::DesktopApp => "desktop_app",
Self::Curl => "curl",
Self::PythonRequests => "python_requests",
Self::Postman => "postman",
}
}
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"web" => Ok(Self::Web),
"desktop_app" => Ok(Self::DesktopApp),
"curl" => Ok(Self::Curl),
"python_requests" => Ok(Self::PythonRequests),
"postman" => Ok(Self::Postman),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
BTreeSet::from([
Self::Web,
Self::DesktopApp,
Self::Curl,
Self::PythonRequests,
Self::Postman,
])
}
}
#[cfg(test)]
mod tests {
use crate::common::platform_type::PlatformType;
use crate::error::enum_error::EnumError;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(PlatformType::Web, "web");
assert_serialization(PlatformType::DesktopApp, "desktop_app");
assert_serialization(PlatformType::Curl, "curl");
assert_serialization(PlatformType::PythonRequests, "python_requests");
assert_serialization(PlatformType::Postman, "postman");
}
#[test]
fn to_str() {
assert_eq!(PlatformType::Web.to_str(), "web");
assert_eq!(PlatformType::DesktopApp.to_str(), "desktop_app");
assert_eq!(PlatformType::Curl.to_str(), "curl");
assert_eq!(PlatformType::PythonRequests.to_str(), "python_requests");
assert_eq!(PlatformType::Postman.to_str(), "postman");
}
#[test]
fn from_str() {
assert_eq!(PlatformType::from_str("web").unwrap(), PlatformType::Web);
assert_eq!(PlatformType::from_str("desktop_app").unwrap(), PlatformType::DesktopApp);
assert_eq!(PlatformType::from_str("curl").unwrap(), PlatformType::Curl);
assert_eq!(PlatformType::from_str("python_requests").unwrap(), PlatformType::PythonRequests);
assert_eq!(PlatformType::from_str("postman").unwrap(), PlatformType::Postman);
}
#[test]
fn from_str_err() {
let result = PlatformType::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = PlatformType::all_variants();
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(PlatformType::Web));
assert_eq!(variants.pop_first(), Some(PlatformType::DesktopApp));
assert_eq!(variants.pop_first(), Some(PlatformType::Curl));
assert_eq!(variants.pop_first(), Some(PlatformType::PythonRequests));
assert_eq!(variants.pop_first(), Some(PlatformType::Postman));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(PlatformType::all_variants().len(), PlatformType::iter().len());
}
#[test]
fn round_trip() {
for variant in PlatformType::all_variants() {
// Test to_str(), from_str(), Display, and Debug.
assert_eq!(variant, PlatformType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, PlatformType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, PlatformType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 16;
for variant in PlatformType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -121,6 +121,7 @@ pub async fn process_successful_job(
.file_size_bytes(video_bytes.len() as u64)
.checksum_sha2(&checksum)
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.maybe_cover_image_media_file_token(maybe_cover_token.as_ref())
.public_bucket_directory_hash(&bucket_path)
.insert_pool(&deps.mysql_pool)
@@ -226,6 +227,7 @@ async fn download_and_upload_thumbnail(
.file_size_bytes(thumb_bytes.len() as u64)
.checksum_sha2(&checksum)
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.public_bucket_directory_hash(&bucket_path)
.insert_pool(&deps.mysql_pool)
.await
@@ -137,6 +137,7 @@ async fn download_and_finalize_video(
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Video)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoGeneration)
@@ -178,6 +178,7 @@ async fn download_and_store_one_image(
.maybe_frame_width(Some(result.width))
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Image)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::ImageGeneration)
@@ -131,6 +131,7 @@ pub async fn process_successful_job(
.maybe_frame_width(maybe_frame_width)
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Video)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoGeneration)
@@ -121,6 +121,7 @@ pub async fn process_successful_job(
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Dimensional)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::WorldGeneration)
@@ -236,6 +237,7 @@ async fn download_and_upload_thumbnail(
.file_size_bytes(thumb_bytes.len() as u64)
.checksum_sha2(&checksum)
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.public_bucket_directory_hash(&bucket_path)
.insert_pool(&deps.mysql_pool)
.await
@@ -127,7 +127,7 @@ mod tests {
{"id":"sub_1LhA3MEU5se17MekeWvmTNyk","automatic_tax":{"enabled":false},
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","default_tax_rates":[],
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","currency":"usd","default_tax_rates":[],
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"deleted":false,"metadata":{},
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
@@ -172,7 +172,7 @@ mod tests {
"cancel_at_period_end":false,"collection_method":"charge_automatically","created":1662948037,
"current_period_end":1665540037,"current_period_start":1662948037,
"customer":"cus_MPrgIen5Wh6QKG",
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","default_tax_rates":[],
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","currency":"usd","default_tax_rates":[],
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"deleted":false,"metadata":{},
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
@@ -216,7 +216,7 @@ mod tests {
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,"canceled_at":1662979740,
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox",
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","default_tax_rates":[],
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","currency":"usd","default_tax_rates":[],
"ended_at":1662979740,"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,
"deleted":false,"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
"billing_scheme":"per_unit","created":1662278586,"currency":"usd","deleted":false,
@@ -127,8 +127,11 @@ mod tests {
{"id":"sub_1LhA3MEU5se17MekeWvmTNyk","automatic_tax":{"enabled":false},
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","default_tax_rates":[],
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"deleted":false,"metadata":{},
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","currency":"usd","default_tax_rates":[],"discounts":[],
"invoice_settings":{"issuer":{"type":"self"}},
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"current_period_end":1665571188,
"current_period_start":1662979188,"deleted":false,"discounts":[],
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
"product":"prod_MMxi2J5y69VPbO","recurring":{"interval":"month","interval_count":1,
@@ -172,8 +175,11 @@ mod tests {
"cancel_at_period_end":false,"collection_method":"charge_automatically","created":1662948037,
"current_period_end":1665540037,"current_period_start":1662948037,
"customer":"cus_MPrgIen5Wh6QKG",
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","default_tax_rates":[],
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"deleted":false,"metadata":{},
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","currency":"usd","default_tax_rates":[],"discounts":[],
"invoice_settings":{"issuer":{"type":"self"}},
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"current_period_end":1665540037,
"current_period_start":1662948037,"deleted":false,"discounts":[],
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
"product":"prod_MMxi2J5y69VPbO","recurring":{"interval":"month","interval_count":1,
@@ -216,9 +222,12 @@ mod tests {
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,"canceled_at":1662979740,
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox",
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","default_tax_rates":[],
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","currency":"usd","default_tax_rates":[],"discounts":[],
"invoice_settings":{"issuer":{"type":"self"}},
"ended_at":1662979740,"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,
"deleted":false,"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
"current_period_end":1665571188,"current_period_start":1662979188,
"deleted":false,"discounts":[],
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
"billing_scheme":"per_unit","created":1662278586,"currency":"usd","deleted":false,
"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO",
"recurring":{"interval":"month","interval_count":1,"usage_type":"licensed"},
@@ -283,7 +292,14 @@ mod tests {
],
"description": null,
"discount": null,
"discounts": [
],
"ended_at": null,
"invoice_settings": {
"issuer": {
"type": "self"
}
},
"items": {
"object": "list",
"data": [
@@ -292,6 +308,9 @@ mod tests {
"object": "subscription_item",
"billing_thresholds": null,
"created": 1663430298,
"current_period_end": 1666022298,
"current_period_start": 1663430298,
"discounts": [],
"metadata": {
},
"plan": {
@@ -2,6 +2,8 @@ use stripe::Client;
#[derive(Clone)]
pub struct ArtcraftStripeConfig {
/// The Stripe account id (eg. "acct_..."), used to build dashboard links.
pub stripe_account_id: String,
pub secret_key: String,
pub secret_webhook_signing_key: String,
pub checkout_success_url: String,
@@ -11,6 +13,8 @@ pub struct ArtcraftStripeConfig {
#[derive(Clone)]
pub struct ArtcraftStripeConfigWithClient {
/// The Stripe account id (eg. "acct_..."), used to build dashboard links.
pub stripe_account_id: String,
pub secret_key: String,
pub secret_webhook_signing_key: String,
pub checkout_success_url: String,
@@ -22,6 +26,7 @@ pub struct ArtcraftStripeConfigWithClient {
impl ArtcraftStripeConfig {
pub fn to_config_with_client(&self) -> ArtcraftStripeConfigWithClient {
ArtcraftStripeConfigWithClient {
stripe_account_id: self.stripe_account_id.clone(),
secret_key: self.secret_key.clone(),
secret_webhook_signing_key: self.secret_webhook_signing_key.clone(),
checkout_success_url: self.checkout_success_url.clone(),
@@ -117,6 +117,7 @@ use artcraft_api_defs::folders::media_files::*;
use artcraft_api_defs::folders::subfolder::*;
use artcraft_api_defs::moderation::user_referrals::list_global_user_referrals::*;
use artcraft_api_defs::moderation::user_referrals::list_user_referrals_for_user::*;
use artcraft_api_defs::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*;
@@ -558,6 +559,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
crate::http_server::endpoints::moderation::user_emails::moderator_change_user_email_handler::moderator_change_user_email_handler,
crate::http_server::endpoints::moderation::user_emails::moderator_list_email_address_changes_for_user_handler::moderator_list_email_address_changes_for_user_handler,
crate::http_server::endpoints::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids_handler::moderator_get_user_stripe_customer_ids_handler,
// Folders
crate::http_server::endpoints::folders::folder::create_folder_handler::create_folder_handler,
@@ -816,6 +818,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ModeratorGetWalletLedgerEntryPathInfo,
ModeratorGetWalletLedgerEntryResponse,
ModeratorGetWalletLedgerEntryDetails,
ModeratorGetUserStripeCustomerIdsPathInfo,
ModeratorGetUserStripeCustomerIdsResponse,
ModeratorUserStripeCustomerIdEntry,
ModeratorStripeCustomerIdSource,
GptImage1EditImageRequest,
GptImage1EditImageImageSize,
GptImage1EditImageNumImages,
@@ -123,14 +123,14 @@ mod tests {
assert_eq!(cover_image.maybe_cover_image_public_bucket_path, Some("/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png".to_string()));
assert_eq!(cover_image.maybe_cover_image_public_bucket_url,
Some(Url::parse("https://storage.googleapis.com/vocodes-public/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap()));
Some(Url::parse("https://cdn-2.fakeyou.com/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap()));
assert_eq!(cover_image.default_cover.image_index, 18);
let links = cover_image.maybe_links.unwrap();
assert_eq!(links.cdn_url, Url::parse(
"https://cdn.storyteller.ai/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap());
"https://cdn-2.fakeyou.com/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap());
assert_eq!(links.thumbnail_template,
"https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png");
"https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png");
}
}
@@ -41,6 +41,11 @@ pub fn is_job_failure_test(prompt: &str) -> bool {
mod tests {
use super::*;
const EXPECTED_FAILURE_MESSAGE: &str =
"This was a simulated failure. This message comes from the backend and \
database to simulate a dynamic failure message. This message could in \
theory be anything.";
#[test]
fn no_trigger_returns_none() {
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
@@ -50,14 +55,14 @@ mod tests {
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
assert_eq!(result.frontend_failure_message.as_deref(), Some(EXPECTED_FAILURE_MESSAGE));
}
#[test]
fn trigger_test_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
assert_eq!(result.frontend_failure_message.as_deref(), Some(EXPECTED_FAILURE_MESSAGE));
}
#[test]
@@ -0,0 +1,85 @@
//! Maps Kinovi (Seedance2Pro) client errors to web errors.
//!
//! Known bad requests (content violations, over-long prompts, too many urls)
//! become 400s with a user-facing message. Everything else is unanticipated
//! and stays a 500.
use seedance2pro_client::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
use crate::http_server::common_responses::common_web_error::CommonWebError;
/// Map a raw Kinovi client error to a web error.
pub fn map_seedance2pro_error_to_web_error(error: Seedance2ProError) -> CommonWebError {
match error {
Seedance2ProError::ApiBadRequest(bad_request) => {
CommonWebError::BadInputWithSimpleMessage(user_facing_bad_request_message(&bad_request))
}
other => CommonWebError::from_error(other),
}
}
fn user_facing_bad_request_message(error: &Seedance2ProBadRequestApiError) -> String {
match error {
Seedance2ProBadRequestApiError::VideoGenerationViolation { .. } => {
"The generation request was flagged as a content violation. \
Please adjust your prompt or input media and try again.".to_string()
}
Seedance2ProBadRequestApiError::PromptIsTooLong { .. } => {
"The prompt is too long. Please shorten it below 10,000 characters and try again.".to_string()
}
Seedance2ProBadRequestApiError::TooManyUrls { .. } => {
"Too many input files were attached. \
Please remove some and try again.".to_string()
}
}
}
#[cfg(test)]
mod tests {
use actix_web::ResponseError;
use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use super::*;
#[test]
fn bad_requests_become_400s_with_user_facing_messages() {
let cases = [
(
Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: "{}".to_string() },
"content violation",
),
(
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: "{}".to_string() },
"prompt is too long",
),
(
Seedance2ProBadRequestApiError::TooManyUrls { raw_body: "{}".to_string() },
"Too many input files",
),
];
for (bad_request, expected_message_fragment) in cases {
let error = map_seedance2pro_error_to_web_error(
Seedance2ProError::ApiBadRequest(bad_request));
assert_eq!(error.status_code().as_u16(), 400);
match error {
CommonWebError::BadInputWithSimpleMessage(message) => {
assert!(
message.contains(expected_message_fragment),
"message {:?} should contain {:?}", message, expected_message_fragment);
}
other => panic!("Expected BadInputWithSimpleMessage, got: {:?}", other),
}
}
}
#[test]
fn unanticipated_errors_stay_500s() {
let error = map_seedance2pro_error_to_web_error(
Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::UnauthorizedSessionExpired));
assert_eq!(error.status_code().as_u16(), 500);
}
}
@@ -1,3 +1,4 @@
pub mod job_failure_test;
pub mod map_seedance2pro_web_errors;
pub mod payments_error_test;
mod test_synthetic_failure_reason;
@@ -4,6 +4,7 @@ use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list;
use actix_web::web::Json;
@@ -239,6 +240,7 @@ pub async fn flux_2_lora_edit_image_angle_handler(
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -4,6 +4,7 @@ use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_image_urls_as_optional_list::lookup_image_urls_as_optional_list;
use actix_web::web::Json;
@@ -240,6 +241,7 @@ pub async fn qwen_edit_2511_edit_image_angle_handler(
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -4,6 +4,7 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
@@ -159,6 +160,7 @@ pub async fn remove_image_background_handler(
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
mysql_executor: &mut *mysql_connection,
phantom: Default::default(),
}).await;
@@ -6,6 +6,7 @@ use crate::http_server::common_responses::media::media_links_builder::MediaLinks
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
@@ -277,6 +278,7 @@ pub async fn flux_pro_kontext_max_edit_image_handler(
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
@@ -6,6 +6,7 @@ use crate::http_server::common_responses::media::media_links_builder::MediaLinks
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
@@ -264,6 +265,7 @@ pub async fn gemini_25_flash_edit_image_handler(
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,

Some files were not shown because too many files have changed in this diff Show More