482 lines
30 KiB
PL/PgSQL
482 lines
30 KiB
PL/PgSQL
begin;
|
|
|
|
create or replace function public.model_provider_base_url_is_safe(p_value text)
|
|
returns boolean language plpgsql immutable set search_path=''
|
|
as $$
|
|
declare v_url text:=lower(btrim(coalesce(p_value,''))); v_host text; v_port text;
|
|
begin
|
|
if char_length(v_url) not between 1 and 2048 or v_url !~ '^https://[^/:?#]+(:[0-9]{1,5})?(/[^?#]*)?$' then return false; end if;
|
|
v_host:=substring(v_url from '^https://([^/:?#]+)');
|
|
if v_host is null or position('.' in v_host)=0 or v_host !~ '^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$'
|
|
or v_host like '%.%' and (v_host like '%..%' or v_host like '%.-%' or v_host like '%-.%')
|
|
or v_host ~ '^[0-9.]+$' or v_host ~ '(^|\.)(localhost|local|internal|lan|home|arpa)$'
|
|
then return false; end if;
|
|
v_port:=substring(v_url from '^https://[^/:?#]+:([0-9]+)');
|
|
return v_port is null or v_port::integer between 1 and 65535;
|
|
exception when others then return false;
|
|
end $$;
|
|
|
|
create or replace function public.expected_model_provider_secret_ref(p_code text,p_provider_type text)
|
|
returns text language plpgsql immutable set search_path=''
|
|
as $$
|
|
declare v_code text:=btrim(coalesce(p_code,''));
|
|
begin
|
|
if p_provider_type='openai' then return 'env:OPENAI_API_KEY'; end if;
|
|
if p_provider_type<>'openai-compatible' or v_code !~ '^[a-z][a-z0-9_-]{1,63}$' then return null; end if;
|
|
if v_code='deepseek' then return 'env:DEEPSEEK_API_KEY'; end if;
|
|
if v_code='legacy-compatible' then return 'env:LLM_API_KEY'; end if;
|
|
return 'env:MODEL_PROVIDER_' || upper(regexp_replace(v_code,'[^a-z0-9]+','_','g')) || '_API_KEY';
|
|
end $$;
|
|
|
|
create or replace function public.model_settings_contain_secrets(p_value jsonb)
|
|
returns boolean language plpgsql immutable set search_path=''
|
|
as $$
|
|
declare v_key text; v_child jsonb; v_normalized text;
|
|
begin
|
|
if p_value is null then return false; end if;
|
|
if jsonb_typeof(p_value)='array' then
|
|
for v_child in select value from jsonb_array_elements(p_value) loop
|
|
if public.model_settings_contain_secrets(v_child) then return true; end if;
|
|
end loop;
|
|
elsif jsonb_typeof(p_value)='object' then
|
|
for v_key,v_child in select key,value from jsonb_each(p_value) loop
|
|
v_normalized:=regexp_replace(lower(v_key),'[^a-z0-9]','','g');
|
|
if v_normalized in ('apikey','authorization','cookie','databaseurl','connectionstring','password','privatekey','clientsecret','secret','token')
|
|
or v_normalized ~ '(apikey|password|authorization|cookie|databaseurl|connectionstring|privatekey|clientsecret|accesstoken|refreshtoken|authtoken|bearertoken)$'
|
|
or public.model_settings_contain_secrets(v_child)
|
|
then return true; end if;
|
|
end loop;
|
|
end if;
|
|
return false;
|
|
end $$;
|
|
|
|
create table if not exists public.model_providers (
|
|
id uuid primary key default gen_random_uuid(),
|
|
code text not null unique check (code ~ '^[a-z][a-z0-9_-]{1,63}$'),
|
|
name text not null check (char_length(name) between 1 and 80),
|
|
provider_type text not null check (provider_type in ('openai', 'openai-compatible')),
|
|
base_url text,
|
|
secret_ref text not null check (
|
|
secret_ref=public.expected_model_provider_secret_ref(code,provider_type)
|
|
),
|
|
enabled boolean not null default true,
|
|
created_by uuid references auth.users(id) on delete set null,
|
|
updated_by uuid references auth.users(id) on delete set null,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
check ((provider_type='openai' and base_url is null)
|
|
or (provider_type='openai-compatible' and public.model_provider_base_url_is_safe(base_url)))
|
|
);
|
|
|
|
create table if not exists public.model_configs (
|
|
id uuid primary key default gen_random_uuid(),
|
|
model_id text not null unique check (model_id ~ '^[a-z0-9][a-z0-9._-]{0,63}$'),
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table if not exists public.model_config_versions (
|
|
id uuid primary key default gen_random_uuid(),
|
|
config_id uuid not null references public.model_configs(id) on delete restrict,
|
|
version integer not null check (version > 0),
|
|
provider_id uuid not null references public.model_providers(id) on delete restrict,
|
|
label text not null check (char_length(label) between 1 and 60),
|
|
description text not null default '' check (char_length(description) <= 200),
|
|
provider_model text not null check (char_length(provider_model) between 1 and 160),
|
|
model_tier text not null default 'standard' check (model_tier in ('standard','premium','internal')),
|
|
credit_cost integer not null default 1 check (credit_cost > 0),
|
|
context_window integer check (context_window is null or context_window > 0),
|
|
input_cost_microusd_per_million bigint not null default 0 check (input_cost_microusd_per_million >= 0),
|
|
output_cost_microusd_per_million bigint not null default 0 check (output_cost_microusd_per_million >= 0),
|
|
enabled boolean not null default true,
|
|
is_default boolean not null default false,
|
|
fallback_model_id text,
|
|
status text not null default 'draft' check (status in ('draft','published','retired')),
|
|
settings jsonb not null default '{}'::jsonb check (jsonb_typeof(settings)='object' and not public.model_settings_contain_secrets(settings)),
|
|
created_by uuid references auth.users(id) on delete set null,
|
|
created_at timestamptz not null default now(),
|
|
published_at timestamptz,
|
|
retired_at timestamptz,
|
|
unique(config_id,version),
|
|
check (not is_default or enabled)
|
|
);
|
|
create unique index if not exists model_versions_one_draft_idx on public.model_config_versions(config_id) where status='draft';
|
|
create unique index if not exists model_versions_one_published_idx on public.model_config_versions(config_id) where status='published';
|
|
create unique index if not exists model_versions_one_default_idx on public.model_config_versions((is_default)) where status='published' and enabled and is_default;
|
|
|
|
create or replace function public.prevent_published_model_provider_runtime_mutation()
|
|
returns trigger language plpgsql set search_path=''
|
|
as $$
|
|
begin
|
|
if (new.code,new.provider_type,new.base_url,new.secret_ref,new.enabled)
|
|
is distinct from
|
|
(old.code,old.provider_type,old.base_url,old.secret_ref,old.enabled)
|
|
and exists(
|
|
select 1 from public.model_config_versions
|
|
where provider_id=old.id and status in ('published','retired')
|
|
)
|
|
then
|
|
raise exception 'model_provider_runtime_immutable' using
|
|
errcode='23514',
|
|
hint='Create a new provider and model config version, then test and publish it.';
|
|
end if;
|
|
return new;
|
|
end $$;
|
|
|
|
drop trigger if exists model_providers_prevent_published_runtime_mutation on public.model_providers;
|
|
create trigger model_providers_prevent_published_runtime_mutation
|
|
before update of code,provider_type,base_url,secret_ref,enabled on public.model_providers
|
|
for each row execute function public.prevent_published_model_provider_runtime_mutation();
|
|
|
|
create table if not exists public.model_publish_events (
|
|
id uuid primary key default gen_random_uuid(),
|
|
config_id uuid not null references public.model_configs(id) on delete restrict,
|
|
from_version_id uuid references public.model_config_versions(id) on delete restrict,
|
|
to_version_id uuid not null references public.model_config_versions(id) on delete restrict,
|
|
action text not null check (action in ('publish','rollback')),
|
|
actor_user_id uuid not null references auth.users(id) on delete restrict,
|
|
reason text not null check (char_length(btrim(reason)) between 1 and 500),
|
|
request_id text not null,
|
|
created_at timestamptz not null default clock_timestamp(),
|
|
unique(actor_user_id,request_id,action,config_id)
|
|
);
|
|
|
|
create or replace function public.model_version_config_hash(p_version_id uuid)
|
|
returns text language sql stable security definer set search_path=''
|
|
as $$
|
|
select encode(public.digest(convert_to(jsonb_build_object(
|
|
'versionId',v.id,
|
|
'configId',v.config_id,
|
|
'version',v.version,
|
|
'providerId',v.provider_id,
|
|
'label',v.label,
|
|
'description',v.description,
|
|
'providerModel',v.provider_model,
|
|
'modelTier',v.model_tier,
|
|
'creditCost',v.credit_cost,
|
|
'contextWindow',v.context_window,
|
|
'inputCost',v.input_cost_microusd_per_million,
|
|
'outputCost',v.output_cost_microusd_per_million,
|
|
'enabled',v.enabled,
|
|
'isDefault',v.is_default,
|
|
'fallbackModelId',v.fallback_model_id,
|
|
'settings',v.settings,
|
|
'providerCode',p.code,
|
|
'providerType',p.provider_type,
|
|
'providerBaseUrl',p.base_url,
|
|
'providerSecretRef',p.secret_ref,
|
|
'providerEnabled',p.enabled
|
|
)::text,'utf8'),'sha256'),'hex')
|
|
from public.model_config_versions v
|
|
join public.model_providers p on p.id=v.provider_id
|
|
where v.id=p_version_id
|
|
$$;
|
|
|
|
create table if not exists public.model_connection_test_evidence (
|
|
id uuid primary key default gen_random_uuid(),
|
|
version_id uuid not null references public.model_config_versions(id) on delete cascade,
|
|
provider_id uuid not null references public.model_providers(id) on delete restrict,
|
|
config_hash text not null check (config_hash ~ '^[0-9a-f]{64}$'),
|
|
http_status integer not null check (http_status between 0 and 599),
|
|
actor_user_id uuid not null references auth.users(id) on delete restrict,
|
|
request_id text not null check (char_length(btrim(request_id)) between 1 and 200),
|
|
tested_at timestamptz not null,
|
|
expires_at timestamptz not null,
|
|
created_at timestamptz not null default clock_timestamp(),
|
|
check (expires_at > tested_at),
|
|
unique(actor_user_id,request_id,version_id)
|
|
);
|
|
create index if not exists model_connection_test_evidence_lookup_idx
|
|
on public.model_connection_test_evidence(version_id,tested_at desc);
|
|
|
|
create or replace function public.model_connection_test_is_fresh(p_version_id uuid,p_request_id text default null)
|
|
returns boolean language sql stable security definer set search_path=''
|
|
as $$
|
|
select coalesce((
|
|
select e.http_status between 200 and 299 and e.expires_at>clock_timestamp()
|
|
from public.model_connection_test_evidence e
|
|
where e.version_id=p_version_id
|
|
and e.provider_id=(select v.provider_id from public.model_config_versions v where v.id=p_version_id)
|
|
and e.config_hash=public.model_version_config_hash(p_version_id)
|
|
and (p_request_id is null or e.request_id=p_request_id)
|
|
order by e.tested_at desc,e.id desc
|
|
limit 1
|
|
),false)
|
|
$$;
|
|
|
|
create or replace function public.admin_record_model_connection_test(
|
|
p_actor_user_id uuid,p_version_id uuid,p_http_status integer,p_request_id text
|
|
)
|
|
returns uuid language plpgsql security definer set search_path=''
|
|
as $$
|
|
declare v_provider_id uuid; v_hash text; v_id uuid; v_now timestamptz:=clock_timestamp();
|
|
begin
|
|
if not (public.admin_has_permission(p_actor_user_id,'models.test')
|
|
or public.admin_has_permission(p_actor_user_id,'models.rollback'))
|
|
then raise exception 'admin_permission_denied' using errcode='42501'; end if;
|
|
if p_http_status not between 0 and 599 then raise exception 'model_connection_status_invalid' using errcode='22023'; end if;
|
|
if char_length(btrim(coalesce(p_request_id,''))) not between 1 and 200 then raise exception 'request_id_invalid' using errcode='22023'; end if;
|
|
select provider_id,public.model_version_config_hash(id) into v_provider_id,v_hash
|
|
from public.model_config_versions where id=p_version_id;
|
|
if v_provider_id is null or v_hash is null then raise exception 'model_version_not_found' using errcode='22023'; end if;
|
|
insert into public.model_connection_test_evidence(
|
|
version_id,provider_id,config_hash,http_status,actor_user_id,request_id,tested_at,expires_at
|
|
) values(
|
|
p_version_id,v_provider_id,v_hash,p_http_status,p_actor_user_id,btrim(p_request_id),v_now,v_now+interval '10 minutes'
|
|
)
|
|
on conflict(actor_user_id,request_id,version_id) do update set
|
|
provider_id=excluded.provider_id,config_hash=excluded.config_hash,http_status=excluded.http_status,
|
|
tested_at=excluded.tested_at,expires_at=excluded.expires_at
|
|
returning id into v_id;
|
|
return v_id;
|
|
end $$;
|
|
|
|
create or replace function public.admin_save_model_provider(
|
|
p_actor_user_id uuid,p_provider_id uuid,p_code text,p_name text,p_provider_type text,p_base_url text,
|
|
p_secret_ref text,p_enabled boolean,p_reason text,p_request_id text
|
|
)
|
|
returns uuid language plpgsql security definer set search_path=''
|
|
as $$ declare v_id uuid; v_expected_secret_ref text;
|
|
begin
|
|
if not public.admin_has_permission(p_actor_user_id,'models.write') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
|
|
if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
|
|
v_expected_secret_ref:=public.expected_model_provider_secret_ref(p_code,p_provider_type);
|
|
if v_expected_secret_ref is null or p_secret_ref is distinct from v_expected_secret_ref then
|
|
raise exception 'model_provider_secret_ref_forbidden' using errcode='23514';
|
|
end if;
|
|
if (p_provider_type='openai' and nullif(btrim(p_base_url),'') is not null)
|
|
or (p_provider_type='openai-compatible' and not public.model_provider_base_url_is_safe(p_base_url))
|
|
then raise exception 'model_provider_url_unsafe' using errcode='23514'; end if;
|
|
if p_provider_id is null then
|
|
insert into public.model_providers(code,name,provider_type,base_url,secret_ref,enabled,created_by,updated_by)
|
|
values(p_code,p_name,p_provider_type,nullif(btrim(p_base_url),''),v_expected_secret_ref,p_enabled,p_actor_user_id,p_actor_user_id) returning id into v_id;
|
|
else
|
|
update public.model_providers set code=p_code,name=p_name,provider_type=p_provider_type,base_url=nullif(btrim(p_base_url),''),
|
|
secret_ref=v_expected_secret_ref,enabled=p_enabled,updated_by=p_actor_user_id,updated_at=clock_timestamp()
|
|
where id=p_provider_id returning id into v_id;
|
|
if v_id is null then raise exception 'provider_not_found' using errcode='22023'; end if;
|
|
end if;
|
|
insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
|
|
request_id,permission_used,reason)
|
|
select p_actor_user_id,lower(btrim(u.email)),'admin','models.provider.save','model_provider',v_id,
|
|
jsonb_build_object('providerCode',p_code,'credentialConfigured',true,'enabled',p_enabled),p_request_id,'models.write',btrim(p_reason)
|
|
from identity.users u where u.id=p_actor_user_id on conflict do nothing;
|
|
return v_id;
|
|
end $$;
|
|
|
|
create or replace function public.admin_save_model_draft(
|
|
p_actor_user_id uuid,p_model_id text,p_version_id uuid,p_provider_id uuid,p_label text,p_description text,
|
|
p_provider_model text,p_model_tier text,p_credit_cost integer,p_context_window integer,
|
|
p_input_cost bigint,p_output_cost bigint,p_enabled boolean,p_is_default boolean,p_fallback_model_id text,
|
|
p_settings jsonb,p_reason text,p_request_id text
|
|
)
|
|
returns uuid language plpgsql security definer set search_path=''
|
|
as $$ declare v_config_id uuid; v_version integer; v_id uuid;
|
|
begin
|
|
if not public.admin_has_permission(p_actor_user_id,'models.write') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
|
|
if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
|
|
if p_is_default and not p_enabled then raise exception 'default_model_disabled' using errcode='23514'; end if;
|
|
if public.model_settings_contain_secrets(coalesce(p_settings,'{}'::jsonb)) then raise exception 'model_settings_secret_forbidden' using errcode='23514'; end if;
|
|
insert into public.model_configs(model_id) values(p_model_id) on conflict(model_id) do update set model_id=excluded.model_id returning id into v_config_id;
|
|
if p_version_id is null then
|
|
select coalesce(max(version),0)+1 into v_version from public.model_config_versions where config_id=v_config_id;
|
|
insert into public.model_config_versions(config_id,version,provider_id,label,description,provider_model,model_tier,
|
|
credit_cost,context_window,input_cost_microusd_per_million,output_cost_microusd_per_million,enabled,is_default,
|
|
fallback_model_id,settings,created_by)
|
|
values(v_config_id,v_version,p_provider_id,p_label,p_description,p_provider_model,p_model_tier,p_credit_cost,p_context_window,
|
|
p_input_cost,p_output_cost,p_enabled,p_is_default,nullif(btrim(p_fallback_model_id),''),coalesce(p_settings,'{}'::jsonb),p_actor_user_id)
|
|
returning id into v_id;
|
|
else
|
|
update public.model_config_versions set provider_id=p_provider_id,label=p_label,description=p_description,
|
|
provider_model=p_provider_model,model_tier=p_model_tier,credit_cost=p_credit_cost,context_window=p_context_window,
|
|
input_cost_microusd_per_million=p_input_cost,output_cost_microusd_per_million=p_output_cost,enabled=p_enabled,
|
|
is_default=p_is_default,fallback_model_id=nullif(btrim(p_fallback_model_id),''),settings=coalesce(p_settings,'{}'::jsonb)
|
|
where id=p_version_id and config_id=v_config_id and status='draft' returning id into v_id;
|
|
if v_id is null then raise exception 'model_draft_not_found' using errcode='22023'; end if;
|
|
end if;
|
|
insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
|
|
request_id,permission_used,reason)
|
|
select p_actor_user_id,lower(btrim(u.email)),'admin','models.draft.save','model_config_version',v_id,
|
|
jsonb_build_object('modelId',p_model_id,'providerId',p_provider_id,'enabled',p_enabled,'isDefault',p_is_default),
|
|
p_request_id,'models.write',btrim(p_reason) from identity.users u where u.id=p_actor_user_id on conflict do nothing;
|
|
return v_id;
|
|
end $$;
|
|
|
|
create or replace function public.admin_publish_model(
|
|
p_actor_user_id uuid,p_version_id uuid,p_reason text,p_request_id text
|
|
)
|
|
returns uuid language plpgsql security definer set search_path=''
|
|
as $$
|
|
declare v_draft public.model_config_versions%rowtype; v_model_id text; v_from uuid; v_cycle boolean;
|
|
begin
|
|
if not public.admin_has_permission(p_actor_user_id,'models.publish') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
|
|
if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
|
|
select * into v_draft from public.model_config_versions where id=p_version_id and status='draft' for update;
|
|
if not found then raise exception 'model_draft_not_found' using errcode='22023'; end if;
|
|
if v_draft.is_default and not v_draft.enabled then raise exception 'default_model_disabled' using errcode='23514'; end if;
|
|
if public.model_settings_contain_secrets(v_draft.settings) then raise exception 'model_settings_secret_forbidden' using errcode='23514'; end if;
|
|
perform 1 from public.model_providers p where p.id=v_draft.provider_id and p.enabled
|
|
and p.secret_ref=public.expected_model_provider_secret_ref(p.code,p.provider_type)
|
|
and (p.provider_type='openai' or public.model_provider_base_url_is_safe(p.base_url))
|
|
for update;
|
|
if not found then raise exception 'model_provider_unavailable' using errcode='23514'; end if;
|
|
if not public.model_connection_test_is_fresh(p_version_id,null) then
|
|
raise exception 'model_connection_test_required' using errcode='23514';
|
|
end if;
|
|
select model_id into v_model_id from public.model_configs where id=v_draft.config_id;
|
|
if v_draft.fallback_model_id=v_model_id then raise exception 'model_fallback_cycle' using errcode='23514'; end if;
|
|
if v_draft.fallback_model_id is not null and not exists(
|
|
select 1 from public.model_configs c
|
|
join public.model_config_versions v on v.config_id=c.id
|
|
join public.model_providers p on p.id=v.provider_id
|
|
where c.model_id=v_draft.fallback_model_id and v.status='published' and v.enabled and p.enabled
|
|
and p.secret_ref=public.expected_model_provider_secret_ref(p.code,p.provider_type)
|
|
and (p.provider_type='openai' or public.model_provider_base_url_is_safe(p.base_url))
|
|
) then raise exception 'fallback_model_unavailable' using errcode='23514'; end if;
|
|
with recursive edges(model_id,fallback_model_id) as (
|
|
select c.model_id,case when v.id=p_version_id then v_draft.fallback_model_id else v.fallback_model_id end
|
|
from public.model_configs c join public.model_config_versions v on v.config_id=c.id
|
|
where (v.status='published' and v.config_id<>v_draft.config_id) or v.id=p_version_id
|
|
), walk(origin,node,path,cycle) as (
|
|
select model_id,fallback_model_id,array[model_id],false from edges where fallback_model_id is not null
|
|
union all
|
|
select w.origin,e.fallback_model_id,w.path||e.model_id,e.model_id=any(w.path)
|
|
from walk w join edges e on e.model_id=w.node where not w.cycle and e.fallback_model_id is not null
|
|
) select coalesce(bool_or(cycle),false) into v_cycle from walk;
|
|
if v_cycle then raise exception 'model_fallback_cycle' using errcode='23514'; end if;
|
|
select id into v_from from public.model_config_versions where config_id=v_draft.config_id and status='published' for update;
|
|
if v_from is not null and not v_draft.is_default and exists(
|
|
select 1 from public.model_config_versions where id=v_from and is_default
|
|
) then
|
|
if not v_draft.enabled then raise exception 'default_model_disabled' using errcode='23514'; end if;
|
|
update public.model_config_versions set is_default=true where id=p_version_id;
|
|
v_draft.is_default:=true;
|
|
end if;
|
|
if v_draft.is_default then update public.model_config_versions set is_default=false where status='published' and is_default; end if;
|
|
update public.model_config_versions set status='retired',is_default=false,retired_at=clock_timestamp() where id=v_from;
|
|
update public.model_config_versions set status='published',published_at=clock_timestamp(),retired_at=null where id=p_version_id;
|
|
if not exists(
|
|
select 1 from public.model_config_versions v join public.model_providers p on p.id=v.provider_id
|
|
where v.status='published' and v.enabled and v.is_default and p.enabled
|
|
and p.secret_ref=public.expected_model_provider_secret_ref(p.code,p.provider_type)
|
|
and (p.provider_type='openai' or public.model_provider_base_url_is_safe(p.base_url))
|
|
) then raise exception 'default_model_required' using errcode='23514'; end if;
|
|
insert into public.model_publish_events(config_id,from_version_id,to_version_id,action,actor_user_id,reason,request_id)
|
|
values(v_draft.config_id,v_from,p_version_id,'publish',p_actor_user_id,btrim(p_reason),p_request_id) on conflict do nothing;
|
|
insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
|
|
request_id,permission_used,reason)
|
|
select p_actor_user_id,lower(btrim(u.email)),'admin','models.publish','model_config_version',p_version_id,
|
|
jsonb_build_object('modelId',v_model_id,'version',v_draft.version),p_request_id,'models.publish',btrim(p_reason)
|
|
from identity.users u where u.id=p_actor_user_id on conflict do nothing;
|
|
return p_version_id;
|
|
end $$;
|
|
|
|
create or replace function public.admin_rollback_model(
|
|
p_actor_user_id uuid,p_config_id uuid,p_target_version integer,p_reason text,p_request_id text
|
|
)
|
|
returns uuid language plpgsql security definer set search_path=''
|
|
as $$
|
|
declare v_current public.model_config_versions%rowtype; v_target public.model_config_versions%rowtype;
|
|
begin
|
|
if not public.admin_has_permission(p_actor_user_id,'models.rollback') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
|
|
if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
|
|
select * into v_current from public.model_config_versions where config_id=p_config_id and status='published' for update;
|
|
select * into v_target from public.model_config_versions where config_id=p_config_id and version=p_target_version and status='retired' for update;
|
|
if v_current.id is null or v_target.id is null then raise exception 'rollback_version_not_found' using errcode='22023'; end if;
|
|
if not v_target.enabled then raise exception 'rollback_model_disabled' using errcode='23514'; end if;
|
|
if public.model_settings_contain_secrets(v_target.settings) then raise exception 'model_settings_secret_forbidden' using errcode='23514'; end if;
|
|
if not exists(
|
|
select 1 from public.model_providers p where p.id=v_target.provider_id and p.enabled
|
|
and p.secret_ref=public.expected_model_provider_secret_ref(p.code,p.provider_type)
|
|
and (p.provider_type='openai' or public.model_provider_base_url_is_safe(p.base_url))
|
|
) then raise exception 'model_provider_unavailable' using errcode='23514'; end if;
|
|
if not public.model_connection_test_is_fresh(v_target.id,p_request_id) then
|
|
raise exception 'model_connection_test_required' using errcode='23514';
|
|
end if;
|
|
v_target.is_default:=v_current.is_default;
|
|
if v_target.is_default then update public.model_config_versions set is_default=false where status='published' and is_default; end if;
|
|
update public.model_config_versions set status='retired',is_default=false,retired_at=clock_timestamp() where id=v_current.id;
|
|
update public.model_config_versions set status='published',is_default=v_target.is_default,published_at=clock_timestamp(),retired_at=null where id=v_target.id;
|
|
if not exists(
|
|
select 1 from public.model_config_versions v join public.model_providers p on p.id=v.provider_id
|
|
where v.status='published' and v.enabled and v.is_default and p.enabled
|
|
and p.secret_ref=public.expected_model_provider_secret_ref(p.code,p.provider_type)
|
|
and (p.provider_type='openai' or public.model_provider_base_url_is_safe(p.base_url))
|
|
) then raise exception 'default_model_required' using errcode='23514'; end if;
|
|
insert into public.model_publish_events(config_id,from_version_id,to_version_id,action,actor_user_id,reason,request_id)
|
|
values(p_config_id,v_current.id,v_target.id,'rollback',p_actor_user_id,btrim(p_reason),p_request_id) on conflict do nothing;
|
|
insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
|
|
request_id,permission_used,reason)
|
|
select p_actor_user_id,lower(btrim(u.email)),'admin','models.rollback','model_config_version',v_target.id,
|
|
jsonb_build_object('version',p_target_version),p_request_id,'models.rollback',btrim(p_reason)
|
|
from identity.users u where u.id=p_actor_user_id on conflict do nothing;
|
|
return v_target.id;
|
|
end $$;
|
|
|
|
alter table public.chat_sessions add column if not exists model_config_version integer;
|
|
|
|
update public.chat_sessions s
|
|
set model_config_version=v.version
|
|
from public.model_configs c
|
|
join public.model_config_versions v on v.config_id=c.id and v.status='published'
|
|
where s.model_id=c.model_id and s.model_config_version is null;
|
|
|
|
create or replace function public.pin_chat_session_model_config_version()
|
|
returns trigger language plpgsql security definer set search_path=''
|
|
as $$
|
|
begin
|
|
if tg_op='UPDATE' and new.model_id is not distinct from old.model_id and old.model_config_version is not null then
|
|
new.model_config_version:=old.model_config_version;
|
|
return new;
|
|
end if;
|
|
if new.model_id is null then
|
|
new.model_config_version:=null;
|
|
return new;
|
|
end if;
|
|
select v.version into new.model_config_version
|
|
from public.model_configs c
|
|
join public.model_config_versions v on v.config_id=c.id
|
|
where c.model_id=new.model_id and v.status='published' and v.enabled
|
|
limit 1;
|
|
if not found then new.model_config_version:=null; end if;
|
|
return new;
|
|
end $$;
|
|
|
|
drop trigger if exists chat_sessions_pin_model_config_version on public.chat_sessions;
|
|
create trigger chat_sessions_pin_model_config_version
|
|
before insert or update of model_id,model_config_version on public.chat_sessions
|
|
for each row execute function public.pin_chat_session_model_config_version();
|
|
|
|
alter table public.model_providers enable row level security;
|
|
alter table public.model_configs enable row level security;
|
|
alter table public.model_config_versions enable row level security;
|
|
alter table public.model_publish_events enable row level security;
|
|
alter table public.model_connection_test_evidence enable row level security;
|
|
revoke all on table public.model_providers,public.model_configs,public.model_config_versions,public.model_publish_events,public.model_connection_test_evidence from public,anon,authenticated;
|
|
grant select on table public.model_providers,public.model_configs,public.model_config_versions,public.model_publish_events,public.model_connection_test_evidence to service_role;
|
|
revoke all on function public.model_provider_base_url_is_safe(text),public.expected_model_provider_secret_ref(text,text),
|
|
public.model_settings_contain_secrets(jsonb),public.prevent_published_model_provider_runtime_mutation(),
|
|
public.model_version_config_hash(uuid),public.model_connection_test_is_fresh(uuid,text),
|
|
public.admin_record_model_connection_test(uuid,uuid,integer,text),
|
|
public.admin_save_model_provider(uuid,uuid,text,text,text,text,text,boolean,text,text),
|
|
public.admin_save_model_draft(uuid,text,uuid,uuid,text,text,text,text,integer,integer,bigint,bigint,boolean,boolean,text,jsonb,text,text),
|
|
public.admin_publish_model(uuid,uuid,text,text),public.admin_rollback_model(uuid,uuid,integer,text,text),
|
|
public.pin_chat_session_model_config_version()
|
|
from public,anon,authenticated;
|
|
grant execute on function public.model_provider_base_url_is_safe(text),public.expected_model_provider_secret_ref(text,text),
|
|
public.model_settings_contain_secrets(jsonb),public.model_version_config_hash(uuid),public.model_connection_test_is_fresh(uuid,text),
|
|
public.admin_record_model_connection_test(uuid,uuid,integer,text),
|
|
public.admin_save_model_provider(uuid,uuid,text,text,text,text,text,boolean,text,text),
|
|
public.admin_save_model_draft(uuid,text,uuid,uuid,text,text,text,text,integer,integer,bigint,bigint,boolean,boolean,text,jsonb,text,text),
|
|
public.admin_publish_model(uuid,uuid,text,text),public.admin_rollback_model(uuid,uuid,integer,text,text) to service_role;
|
|
do $$ begin if exists(select 1 from pg_roles where rolname='admin_runtime') then
|
|
grant select on table public.model_providers,public.model_configs,public.model_config_versions,public.model_publish_events,public.model_connection_test_evidence to admin_runtime;
|
|
grant execute on function public.model_provider_base_url_is_safe(text),public.expected_model_provider_secret_ref(text,text),
|
|
public.model_settings_contain_secrets(jsonb),public.model_version_config_hash(uuid),public.model_connection_test_is_fresh(uuid,text),
|
|
public.admin_record_model_connection_test(uuid,uuid,integer,text),
|
|
public.admin_save_model_provider(uuid,uuid,text,text,text,text,text,boolean,text,text),
|
|
public.admin_save_model_draft(uuid,text,uuid,uuid,text,text,text,text,integer,integer,bigint,bigint,boolean,boolean,text,jsonb,text,text),
|
|
public.admin_publish_model(uuid,uuid,text,text),public.admin_rollback_model(uuid,uuid,integer,text,text) to admin_runtime;
|
|
end if; end $$;
|
|
|
|
commit;
|