mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 14:10:19 +08:00
Merge branch 'fawney19:main' into main
This commit is contained in:
@@ -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) = '';
|
||||
+155
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user