test: stabilize merged PR checks

This commit is contained in:
fawney19
2026-05-19 10:18:25 +08:00
parent d2b42e91d2
commit 052de6b96e
25 changed files with 726 additions and 77 deletions

View File

@@ -138,3 +138,80 @@ pub(crate) fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<serde
pub(crate) fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_formats::api::aggregate_gemini_stream_sync_response(body)
}
pub(crate) fn gemini_generate_content_response_has_visible_output(
body: &serde_json::Value,
) -> bool {
if aether_ai_formats::formats::gemini::generate_content::response::from_raw(body).is_some() {
return true;
}
openai_chat_response_has_visible_output(body) || openai_responses_body_has_visible_output(body)
}
fn openai_chat_response_has_visible_output(body: &serde_json::Value) -> bool {
body.get("choices")
.and_then(serde_json::Value::as_array)
.is_some_and(|choices| {
choices.iter().any(|choice| {
choice
.get("message")
.or_else(|| choice.get("delta"))
.is_some_and(message_like_value_has_visible_output)
|| value_has_non_empty_text(choice.get("text"))
|| choice
.get("finish_reason")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| !value.trim().is_empty() && value != "length")
})
})
}
fn openai_responses_body_has_visible_output(body: &serde_json::Value) -> bool {
body.get("output")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| {
items.iter().any(|item| {
item.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "function_call" | "image_generation_call"))
|| item
.get("content")
.and_then(serde_json::Value::as_array)
.is_some_and(|content| {
content.iter().any(response_content_has_visible_output)
})
})
})
}
fn message_like_value_has_visible_output(value: &serde_json::Value) -> bool {
value_has_non_empty_text(value.get("content"))
|| value
.get("tool_calls")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| !items.is_empty())
}
fn response_content_has_visible_output(value: &serde_json::Value) -> bool {
value_has_non_empty_text(value.get("text"))
|| value
.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "function_call" | "output_image"))
}
fn value_has_non_empty_text(value: Option<&serde_json::Value>) -> bool {
match value {
Some(serde_json::Value::String(text)) => !text.trim().is_empty(),
Some(serde_json::Value::Array(items)) => items.iter().any(|item| {
value_has_non_empty_text(item.get("text"))
|| value_has_non_empty_text(item.get("content"))
|| item
.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| matches!(kind, "image_url" | "input_image"))
}),
_ => false,
}
}

View File

@@ -13,6 +13,7 @@ use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
pub(crate) use self::adaptation::{
maybe_build_provider_private_stream_normalizer, ProviderPrivateStreamNormalizer,
};
pub(crate) use self::api::gemini_generate_content_response_has_visible_output;
pub(crate) use self::finalize::common::LocalCoreSyncFinalizeOutcome;
pub(crate) use self::finalize::internal::{
maybe_bridge_standard_sync_json_to_stream, maybe_build_stream_response_rewriter,

View File

@@ -1249,49 +1249,6 @@ fn chatgpt_web_image_internal_url(base_url: &str) -> String {
format!("{base_url}/__aether/chatgpt-web-image")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "system", "content": "Use crisp vector-like shapes."},
{
"role": "user",
"content": [
{"type": "text", "text": "Draw a glass city"},
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}
]
}
],
"size": "1536x1024",
"output_format": "webp",
"web_model": "gpt-5-image-test"
});
let (provider_body, summary) =
build_chatgpt_web_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2")
.expect("chat image body should convert");
assert_eq!(provider_body["operation"], "edit");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["web_model"], "gpt-5-image-test");
assert_eq!(
provider_body["prompt"],
"Use crisp vector-like shapes.\nDraw a glass city"
);
assert_eq!(provider_body["size"], "1536x1024");
assert_eq!(provider_body["ratio"], "3:2");
assert_eq!(provider_body["output_format"], "webp");
assert_eq!(provider_body["images"][0], "https://example.com/ref.png");
assert_eq!(summary["operation"], "edit");
assert_eq!(summary["output_format"], "webp");
}
}
#[allow(clippy::too_many_arguments)]
async fn build_kiro_openai_chat_cross_format_payload_parts(
state: &AppState,
@@ -1515,3 +1472,46 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({
"model": "gpt-image-2",
"messages": [
{"role": "system", "content": "Use crisp vector-like shapes."},
{
"role": "user",
"content": [
{"type": "text", "text": "Draw a glass city"},
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}
]
}
],
"size": "1536x1024",
"output_format": "webp",
"web_model": "gpt-5-image-test"
});
let (provider_body, summary) =
build_chatgpt_web_image_provider_body_from_openai_chat_body(&body_json, "gpt-image-2")
.expect("chat image body should convert");
assert_eq!(provider_body["operation"], "edit");
assert_eq!(provider_body["model"], "gpt-image-2");
assert_eq!(provider_body["web_model"], "gpt-5-image-test");
assert_eq!(
provider_body["prompt"],
"Use crisp vector-like shapes.\nDraw a glass city"
);
assert_eq!(provider_body["size"], "1536x1024");
assert_eq!(provider_body["ratio"], "3:2");
assert_eq!(provider_body["output_format"], "webp");
assert_eq!(provider_body["images"][0], "https://example.com/ref.png");
assert_eq!(summary["operation"], "edit");
assert_eq!(summary["output_format"], "webp");
}
}

