Merge branch 'fawney19:main' into main

This commit is contained in:
ZheFox
2026-05-29 12:27:38 +08:00
committed by GitHub
45 changed files with 1192 additions and 206 deletions
@@ -0,0 +1,132 @@
UPDATE provider_endpoints e
LEFT JOIN providers p ON p.id = e.provider_id
SET e.base_url = CONCAT(
TRIM(TRAILING '/' FROM SUBSTRING_INDEX(e.base_url, '?', 1)),
CASE
WHEN LOWER(TRIM(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN '/v1beta'
ELSE '/v1'
END,
IF(LOCATE('?', e.base_url) > 0, SUBSTRING(e.base_url, LOCATE('?', e.base_url)), '')
)
WHERE LOWER(TRIM(e.api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
AND COALESCE(LOWER(TRIM(p.provider_type)), '') NOT IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
AND LOWER(TRIM(TRAILING '/' FROM SUBSTRING_INDEX(e.base_url, '?', 1))) NOT REGEXP '/v[0-9]+(beta[0-9]*)?(/|$)'
AND (
(
LOWER(TRIM(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) LIKE '/v1beta/%'
)
OR (
LOWER(TRIM(e.api_format)) NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) LIKE '/v1/%'
)
OR COALESCE(TRIM(e.custom_path), '') = ''
);
UPDATE provider_endpoints e
LEFT JOIN providers p ON p.id = e.provider_id
SET e.custom_path = CASE
WHEN LOWER(TRIM(e.api_format)) = 'openai:chat'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/chat/completions'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'openai:responses'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/responses'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'openai:responses:compact'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/responses/compact'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'claude:messages'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/messages'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) IN ('openai:embedding', 'jina:embedding')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/embeddings'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) IN ('openai:rerank', 'jina:rerank')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/rerank'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'openai:image'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/images/generations'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'openai:video'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1/videos'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'gemini:generate_content'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1beta/models/{model}:{action}'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'gemini:embedding'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) IN ('/v1beta/models/{model}:embedcontent', '/v1beta/models/{model}:{action}')
THEN NULL
WHEN LOWER(TRIM(e.api_format)) = 'gemini:video'
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) = '/v1beta/models/{model}:predictlongrunning'
THEN NULL
WHEN LOWER(TRIM(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN CONCAT('/', SUBSTRING(TRIM(e.custom_path), 9))
ELSE CONCAT('/', SUBSTRING(TRIM(e.custom_path), 5))
END
WHERE LOWER(TRIM(e.api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
AND COALESCE(LOWER(TRIM(p.provider_type)), '') NOT IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
AND (
(
LOWER(TRIM(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) LIKE '/v1beta/%'
)
OR (
LOWER(TRIM(e.api_format)) NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND LOWER(TRIM(COALESCE(e.custom_path, ''))) LIKE '/v1/%'
)
)
AND LOWER(TRIM(TRAILING '/' FROM SUBSTRING_INDEX(e.base_url, '?', 1))) REGEXP '/v[0-9]+(beta[0-9]*)?(/|$)';
UPDATE provider_endpoints
SET custom_path = NULL
WHERE custom_path IS NOT NULL
AND TRIM(custom_path) = '';
@@ -0,0 +1,155 @@
WITH endpoint_url_parts AS (
SELECT
e.id,
e.base_url,
rtrim(split_part(e.base_url, '?', 1), '/') AS base_without_query,
CASE
WHEN position('?' IN e.base_url) > 0 THEN substring(e.base_url FROM position('?' IN e.base_url))
ELSE ''
END AS query_suffix,
lower(trim(e.api_format)) AS normalized_api_format,
lower(trim(coalesce(e.custom_path, ''))) AS normalized_custom_path,
lower(rtrim(split_part(e.base_url, '?', 1), '/')) AS normalized_base,
lower(trim(coalesce(p.provider_type, ''))) AS provider_type
FROM public.provider_endpoints e
LEFT JOIN public.providers p ON p.id = e.provider_id
WHERE lower(trim(e.api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
),
endpoint_api_root_updates AS (
SELECT
id,
base_without_query
|| CASE
WHEN normalized_api_format IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN '/v1beta'
ELSE '/v1'
END
|| query_suffix AS next_base_url
FROM endpoint_url_parts
WHERE provider_type NOT IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
AND normalized_base !~ '/v[0-9]+(beta[0-9]*)?(/|$)'
AND (
(
normalized_api_format IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND normalized_custom_path LIKE '/v1beta/%'
)
OR (
normalized_api_format NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND normalized_custom_path LIKE '/v1/%'
)
OR normalized_custom_path = ''
)
)
UPDATE public.provider_endpoints e
SET base_url = u.next_base_url
FROM endpoint_api_root_updates u
WHERE e.id = u.id
AND e.base_url IS DISTINCT FROM u.next_base_url;
UPDATE public.provider_endpoints e
SET custom_path = CASE
WHEN lower(trim(e.api_format)) = 'openai:chat'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/chat/completions'
THEN NULL
WHEN lower(trim(e.api_format)) = 'openai:responses'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/responses'
THEN NULL
WHEN lower(trim(e.api_format)) = 'openai:responses:compact'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/responses/compact'
THEN NULL
WHEN lower(trim(e.api_format)) = 'claude:messages'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/messages'
THEN NULL
WHEN lower(trim(e.api_format)) IN ('openai:embedding', 'jina:embedding')
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/embeddings'
THEN NULL
WHEN lower(trim(e.api_format)) IN ('openai:rerank', 'jina:rerank')
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/rerank'
THEN NULL
WHEN lower(trim(e.api_format)) = 'openai:image'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/images/generations'
THEN NULL
WHEN lower(trim(e.api_format)) = 'openai:video'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1/videos'
THEN NULL
WHEN lower(trim(e.api_format)) = 'gemini:generate_content'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1beta/models/{model}:{action}'
THEN NULL
WHEN lower(trim(e.api_format)) = 'gemini:embedding'
AND lower(trim(coalesce(e.custom_path, ''))) IN ('/v1beta/models/{model}:embedcontent', '/v1beta/models/{model}:{action}')
THEN NULL
WHEN lower(trim(e.api_format)) = 'gemini:video'
AND lower(trim(coalesce(e.custom_path, ''))) = '/v1beta/models/{model}:predictlongrunning'
THEN NULL
WHEN lower(trim(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN regexp_replace(trim(e.custom_path), '^/v1beta(?=/)', '', 'i')
ELSE regexp_replace(trim(e.custom_path), '^/v1(?=/)', '', 'i')
END
FROM public.providers p
WHERE lower(trim(e.api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
AND p.id = e.provider_id
AND lower(trim(coalesce(p.provider_type, ''))) NOT IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
AND (
(
lower(trim(e.api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND lower(trim(coalesce(e.custom_path, ''))) LIKE '/v1beta/%'
)
OR (
lower(trim(e.api_format)) NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND lower(trim(coalesce(e.custom_path, ''))) LIKE '/v1/%'
)
)
AND lower(rtrim(split_part(e.base_url, '?', 1), '/')) ~ '/v[0-9]+(beta[0-9]*)?(/|$)';
UPDATE public.provider_endpoints
SET custom_path = NULL
WHERE custom_path IS NOT NULL
AND trim(custom_path) = '';
@@ -0,0 +1,177 @@
WITH endpoint_url_parts AS (
SELECT
e.id,
CASE
WHEN instr(e.base_url, '?') > 0 THEN rtrim(substr(e.base_url, 1, instr(e.base_url, '?') - 1), '/')
ELSE rtrim(e.base_url, '/')
END AS base_without_query,
CASE
WHEN instr(e.base_url, '?') > 0 THEN substr(e.base_url, instr(e.base_url, '?'))
ELSE ''
END AS query_suffix,
lower(trim(e.api_format)) AS normalized_api_format,
lower(trim(coalesce(e.custom_path, ''))) AS normalized_custom_path,
lower(CASE
WHEN instr(e.base_url, '?') > 0 THEN rtrim(substr(e.base_url, 1, instr(e.base_url, '?') - 1), '/')
ELSE rtrim(e.base_url, '/')
END) AS normalized_base,
lower(trim(coalesce(p.provider_type, ''))) AS provider_type
FROM provider_endpoints e
LEFT JOIN providers p ON p.id = e.provider_id
WHERE lower(trim(e.api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
),
endpoint_api_root_updates AS (
SELECT
id,
base_without_query
|| CASE
WHEN normalized_api_format IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN '/v1beta'
ELSE '/v1'
END
|| query_suffix AS next_base_url
FROM endpoint_url_parts
WHERE provider_type NOT IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
AND normalized_base NOT GLOB '*/v[0-9]'
AND normalized_base NOT GLOB '*/v[0-9][0-9]'
AND normalized_base NOT GLOB '*/v[0-9]/*'
AND normalized_base NOT GLOB '*/v[0-9][0-9]/*'
AND normalized_base NOT GLOB '*/v[0-9]beta*'
AND normalized_base NOT GLOB '*/v[0-9][0-9]beta*'
AND (
(
normalized_api_format IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND normalized_custom_path LIKE '/v1beta/%'
)
OR (
normalized_api_format NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND normalized_custom_path LIKE '/v1/%'
)
OR normalized_custom_path = ''
)
)
UPDATE provider_endpoints
SET base_url = (
SELECT next_base_url
FROM endpoint_api_root_updates
WHERE endpoint_api_root_updates.id = provider_endpoints.id
)
WHERE id IN (SELECT id FROM endpoint_api_root_updates);
UPDATE provider_endpoints
SET custom_path = CASE
WHEN lower(trim(api_format)) = 'openai:chat'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/chat/completions'
THEN NULL
WHEN lower(trim(api_format)) = 'openai:responses'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/responses'
THEN NULL
WHEN lower(trim(api_format)) = 'openai:responses:compact'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/responses/compact'
THEN NULL
WHEN lower(trim(api_format)) = 'claude:messages'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/messages'
THEN NULL
WHEN lower(trim(api_format)) IN ('openai:embedding', 'jina:embedding')
AND lower(trim(coalesce(custom_path, ''))) = '/v1/embeddings'
THEN NULL
WHEN lower(trim(api_format)) IN ('openai:rerank', 'jina:rerank')
AND lower(trim(coalesce(custom_path, ''))) = '/v1/rerank'
THEN NULL
WHEN lower(trim(api_format)) = 'openai:image'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/images/generations'
THEN NULL
WHEN lower(trim(api_format)) = 'openai:video'
AND lower(trim(coalesce(custom_path, ''))) = '/v1/videos'
THEN NULL
WHEN lower(trim(api_format)) = 'gemini:generate_content'
AND lower(trim(coalesce(custom_path, ''))) = '/v1beta/models/{model}:{action}'
THEN NULL
WHEN lower(trim(api_format)) = 'gemini:embedding'
AND lower(trim(coalesce(custom_path, ''))) IN ('/v1beta/models/{model}:embedcontent', '/v1beta/models/{model}:{action}')
THEN NULL
WHEN lower(trim(api_format)) = 'gemini:video'
AND lower(trim(coalesce(custom_path, ''))) = '/v1beta/models/{model}:predictlongrunning'
THEN NULL
WHEN lower(trim(api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
THEN '/' || substr(trim(custom_path), 9)
ELSE '/' || substr(trim(custom_path), 5)
END
WHERE lower(trim(api_format)) IN (
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:embedding',
'openai:rerank',
'openai:image',
'openai:video',
'jina:embedding',
'jina:rerank',
'claude:messages',
'gemini:generate_content',
'gemini:embedding',
'gemini:video'
)
AND NOT EXISTS (
SELECT 1
FROM providers p
WHERE p.id = provider_endpoints.provider_id
AND lower(trim(coalesce(p.provider_type, ''))) IN (
'codex',
'chatgpt_web',
'claude_code',
'kiro',
'gemini_cli',
'vertex_ai',
'antigravity',
'grok',
'windsurf'
)
)
AND (
(
lower(trim(api_format)) IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND lower(trim(coalesce(custom_path, ''))) LIKE '/v1beta/%'
)
OR (
lower(trim(api_format)) NOT IN ('gemini:generate_content', 'gemini:embedding', 'gemini:video')
AND lower(trim(coalesce(custom_path, ''))) LIKE '/v1/%'
)
)
AND (
lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9]'
OR lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9][0-9]'
OR lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9]/*'
OR lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9][0-9]/*'
OR lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9]beta*'
OR lower(rtrim(CASE WHEN instr(base_url, '?') > 0 THEN substr(base_url, 1, instr(base_url, '?') - 1) ELSE base_url END, '/')) GLOB '*/v[0-9][0-9]beta*'
);
UPDATE provider_endpoints
SET custom_path = NULL
WHERE custom_path IS NOT NULL
AND trim(custom_path) = '';
@@ -5,7 +5,9 @@ use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use sqlx::{migrate::AppliedMigration, query, query_scalar, Connection, PgConnection, PgPool};
use sqlx::{
migrate::AppliedMigration, query, query_scalar, Connection, PgConnection, PgPool, SqlitePool,
};
use super::{
all_up_migrations, pending_migrations_from_applied, prepare_database_for_startup,
@@ -723,6 +725,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
20260520010000,
20260524000000,
20260527000000,
20260528000000,
]
);
assert_eq!(
@@ -748,10 +751,287 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
20260520010000,
20260524000000,
20260527000000,
20260528000000,
]
);
}
#[tokio::test]
async fn endpoint_api_root_migration_moves_v1_from_stored_default_paths() {
let pool = SqlitePool::connect("sqlite::memory:")
.await
.expect("sqlite pool should connect");
query(
r#"
CREATE TABLE providers (
id TEXT PRIMARY KEY,
provider_type TEXT NOT NULL
);
"#,
)
.execute(&pool)
.await
.expect("providers table should be created");
query(
r#"
CREATE TABLE provider_endpoints (
id TEXT PRIMARY KEY,
provider_id TEXT NOT NULL,
api_format TEXT NOT NULL,
base_url TEXT NOT NULL,
custom_path TEXT
);
"#,
)
.execute(&pool)
.await
.expect("provider_endpoints table should be created");
query(
r#"
INSERT INTO providers (id, provider_type) VALUES
('provider-custom', 'custom'),
('provider-fixed-vertex', 'vertex_ai'),
('provider-fixed-grok', 'grok');
"#,
)
.execute(&pool)
.await
.expect("providers fixture should insert");
query(
r#"
INSERT INTO provider_endpoints (id, provider_id, api_format, base_url, custom_path) VALUES
('openai-root', 'provider-custom', 'openai:chat', 'https://api.openai.example', NULL),
('responses-root', 'provider-custom', 'openai:responses', 'https://responses.example.com', NULL),
('responses-compact-root', 'provider-custom', 'openai:responses:compact', 'https://compact.example.com', NULL),
('openai-path-root', 'provider-custom', 'openai:chat', 'https://proxy.example.com/api', NULL),
('openai-old-default-path', 'provider-custom', 'openai:chat', 'https://proxy.example.com/api?tenant=demo', '/v1/chat/completions'),
('openai-mismatched-custom-path', 'provider-custom', 'openai:chat', 'https://proxy.example.com/api', '/v1/responses'),
('openai-v1-slash-old-default', 'provider-custom', 'openai:chat', 'https://already-versioned.example.com/v1/', '/v1/chat/completions'),
('openai-v4-old-default-path', 'provider-custom', 'openai:chat', 'https://open.bigmodel.cn/api/coding/paas/v4', '/v1/chat/completions'),
('embedding-root', 'provider-custom', 'openai:embedding', 'https://embedding.example.com', NULL),
('embedding-v4-old-default-path', 'provider-custom', 'openai:embedding', 'https://embedding.example.com/api/v4', '/v1/embeddings'),
('jina-embedding-root', 'provider-custom', 'jina:embedding', 'https://api.jina.example', NULL),
('rerank-old-default-path', 'provider-custom', 'openai:rerank', 'https://rerank.example.com/api', '/v1/rerank'),
('jina-rerank-old-default-path', 'provider-custom', 'jina:rerank', 'https://api.jina.example?tenant=demo', '/v1/rerank'),
('image-root', 'provider-custom', 'openai:image', 'https://image.example.com', NULL),
('image-edit-custom-path', 'provider-custom', 'openai:image', 'https://image.example.com/api', '/v1/images/edits'),
('image-v4-edit-custom-path', 'provider-custom', 'openai:image', 'https://image.example.com/api/v4', '/v1/images/edits'),
('video-root', 'provider-custom', 'openai:video', 'https://video.example.com', NULL),
('video-v1beta-old-default-path', 'provider-custom', 'openai:video', 'https://video.example.com/api/v1beta', '/v1/videos'),
('video-versioned-root', 'provider-custom', 'openai:video', 'https://ark.example.com/api/v3', NULL),
('google-versioned-segment-root', 'provider-custom', 'openai:embedding', 'https://generativelanguage.googleapis.com/v1beta/openai', NULL),
('gemini-root', 'provider-custom', 'gemini:generate_content', 'https://generativelanguage.googleapis.com', NULL),
('gemini-old-default-path', 'provider-custom', 'gemini:generate_content', 'https://generativelanguage.googleapis.com?tenant=demo', '/v1beta/models/{model}:{action}'),
('gemini-custom-path', 'provider-custom', 'gemini:generate_content', 'https://proxy.example.com/google', '/v1beta/models/gemini-upstream:generateContent'),
('gemini-versioned-old-default', 'provider-custom', 'gemini:generate_content', 'https://generativelanguage.googleapis.com/v1beta', '/v1beta/models/{model}:{action}'),
('gemini-embedding-root', 'provider-custom', 'gemini:embedding', 'https://generativelanguage.googleapis.com', NULL),
('gemini-embedding-old-default', 'provider-custom', 'gemini:embedding', 'https://generativelanguage.googleapis.com', '/v1beta/models/{model}:embedContent'),
('gemini-video-root', 'provider-custom', 'gemini:video', 'https://generativelanguage.googleapis.com', NULL),
('gemini-video-versioned-old-default', 'provider-custom', 'gemini:video', 'https://generativelanguage.googleapis.com/v1beta', '/v1beta/models/{model}:predictLongRunning'),
('fixed-vertex-gemini-root', 'provider-fixed-vertex', 'gemini:embedding', 'https://aiplatform.googleapis.com', NULL),
('claude-path-root', 'provider-custom', 'claude:messages', 'https://proxy.example.com/anthropic', NULL),
('claude-old-default-path', 'provider-custom', 'claude:messages', 'https://proxy.example.com/anthropic', '/v1/messages'),
('fixed-grok-root', 'provider-fixed-grok', 'openai:chat', 'https://grok.com', NULL);
"#,
)
.execute(&pool)
.await
.expect("endpoint fixture should insert");
let migration = super::sqlite::MIGRATOR
.iter()
.find(|migration| migration.version == 20260528000000)
.expect("endpoint API root migration should be embedded");
sqlx::raw_sql(migration.sql.as_ref())
.execute(&pool)
.await
.expect("endpoint API root migration should apply");
let rows: Vec<(String, String, Option<String>)> =
sqlx::query_as("SELECT id, base_url, custom_path FROM provider_endpoints ORDER BY id")
.fetch_all(&pool)
.await
.expect("endpoint rows should load");
let rows = rows
.into_iter()
.map(|(id, base_url, custom_path)| (id, (base_url, custom_path)))
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(
rows.get("openai-root"),
Some(&("https://api.openai.example/v1".to_string(), None))
);
assert_eq!(
rows.get("responses-root"),
Some(&("https://responses.example.com/v1".to_string(), None))
);
assert_eq!(
rows.get("responses-compact-root"),
Some(&("https://compact.example.com/v1".to_string(), None))
);
assert_eq!(
rows.get("openai-path-root"),
Some(&("https://proxy.example.com/api/v1".to_string(), None))
);
assert_eq!(
rows.get("openai-old-default-path"),
Some(&(
"https://proxy.example.com/api/v1?tenant=demo".to_string(),
None
))
);
assert_eq!(
rows.get("openai-mismatched-custom-path"),
Some(&(
"https://proxy.example.com/api/v1".to_string(),
Some("/responses".to_string())
))
);
assert_eq!(
rows.get("openai-v1-slash-old-default"),
Some(&(
"https://already-versioned.example.com/v1/".to_string(),
None
))
);
assert_eq!(
rows.get("openai-v4-old-default-path"),
Some(&(
"https://open.bigmodel.cn/api/coding/paas/v4".to_string(),
None
))
);
assert_eq!(
rows.get("embedding-root"),
Some(&("https://embedding.example.com/v1".to_string(), None))
);
assert_eq!(
rows.get("embedding-v4-old-default-path"),
Some(&("https://embedding.example.com/api/v4".to_string(), None))
);
assert_eq!(
rows.get("jina-embedding-root"),
Some(&("https://api.jina.example/v1".to_string(), None))
);
assert_eq!(
rows.get("rerank-old-default-path"),
Some(&("https://rerank.example.com/api/v1".to_string(), None))
);
assert_eq!(
rows.get("jina-rerank-old-default-path"),
Some(&("https://api.jina.example/v1?tenant=demo".to_string(), None))
);
assert_eq!(
rows.get("image-root"),
Some(&("https://image.example.com/v1".to_string(), None))
);
assert_eq!(
rows.get("image-edit-custom-path"),
Some(&(
"https://image.example.com/api/v1".to_string(),
Some("/images/edits".to_string())
))
);
assert_eq!(
rows.get("image-v4-edit-custom-path"),
Some(&(
"https://image.example.com/api/v4".to_string(),
Some("/images/edits".to_string())
))
);
assert_eq!(
rows.get("video-root"),
Some(&("https://video.example.com/v1".to_string(), None))
);
assert_eq!(
rows.get("video-v1beta-old-default-path"),
Some(&("https://video.example.com/api/v1beta".to_string(), None))
);
assert_eq!(
rows.get("video-versioned-root"),
Some(&("https://ark.example.com/api/v3".to_string(), None))
);
assert_eq!(
rows.get("google-versioned-segment-root"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta/openai".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-root"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-old-default-path"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta?tenant=demo".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-custom-path"),
Some(&(
"https://proxy.example.com/google/v1beta".to_string(),
Some("/models/gemini-upstream:generateContent".to_string())
))
);
assert_eq!(
rows.get("gemini-versioned-old-default"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-embedding-root"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-embedding-old-default"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-video-root"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("gemini-video-versioned-old-default"),
Some(&(
"https://generativelanguage.googleapis.com/v1beta".to_string(),
None
))
);
assert_eq!(
rows.get("fixed-vertex-gemini-root"),
Some(&("https://aiplatform.googleapis.com".to_string(), None))
);
assert_eq!(
rows.get("claude-path-root"),
Some(&("https://proxy.example.com/anthropic/v1".to_string(), None))
);
assert_eq!(
rows.get("claude-old-default-path"),
Some(&("https://proxy.example.com/anthropic/v1".to_string(), None))
);
assert_eq!(
rows.get("fixed-grok-root"),
Some(&("https://grok.com".to_string(), None))
);
}
#[test]
fn fresh_usage_schema_projects_upstream_stream_mode_for_all_drivers() {
let mysql_baseline = include_str!("../../../migrations/mysql/20260403000000_baseline.sql");