mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(image): 接入 ChatGPT Web 生图反代
This commit is contained in:
@@ -184,6 +184,9 @@ fn key_auth_channel_matches(row: &StoredMinimalCandidateSelectionRow, api_format
|
||||
"openai:responses" | "openai:responses:compact" | "openai:image"
|
||||
)
|
||||
}
|
||||
"chatgpt_web" => {
|
||||
matches!(auth_type.as_str(), "oauth" | "bearer") && api_format == "openai:image"
|
||||
}
|
||||
"claude_code" => auth_type == "oauth" && api_format == "claude:messages",
|
||||
"kiro" => {
|
||||
matches!(auth_type.as_str(), "oauth" | "bearer") && api_format == "claude:messages"
|
||||
@@ -324,6 +327,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allows_chatgpt_web_oauth_and_bearer_for_openai_image_only() {
|
||||
let mut oauth = sample_row("chatgpt-web-oauth", "openai:image", "gpt-image-2", 10);
|
||||
oauth.provider_type = "chatgpt_web".to_string();
|
||||
oauth.key_auth_type = "oauth".to_string();
|
||||
let mut bearer = sample_row("chatgpt-web-bearer", "openai:image", "gpt-image-2", 20);
|
||||
bearer.provider_type = "chatgpt_web".to_string();
|
||||
bearer.key_auth_type = "bearer".to_string();
|
||||
let mut api_key = sample_row("chatgpt-web-api-key", "openai:image", "gpt-image-2", 30);
|
||||
api_key.provider_type = "chatgpt_web".to_string();
|
||||
api_key.key_auth_type = "api_key".to_string();
|
||||
let mut responses = sample_row("chatgpt-web-responses", "openai:responses", "gpt-5", 40);
|
||||
responses.provider_type = "chatgpt_web".to_string();
|
||||
responses.key_auth_type = "oauth".to_string();
|
||||
|
||||
let repository = InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
oauth, bearer, api_key, responses,
|
||||
]);
|
||||
|
||||
let rows = repository
|
||||
.list_for_exact_api_format_and_requested_model("openai:image", "gpt-image-2")
|
||||
.await
|
||||
.expect("list should succeed");
|
||||
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.map(|row| row.provider_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["chatgpt-web-oauth", "chatgpt-web-bearer"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filters_by_exact_api_format_only() {
|
||||
let repository = InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
|
||||
@@ -289,6 +289,9 @@ fn key_auth_channel_matches(row: &CandidateSelectionRow, api_format: &str) -> bo
|
||||
"openai:responses" | "openai:responses:compact" | "openai:image"
|
||||
)
|
||||
}
|
||||
"chatgpt_web" => {
|
||||
matches!(auth_type.as_str(), "oauth" | "bearer") && api_format == "openai:image"
|
||||
}
|
||||
"claude_code" => auth_type == "oauth" && api_format == "claude:messages",
|
||||
"kiro" => {
|
||||
api_format == "claude:messages"
|
||||
|
||||
@@ -80,6 +80,11 @@ WHERE p.is_active = TRUE
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
AND LOWER($3) IN ('openai:responses', 'openai:responses:compact', 'openai:image')
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'chatgpt_web'
|
||||
AND LOWER(BTRIM(pak.auth_type)) IN ('oauth', 'bearer')
|
||||
AND LOWER($3) = 'openai:image'
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'claude_code'
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
@@ -117,6 +122,7 @@ WHERE p.is_active = TRUE
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) NOT IN (
|
||||
'chatgpt_web',
|
||||
'claude_code',
|
||||
'codex',
|
||||
'gemini_cli',
|
||||
@@ -257,6 +263,11 @@ WHERE p.is_active = TRUE
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
AND LOWER($4) IN ('openai:responses', 'openai:responses:compact', 'openai:image')
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'chatgpt_web'
|
||||
AND LOWER(BTRIM(pak.auth_type)) IN ('oauth', 'bearer')
|
||||
AND LOWER($4) = 'openai:image'
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'claude_code'
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
@@ -294,6 +305,7 @@ WHERE p.is_active = TRUE
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) NOT IN (
|
||||
'chatgpt_web',
|
||||
'claude_code',
|
||||
'codex',
|
||||
'gemini_cli',
|
||||
@@ -433,6 +445,11 @@ WHERE p.is_active = TRUE
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
AND LOWER($6) IN ('openai:responses', 'openai:responses:compact', 'openai:image')
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'chatgpt_web'
|
||||
AND LOWER(BTRIM(pak.auth_type)) IN ('oauth', 'bearer')
|
||||
AND LOWER($6) = 'openai:image'
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) = 'claude_code'
|
||||
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
|
||||
@@ -470,6 +487,7 @@ WHERE p.is_active = TRUE
|
||||
)
|
||||
OR (
|
||||
LOWER(BTRIM(p.provider_type)) NOT IN (
|
||||
'chatgpt_web',
|
||||
'claude_code',
|
||||
'codex',
|
||||
'gemini_cli',
|
||||
@@ -1007,6 +1025,8 @@ mod tests {
|
||||
use super::{
|
||||
parse_provider_model_mappings, parse_string_list, requested_model_selection_page_sql,
|
||||
requested_model_selection_sql, SqlxMinimalCandidateSelectionReadRepository,
|
||||
LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL, LIST_FOR_EXACT_API_FORMAT_SQL,
|
||||
LIST_POOL_KEYS_FOR_GROUP_SQL,
|
||||
};
|
||||
use crate::driver::postgres::{PostgresPoolConfig, PostgresPoolFactory};
|
||||
use crate::repository::candidate_selection::StoredProviderModelMapping;
|
||||
@@ -1042,6 +1062,21 @@ mod tests {
|
||||
assert!(!sql.contains("AND gm.name = $2\n AND"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_selection_sql_allows_chatgpt_web_image_auth() {
|
||||
let requested_model_sql = requested_model_selection_sql();
|
||||
for sql in [
|
||||
LIST_FOR_EXACT_API_FORMAT_SQL,
|
||||
LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL,
|
||||
LIST_POOL_KEYS_FOR_GROUP_SQL,
|
||||
requested_model_sql.as_str(),
|
||||
] {
|
||||
assert!(sql.contains("LOWER(BTRIM(p.provider_type)) = 'chatgpt_web'"));
|
||||
assert!(sql.contains("LOWER(BTRIM(pak.auth_type)) IN ('oauth', 'bearer')"));
|
||||
assert!(sql.contains("'chatgpt_web',"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requested_model_selection_page_sql_adds_limit_and_offset() {
|
||||
let sql = requested_model_selection_page_sql();
|
||||
|
||||
@@ -289,6 +289,9 @@ fn key_auth_channel_matches(row: &CandidateSelectionRow, api_format: &str) -> bo
|
||||
"openai:responses" | "openai:responses:compact" | "openai:image"
|
||||
)
|
||||
}
|
||||
"chatgpt_web" => {
|
||||
matches!(auth_type.as_str(), "oauth" | "bearer") && api_format == "openai:image"
|
||||
}
|
||||
"claude_code" => auth_type == "oauth" && api_format == "claude:messages",
|
||||
"kiro" => {
|
||||
api_format == "claude:messages"
|
||||
@@ -666,6 +669,25 @@ mod tests {
|
||||
.expect("pool keys should load");
|
||||
assert_eq!(pool_keys.len(), 1);
|
||||
assert_eq!(pool_keys[0].key_id, "key-2");
|
||||
|
||||
let image_rows = repository
|
||||
.list_for_exact_api_format_and_requested_model_page(
|
||||
&StoredRequestedModelCandidateRowsQuery {
|
||||
api_format: "openai:image".to_string(),
|
||||
requested_model_name: "gpt-image-2".to_string(),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("chatgpt web image rows should load");
|
||||
assert_eq!(
|
||||
image_rows
|
||||
.iter()
|
||||
.map(|row| row.key_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["key-chatgpt-web-oauth", "key-chatgpt-web-bearer"]
|
||||
);
|
||||
}
|
||||
|
||||
async fn seed_candidate_selection(pool: &sqlx::SqlitePool) {
|
||||
@@ -688,10 +710,33 @@ VALUES
|
||||
('key-1', 'provider-1', 'Key One', 'api_key', '["openai:chat"]', 10, 1, 1, 1),
|
||||
('key-2', 'provider-1', 'Key Two', 'api_key', '["openai:chat"]', 20, 1, 1, 1);
|
||||
|
||||
INSERT INTO providers (
|
||||
id, name, provider_type, provider_priority, is_active, created_at, updated_at
|
||||
)
|
||||
VALUES ('provider-chatgpt-web', 'ChatGPT Web', 'chatgpt_web', 20, 1, 1, 1);
|
||||
|
||||
INSERT INTO provider_endpoints (
|
||||
id, provider_id, name, base_url, api_format, is_active, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'endpoint-chatgpt-web', 'provider-chatgpt-web', 'ChatGPT Web Image',
|
||||
'https://chatgpt.com', 'openai:image', 1, 1, 1
|
||||
);
|
||||
|
||||
INSERT INTO provider_api_keys (
|
||||
id, provider_id, name, auth_type, api_formats, internal_priority, is_active, created_at, updated_at
|
||||
)
|
||||
VALUES
|
||||
('key-chatgpt-web-oauth', 'provider-chatgpt-web', 'OAuth', 'oauth', '["openai:image"]', 10, 1, 1, 1),
|
||||
('key-chatgpt-web-bearer', 'provider-chatgpt-web', 'Bearer', 'bearer', '["openai:image"]', 20, 1, 1, 1),
|
||||
('key-chatgpt-web-api-key', 'provider-chatgpt-web', 'API Key', 'api_key', '["openai:image"]', 30, 1, 1, 1);
|
||||
|
||||
INSERT INTO global_models (
|
||||
id, name, config, is_active, created_at, updated_at
|
||||
)
|
||||
VALUES ('global-1', 'gpt-5', '{"model_mappings":["alias-global"],"streaming":true}', 1, 1, 1);
|
||||
VALUES
|
||||
('global-1', 'gpt-5', '{"model_mappings":["alias-global"],"streaming":true}', 1, 1, 1),
|
||||
('global-image-1', 'gpt-image-2', NULL, 1, 1, 1);
|
||||
|
||||
INSERT INTO models (
|
||||
id, provider_id, global_model_id, provider_model_name, provider_model_mappings,
|
||||
@@ -701,6 +746,10 @@ VALUES (
|
||||
'model-1', 'provider-1', 'global-1', 'provider-model',
|
||||
'[{"name":"alias-provider","api_formats":["openai:chat"],"priority":1}]',
|
||||
1, 1, 1, 1, 1
|
||||
),
|
||||
(
|
||||
'model-chatgpt-web-image', 'provider-chatgpt-web', 'global-image-1', 'gpt-image-2',
|
||||
NULL, 1, 1, 1, 1, 1
|
||||
);
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -304,8 +304,8 @@ SET total_requests = 0,
|
||||
SELECT
|
||||
api_key_id,
|
||||
COUNT(*) AS total_requests,
|
||||
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
||||
COALESCE(SUM(total_cost_usd), 0) AS total_cost_usd,
|
||||
CAST(COALESCE(SUM(total_tokens), 0) AS SIGNED) AS total_tokens,
|
||||
CAST(COALESCE(SUM(total_cost_usd), 0) AS DOUBLE) AS total_cost_usd,
|
||||
MAX(updated_at_unix_secs) AS last_used_at
|
||||
FROM `usage`
|
||||
WHERE api_key_id IS NOT NULL AND api_key_id <> ''
|
||||
|
||||
@@ -3483,8 +3483,13 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.expect("admin wallets should list");
|
||||
assert_eq!(page.total, 1);
|
||||
assert_eq!(page.items[0].total_adjusted, 3.0);
|
||||
let wallet_item = page
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.id == "wallet-1")
|
||||
.expect("seeded wallet should be listed");
|
||||
assert!(page.total >= 1);
|
||||
assert_eq!(wallet_item.total_adjusted, 3.0);
|
||||
|
||||
let orders = repository
|
||||
.list_admin_payment_orders(&AdminPaymentOrderListQuery {
|
||||
|
||||
Reference in New Issue
Block a user