View File

@@ -199,7 +199,7 @@ fn local_core_sync_finalize_has_invalid_provider_success(
return Ok(false);
}
let provider_api_format = resolve_local_sync_provider_api_format(payload);
if aether_ai_formats::normalize_api_format_alias(&provider_api_format)
if crate::ai_serving::normalize_api_format_alias(&provider_api_format)
!= "gemini:generate_content"
{
return Ok(false);
@@ -210,10 +210,7 @@ fn local_core_sync_finalize_has_invalid_provider_success(
if has_nested_error(&body_json) {
return Ok(false);
}
Ok(
aether_ai_formats::formats::gemini::generate_content::response::from_raw(&body_json)
.is_none(),
)
Ok(!crate::ai_serving::gemini_generate_content_response_has_visible_output(&body_json))
}
pub(crate) fn build_best_effort_local_core_error_body(

View File

@@ -363,7 +363,7 @@ fn invalid_gemini_provider_success_message(
.and_then(|value| value.get("provider_api_format"))
.and_then(Value::as_str)
.unwrap_or(plan.provider_api_format.as_str());
if aether_ai_formats::normalize_api_format_alias(provider_api_format)
if crate::ai_serving::normalize_api_format_alias(provider_api_format)
!= "gemini:generate_content"
{
return None;
@@ -375,8 +375,7 @@ fn invalid_gemini_provider_success_message(
{
return None;
}
if aether_ai_formats::formats::gemini::generate_content::response::from_raw(body_json).is_some()
{
if crate::ai_serving::gemini_generate_content_response_has_visible_output(body_json) {
return None;
}
Some("Provider returned HTTP 200 but the Gemini response did not contain visible model output; refusing to finalize it as a successful response.")

View File

@@ -1494,7 +1494,7 @@ fn provider_query_standard_execution_response_body(
if result.status_code < 400
&& provider_query_normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
&& aether_ai_formats::formats::gemini::generate_content::response::from_raw(&body).is_none()
&& !crate::ai_serving::gemini_generate_content_response_has_visible_output(&body)
{
return None;
}

View File

@@ -53,7 +53,7 @@ fn provider_query_endpoint_route_payload(
candidate: &ProviderQueryTestCandidate,
execution: &ProviderQueryExecutionOutcome,
) -> Value {
let api_format = aether_ai_formats::normalize_api_format_alias(&candidate.endpoint.api_format);
let api_format = crate::ai_serving::normalize_api_format_alias(&candidate.endpoint.api_format);
let request_url = execution.request_url.to_ascii_lowercase();
let base_url = candidate.endpoint.base_url.to_ascii_lowercase();
let is_vertex = request_url.contains("aiplatform.googleapis.com")

View File

@@ -349,7 +349,13 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,
@@ -1117,7 +1123,13 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,
@@ -1595,7 +1607,13 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Vertex Gemini CLI"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,

View File

@@ -349,7 +349,13 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 1,
"candidatesTokenCount": 2,

View File

@@ -1224,7 +1224,7 @@ async fn gateway_refresh_quota_reconciles_unsupported_fixed_provider_endpoints_b
(
"provider-vertex-ai-reconcile",
"vertex_ai",
2usize,
3usize,
"gemini:generate_content",
"https://aiplatform.googleapis.com",
"Vertex AI 暂不支持自动刷新额度",

View File

@@ -1177,7 +1177,13 @@ async fn gateway_records_gemini_sync_usage_and_pricing_with_cache_read_tokens_im
},
"body": {
"json_body": {
"candidates": [],
"candidates": [{
"content": {
"role": "model",
"parts": [{"text": "Hello from Gemini"}]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": expected.input_tokens,
"candidatesTokenCount": expected.output_tokens,

View File

@@ -680,16 +680,15 @@ impl OpenAiImageStreamTerminalState {
fn observe_image_completed(&mut self, event: &Value) {
self.observed_finish = true;
if self.image_count == 0 {
if event
if self.image_count == 0
&& event
.get("b64_json")
.or_else(|| event.get("result"))
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty())
{
self.image_count = 1;
}
{
self.image_count = 1;
}
self.usage = event.get("usage").cloned().or_else(|| self.usage.clone());
}
@@ -798,7 +797,7 @@ fn openai_image_stream_standardized_usage(
) -> Option<StandardizedUsage> {
let mut standardized_usage = usage
.and_then(openai_image_usage_to_standardized_usage)
.unwrap_or_else(StandardizedUsage::new);
.unwrap_or_default();
if image_count > 0 {
standardized_usage.request_count = i64::try_from(image_count).unwrap_or(i64::MAX);
standardized_usage

View File

@@ -454,7 +454,7 @@ fn openai_image_standardized_usage(
) -> Option<StandardizedUsage> {
let mut standardized_usage = usage
.and_then(standardized_usage_from_openai_usage)
.unwrap_or_else(StandardizedUsage::new);
.unwrap_or_default();
if image_count > 0 {
standardized_usage.request_count = i64::try_from(image_count).unwrap_or(i64::MAX);
standardized_usage

View File

@@ -0,0 +1,32 @@
CREATE TABLE IF NOT EXISTS usage_counter_deltas (
`id` VARCHAR(36) NOT NULL,
`request_id` VARCHAR(128) NOT NULL,
`kind` VARCHAR(64) NOT NULL,
`target_id` TEXT NOT NULL,
`request_count_delta` BIGINT NOT NULL DEFAULT 0,
`total_requests_delta` BIGINT NOT NULL DEFAULT 0,
`success_count_delta` BIGINT NOT NULL DEFAULT 0,
`error_count_delta` BIGINT NOT NULL DEFAULT 0,
`dns_failures_delta` BIGINT NOT NULL DEFAULT 0,
`stream_errors_delta` BIGINT NOT NULL DEFAULT 0,
`total_tokens_delta` BIGINT NOT NULL DEFAULT 0,
`total_cost_usd_delta` DOUBLE NOT NULL DEFAULT 0,
`total_response_time_ms_delta` BIGINT NOT NULL DEFAULT 0,
`last_used_at_unix_secs` BIGINT,
`last_used_ip` TEXT,
`candidate_last_used_at_unix_secs` BIGINT,
`removed_last_used_at_unix_secs` BIGINT,
`usage_created_at_unix_secs` BIGINT,
`created_at` BIGINT NOT NULL,
`processed_at` BIGINT,
PRIMARY KEY (`id`),
KEY ix_usage_counter_deltas_unprocessed (`created_at`, `id`),
KEY ix_usage_counter_deltas_processed (`processed_at`, `created_at`, `id`),
KEY ix_usage_counter_deltas_request_kind (`request_id`, `kind`, `target_id`(191))
);
CREATE INDEX video_tasks_due_poll_idx
ON video_tasks (status, next_poll_at, updated_at);
CREATE INDEX idx_entitlement_usage_entitlement_date
ON entitlement_usage_ledgers (user_entitlement_id, usage_date);

View File

@@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS usage_counter_deltas (
id TEXT PRIMARY KEY NOT NULL,
request_id TEXT NOT NULL,
kind TEXT NOT NULL,
target_id TEXT NOT NULL,
request_count_delta INTEGER NOT NULL DEFAULT 0,
total_requests_delta INTEGER NOT NULL DEFAULT 0,
success_count_delta INTEGER NOT NULL DEFAULT 0,
error_count_delta INTEGER NOT NULL DEFAULT 0,
dns_failures_delta INTEGER NOT NULL DEFAULT 0,
stream_errors_delta INTEGER NOT NULL DEFAULT 0,
total_tokens_delta INTEGER NOT NULL DEFAULT 0,
total_cost_usd_delta REAL NOT NULL DEFAULT 0,
total_response_time_ms_delta INTEGER NOT NULL DEFAULT 0,
last_used_at_unix_secs INTEGER,
last_used_ip TEXT,
candidate_last_used_at_unix_secs INTEGER,
removed_last_used_at_unix_secs INTEGER,
usage_created_at_unix_secs INTEGER,
created_at INTEGER NOT NULL,
processed_at INTEGER
);
CREATE INDEX IF NOT EXISTS ix_usage_counter_deltas_unprocessed
ON usage_counter_deltas (created_at, id);
CREATE INDEX IF NOT EXISTS ix_usage_counter_deltas_processed
ON usage_counter_deltas (processed_at, created_at, id);
CREATE INDEX IF NOT EXISTS ix_usage_counter_deltas_request_kind
ON usage_counter_deltas (request_id, kind, target_id);
CREATE INDEX IF NOT EXISTS video_tasks_due_poll_idx
ON video_tasks (status, next_poll_at, updated_at);
CREATE INDEX IF NOT EXISTS idx_entitlement_usage_entitlement_date
ON entitlement_usage_ledgers (user_entitlement_id, usage_date);

View File

@@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS users (
`feature_settings` JSON,
`rate_limit` INT,
`rate_limit_mode` VARCHAR(32) NOT NULL DEFAULT 'system',
`privacy_policy_accepted_version` VARCHAR(64),
`privacy_policy_accepted_at` BIGINT,
`metadata` JSON,
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NOT NULL,
@@ -123,6 +125,7 @@ CREATE TABLE IF NOT EXISTS announcements (
`author_id` VARCHAR(64),
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_pinned` TINYINT(1) NOT NULL DEFAULT 0,
`requires_ack` TINYINT(1) NOT NULL DEFAULT 0,
`start_time` BIGINT,
`end_time` BIGINT,
`created_at` BIGINT NOT NULL,

View File

@@ -104,6 +104,68 @@ CREATE TABLE IF NOT EXISTS payment_orders (
KEY idx_payment_orders_product (`product_id`)
);
CREATE TABLE IF NOT EXISTS user_invite_codes (
`user_id` VARCHAR(64) NOT NULL,
`invite_code` VARCHAR(64) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NOT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY user_invite_codes_invite_code_key (`invite_code`),
CONSTRAINT user_invite_codes_user_id_fkey FOREIGN KEY (`user_id`) REFERENCES users (`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_referrals (
`id` VARCHAR(64) NOT NULL,
`inviter_user_id` VARCHAR(64) NOT NULL,
`invitee_user_id` VARCHAR(64) NOT NULL,
`invite_code_snapshot` VARCHAR(64) NOT NULL,
`source_json` JSON,
`first_paid_order_id` VARCHAR(64),
`first_paid_at` BIGINT,
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY user_referrals_invitee_user_id_key (`invitee_user_id`),
KEY idx_user_referrals_inviter (`inviter_user_id`, `created_at`),
KEY idx_user_referrals_created (`created_at`),
KEY idx_user_referrals_invite_code (`invite_code_snapshot`),
CONSTRAINT user_referrals_inviter_user_id_fkey FOREIGN KEY (`inviter_user_id`) REFERENCES users (`id`) ON DELETE CASCADE,
CONSTRAINT user_referrals_invitee_user_id_fkey FOREIGN KEY (`invitee_user_id`) REFERENCES users (`id`) ON DELETE CASCADE,
CONSTRAINT user_referrals_first_paid_order_fkey FOREIGN KEY (`first_paid_order_id`) REFERENCES payment_orders (`id`) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS referral_rewards (
`id` VARCHAR(64) NOT NULL,
`referral_id` VARCHAR(64) NOT NULL,
`inviter_user_id` VARCHAR(64) NOT NULL,
`invitee_user_id` VARCHAR(64) NOT NULL,
`reward_type` VARCHAR(32) NOT NULL,
`trigger_point` VARCHAR(64) NOT NULL,
`source_order_id` VARCHAR(64),
`idempotency_key` VARCHAR(128) NOT NULL,
`amount_usd` DOUBLE NOT NULL,
`status` VARCHAR(32) NOT NULL DEFAULT 'pending',
`wallet_transaction_id` VARCHAR(64),
`reversed_amount_usd` DOUBLE NOT NULL DEFAULT 0,
`pending_reversal_amount_usd` DOUBLE NOT NULL DEFAULT 0,
`failure_reason` LONGTEXT,
`admin_operator_id` VARCHAR(64),
`admin_note` LONGTEXT,
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY referral_rewards_idempotency_key_key (`idempotency_key`),
KEY idx_referral_rewards_inviter_status (`inviter_user_id`, `status`, `created_at`),
KEY idx_referral_rewards_inviter_created (`inviter_user_id`, `created_at`),
KEY idx_referral_rewards_created (`created_at`),
KEY idx_referral_rewards_source_order (`source_order_id`),
CONSTRAINT referral_rewards_referral_id_fkey FOREIGN KEY (`referral_id`) REFERENCES user_referrals (`id`) ON DELETE CASCADE,
CONSTRAINT referral_rewards_inviter_user_id_fkey FOREIGN KEY (`inviter_user_id`) REFERENCES users (`id`) ON DELETE CASCADE,
CONSTRAINT referral_rewards_invitee_user_id_fkey FOREIGN KEY (`invitee_user_id`) REFERENCES users (`id`) ON DELETE CASCADE,
CONSTRAINT referral_rewards_source_order_fkey FOREIGN KEY (`source_order_id`) REFERENCES payment_orders (`id`) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS payment_gateway_configs (
`provider` VARCHAR(64) NOT NULL,
`enabled` TINYINT(1) NOT NULL DEFAULT 0,

View File

@@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS public.users (
feature_settings jsonb,
rate_limit integer,
rate_limit_mode character varying(32) DEFAULT 'system' NOT NULL,
privacy_policy_accepted_version character varying(64),
privacy_policy_accepted_at timestamp with time zone,
metadata jsonb,
created_at bigint NOT NULL,
updated_at bigint NOT NULL,
@@ -128,6 +130,7 @@ CREATE TABLE IF NOT EXISTS public.announcements (
author_id character varying(64),
is_active boolean DEFAULT true NOT NULL,
is_pinned boolean DEFAULT false NOT NULL,
requires_ack boolean DEFAULT false NOT NULL,
start_time bigint,
end_time bigint,
created_at bigint NOT NULL,

View File

@@ -108,6 +108,71 @@ CREATE INDEX IF NOT EXISTS idx_payment_orders_gateway_order_id ON public.payment
CREATE INDEX IF NOT EXISTS idx_payment_orders_kind_status ON public.payment_orders USING btree (order_kind, status);
CREATE INDEX IF NOT EXISTS idx_payment_orders_product ON public.payment_orders USING btree (product_id);
CREATE TABLE IF NOT EXISTS public.user_invite_codes (
user_id character varying(64) NOT NULL,
invite_code character varying(64) NOT NULL,
active boolean DEFAULT true NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
ALTER TABLE ONLY public.user_invite_codes ADD CONSTRAINT user_invite_codes_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY public.user_invite_codes ADD CONSTRAINT user_invite_codes_invite_code_key UNIQUE (invite_code);
ALTER TABLE ONLY public.user_invite_codes ADD CONSTRAINT user_invite_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
CREATE TABLE IF NOT EXISTS public.user_referrals (
id character varying(64) NOT NULL,
inviter_user_id character varying(64) NOT NULL,
invitee_user_id character varying(64) NOT NULL,
invite_code_snapshot character varying(64) NOT NULL,
source_json jsonb,
first_paid_order_id character varying(64),
first_paid_at timestamp with time zone,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
ALTER TABLE ONLY public.user_referrals ADD CONSTRAINT user_referrals_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.user_referrals ADD CONSTRAINT user_referrals_invitee_user_id_key UNIQUE (invitee_user_id);
CREATE INDEX IF NOT EXISTS idx_user_referrals_inviter ON public.user_referrals USING btree (inviter_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_user_referrals_created ON public.user_referrals USING btree (created_at);
CREATE INDEX IF NOT EXISTS idx_user_referrals_invite_code ON public.user_referrals USING btree (invite_code_snapshot);
ALTER TABLE ONLY public.user_referrals ADD CONSTRAINT user_referrals_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.user_referrals ADD CONSTRAINT user_referrals_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.user_referrals ADD CONSTRAINT user_referrals_first_paid_order_fkey FOREIGN KEY (first_paid_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
CREATE TABLE IF NOT EXISTS public.referral_rewards (
id character varying(64) NOT NULL,
referral_id character varying(64) NOT NULL,
inviter_user_id character varying(64) NOT NULL,
invitee_user_id character varying(64) NOT NULL,
reward_type character varying(32) NOT NULL,
trigger_point character varying(64) NOT NULL,
source_order_id character varying(64),
idempotency_key character varying(128) NOT NULL,
amount_usd double precision NOT NULL,
status character varying(32) DEFAULT 'pending' NOT NULL,
wallet_transaction_id character varying(64),
reversed_amount_usd double precision DEFAULT 0 NOT NULL,
pending_reversal_amount_usd double precision DEFAULT 0 NOT NULL,
failure_reason text,
admin_operator_id character varying(64),
admin_note text,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_idempotency_key_key UNIQUE (idempotency_key);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_status ON public.referral_rewards USING btree (inviter_user_id, status, created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_created ON public.referral_rewards USING btree (inviter_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_created ON public.referral_rewards USING btree (created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_source_order ON public.referral_rewards USING btree (source_order_id);
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_referral_id_fkey FOREIGN KEY (referral_id) REFERENCES public.user_referrals(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.referral_rewards ADD CONSTRAINT referral_rewards_source_order_fkey FOREIGN KEY (source_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
CREATE TABLE IF NOT EXISTS public.payment_gateway_configs (
provider character varying(64) NOT NULL,
enabled boolean DEFAULT false NOT NULL,

View File

@@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS users (
feature_settings TEXT,
rate_limit INTEGER,
rate_limit_mode TEXT NOT NULL DEFAULT 'system',
privacy_policy_accepted_version TEXT,
privacy_policy_accepted_at INTEGER,
metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
@@ -119,6 +121,7 @@ CREATE TABLE IF NOT EXISTS announcements (
author_id TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
is_pinned INTEGER NOT NULL DEFAULT 0,
requires_ack INTEGER NOT NULL DEFAULT 0,
start_time INTEGER,
end_time INTEGER,
created_at INTEGER NOT NULL,

View File

@@ -100,6 +100,65 @@ CREATE INDEX IF NOT EXISTS idx_payment_orders_gateway_order_id ON payment_orders
CREATE INDEX IF NOT EXISTS idx_payment_orders_kind_status ON payment_orders (order_kind, status);
CREATE INDEX IF NOT EXISTS idx_payment_orders_product ON payment_orders (product_id);
CREATE TABLE IF NOT EXISTS user_invite_codes (
user_id TEXT PRIMARY KEY NOT NULL,
invite_code TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE (invite_code),
CONSTRAINT user_invite_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_referrals (
id TEXT PRIMARY KEY NOT NULL,
inviter_user_id TEXT NOT NULL,
invitee_user_id TEXT NOT NULL,
invite_code_snapshot TEXT NOT NULL,
source_json TEXT,
first_paid_order_id TEXT,
first_paid_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE (invitee_user_id),
CONSTRAINT user_referrals_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT user_referrals_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT user_referrals_first_paid_order_fkey FOREIGN KEY (first_paid_order_id) REFERENCES payment_orders (id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_user_referrals_inviter ON user_referrals (inviter_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_user_referrals_created ON user_referrals (created_at);
CREATE INDEX IF NOT EXISTS idx_user_referrals_invite_code ON user_referrals (invite_code_snapshot);
CREATE TABLE IF NOT EXISTS referral_rewards (
id TEXT PRIMARY KEY NOT NULL,
referral_id TEXT NOT NULL,
inviter_user_id TEXT NOT NULL,
invitee_user_id TEXT NOT NULL,
reward_type TEXT NOT NULL,
trigger_point TEXT NOT NULL,
source_order_id TEXT,
idempotency_key TEXT NOT NULL,
amount_usd REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
wallet_transaction_id TEXT,
reversed_amount_usd REAL NOT NULL DEFAULT 0,
pending_reversal_amount_usd REAL NOT NULL DEFAULT 0,
failure_reason TEXT,
admin_operator_id TEXT,
admin_note TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE (idempotency_key),
CONSTRAINT referral_rewards_referral_id_fkey FOREIGN KEY (referral_id) REFERENCES user_referrals (id) ON DELETE CASCADE,
CONSTRAINT referral_rewards_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT referral_rewards_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT referral_rewards_source_order_fkey FOREIGN KEY (source_order_id) REFERENCES payment_orders (id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_status ON referral_rewards (inviter_user_id, status, created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_created ON referral_rewards (inviter_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_created ON referral_rewards (created_at);
CREATE INDEX IF NOT EXISTS idx_referral_rewards_source_order ON referral_rewards (source_order_id);
CREATE TABLE IF NOT EXISTS payment_gateway_configs (
provider TEXT PRIMARY KEY NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,

View File

@@ -113,6 +113,17 @@ type = "text"
length = 32
default = "system"
[[table.users.columns]]
name = "privacy_policy_accepted_version"
type = "text"
length = 64
nullable = true
[[table.users.columns]]
name = "privacy_policy_accepted_at"
type = "timestamp"
nullable = true
[[table.users.columns]]
name = "metadata"
type = "json"
@@ -539,6 +550,11 @@ name = "is_pinned"
type = "bool"
default = false
[[table.announcements.columns]]
name = "requires_ack"
type = "bool"
default = false
[[table.announcements.columns]]
name = "start_time"
type = "unix_seconds"

View File

@@ -438,6 +438,275 @@ columns = ["order_kind", "status"]
name = "idx_payment_orders_product"
columns = ["product_id"]
[table.user_invite_codes]
domain = "wallet_billing"
order = 42
primary_key = ["user_id"]
[[table.user_invite_codes.columns]]
name = "user_id"
type = "text_id"
length = 64
[[table.user_invite_codes.columns]]
name = "invite_code"
type = "text"
length = 64
[[table.user_invite_codes.columns]]
name = "active"
type = "bool"
default = true
[[table.user_invite_codes.columns]]
name = "created_at"
type = "timestamp"
[[table.user_invite_codes.columns]]
name = "updated_at"
type = "timestamp"
[[table.user_invite_codes.uniques]]
name = "user_invite_codes_invite_code_key"
columns = ["invite_code"]
[[table.user_invite_codes.foreign_keys]]
name = "user_invite_codes_user_id_fkey"
columns = ["user_id"]
references_table = "users"
references_columns = ["id"]
on_delete = "cascade"
[table.user_referrals]
domain = "wallet_billing"
order = 43
primary_key = ["id"]
[[table.user_referrals.columns]]
name = "id"
type = "text_id"
length = 64
[[table.user_referrals.columns]]
name = "inviter_user_id"
type = "text_id"
length = 64
[[table.user_referrals.columns]]
name = "invitee_user_id"
type = "text_id"
length = 64
[[table.user_referrals.columns]]
name = "invite_code_snapshot"
type = "text"
length = 64
[[table.user_referrals.columns]]
name = "source_json"
type = "json"
nullable = true
[[table.user_referrals.columns]]
name = "first_paid_order_id"
type = "text_id"
length = 64
nullable = true
[[table.user_referrals.columns]]
name = "first_paid_at"
type = "timestamp"
nullable = true
[[table.user_referrals.columns]]
name = "created_at"
type = "timestamp"
[[table.user_referrals.columns]]
name = "updated_at"
type = "timestamp"
[[table.user_referrals.uniques]]
name = "user_referrals_invitee_user_id_key"
columns = ["invitee_user_id"]
[[table.user_referrals.indexes]]
name = "idx_user_referrals_inviter"
columns = ["inviter_user_id", "created_at"]
[[table.user_referrals.indexes]]
name = "idx_user_referrals_created"
columns = ["created_at"]
[[table.user_referrals.indexes]]
name = "idx_user_referrals_invite_code"
columns = ["invite_code_snapshot"]
[[table.user_referrals.foreign_keys]]
name = "user_referrals_inviter_user_id_fkey"
columns = ["inviter_user_id"]
references_table = "users"
references_columns = ["id"]
on_delete = "cascade"
[[table.user_referrals.foreign_keys]]
name = "user_referrals_invitee_user_id_fkey"
columns = ["invitee_user_id"]
references_table = "users"
references_columns = ["id"]
on_delete = "cascade"
[[table.user_referrals.foreign_keys]]
name = "user_referrals_first_paid_order_fkey"
columns = ["first_paid_order_id"]
references_table = "payment_orders"
references_columns = ["id"]
on_delete = "set_null"
[table.referral_rewards]
domain = "wallet_billing"
order = 44
primary_key = ["id"]
[[table.referral_rewards.columns]]
name = "id"
type = "text_id"
length = 64
[[table.referral_rewards.columns]]
name = "referral_id"
type = "text_id"
length = 64
[[table.referral_rewards.columns]]
name = "inviter_user_id"
type = "text_id"
length = 64
[[table.referral_rewards.columns]]
name = "invitee_user_id"
type = "text_id"
length = 64
[[table.referral_rewards.columns]]
name = "reward_type"
type = "text"
length = 32
[[table.referral_rewards.columns]]
name = "trigger_point"
type = "text"
length = 64
[[table.referral_rewards.columns]]
name = "source_order_id"
type = "text_id"
length = 64
nullable = true
[[table.referral_rewards.columns]]
name = "idempotency_key"
type = "text"
length = 128
[[table.referral_rewards.columns]]
name = "amount_usd"
type = "float64"
[[table.referral_rewards.columns]]
name = "status"
type = "text"
length = 32
default = "pending"
[[table.referral_rewards.columns]]
name = "wallet_transaction_id"
type = "text_id"
length = 64
nullable = true
[[table.referral_rewards.columns]]
name = "reversed_amount_usd"
type = "float64"
default = 0
[[table.referral_rewards.columns]]
name = "pending_reversal_amount_usd"
type = "float64"
default = 0
[[table.referral_rewards.columns]]
name = "failure_reason"
type = "long_text"
nullable = true
[[table.referral_rewards.columns]]
name = "admin_operator_id"
type = "text_id"
length = 64
nullable = true
[[table.referral_rewards.columns]]
name = "admin_note"
type = "long_text"
nullable = true
[[table.referral_rewards.columns]]
name = "created_at"
type = "timestamp"
[[table.referral_rewards.columns]]
name = "updated_at"
type = "timestamp"
[[table.referral_rewards.uniques]]
name = "referral_rewards_idempotency_key_key"
columns = ["idempotency_key"]
[[table.referral_rewards.indexes]]
name = "idx_referral_rewards_inviter_status"
columns = ["inviter_user_id", "status", "created_at"]
[[table.referral_rewards.indexes]]
name = "idx_referral_rewards_inviter_created"
columns = ["inviter_user_id", "created_at"]
[[table.referral_rewards.indexes]]
name = "idx_referral_rewards_created"
columns = ["created_at"]
[[table.referral_rewards.indexes]]
name = "idx_referral_rewards_source_order"
columns = ["source_order_id"]
[[table.referral_rewards.foreign_keys]]
name = "referral_rewards_referral_id_fkey"
columns = ["referral_id"]
references_table = "user_referrals"
references_columns = ["id"]
on_delete = "cascade"
[[table.referral_rewards.foreign_keys]]
name = "referral_rewards_inviter_user_id_fkey"
columns = ["inviter_user_id"]
references_table = "users"
references_columns = ["id"]
on_delete = "cascade"
[[table.referral_rewards.foreign_keys]]
name = "referral_rewards_invitee_user_id_fkey"
columns = ["invitee_user_id"]
references_table = "users"
references_columns = ["id"]
on_delete = "cascade"
[[table.referral_rewards.foreign_keys]]
name = "referral_rewards_source_order_fkey"
columns = ["source_order_id"]
references_table = "payment_orders"
references_columns = ["id"]
on_delete = "set_null"
[table.payment_gateway_configs]
domain = "wallet_billing"
order = 45

View File

@@ -43,6 +43,15 @@ fn settlement_billing_status_for_usage_status(status: &str) -> &'static str {
}
}
#[allow(unused_imports)]
pub(crate) use aether_data_contracts::repository::settlement::{
SettlementRepository, SettlementWriteRepository, StoredUsageSettlement, UsageSettlementInput,
};
pub use memory::InMemorySettlementRepository;
pub use mysql::MysqlSettlementRepository;
pub use postgres::SqlxSettlementRepository;
pub use sqlite::SqliteSettlementRepository;
#[cfg(test)]
mod tests {
use super::settlement_billing_status_for_usage_status;
@@ -60,12 +69,3 @@ mod tests {
assert_eq!(settlement_billing_status_for_usage_status("failed"), "void");
}
}
#[allow(unused_imports)]
pub(crate) use aether_data_contracts::repository::settlement::{
SettlementRepository, SettlementWriteRepository, StoredUsageSettlement, UsageSettlementInput,
};
pub use memory::InMemorySettlementRepository;
pub use mysql::MysqlSettlementRepository;
pub use postgres::SqlxSettlementRepository;
pub use sqlite::SqliteSettlementRepository;

View File

@@ -4753,7 +4753,7 @@ mod tests {
assert_eq!(event.data.input_tokens, Some(3));
assert_eq!(event.data.output_tokens, Some(5));
assert_eq!(event.data.total_tokens, Some(11));
assert_eq!(event.data.total_tokens, Some(8));
assert_eq!(event.data.cache_creation_input_tokens, Some(1));
assert_eq!(event.data.cache_read_input_tokens, Some(2));
assert_eq!(
@@ -5291,7 +5291,7 @@ mod tests {
assert_eq!(
extract_token_counts_from_value(&Value::String(sse_body.to_string())),
Some((3, 5, 11)),
Some((3, 5, 8)),
);
}
}