-- ============================================================================= -- ORDS-Endpunkte: KI-Kostenverfolgung (ai-costs) -- Ergänzt das Modul frigosped.dc um Template 17 (POST + GET). -- -- Voraussetzung: dc_ai_cost_log-Tabelle und DC_COSTS_PKG bereits vorhanden -- sqlplus user/pass@db @"Scripts/KI-Kostenverfolgung.sql" -- -- Deployment: -- sqlplus user/pass@db @"Scripts/ORDS-ai-costs-Endpunkt.sql" -- -- Idempotent: bestehende Handler werden zuerst gelöscht. -- ============================================================================= SET DEFINE OFF BEGIN -- Idempotenz: Handler entfernen falls vorhanden BEGIN ORDS.DELETE_HANDLER( p_module_name => 'frigosped.dc', p_uri_template => 'ai-costs', p_method => 'POST' ); EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ORDS.DELETE_HANDLER( p_module_name => 'frigosped.dc', p_uri_template => 'ai-costs', p_method => 'GET' ); EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ORDS.DELETE_TEMPLATE( p_module_name => 'frigosped.dc', p_uri_template => 'ai-costs' ); EXCEPTION WHEN OTHERS THEN NULL; END; -- Template anlegen ORDS.DEFINE_TEMPLATE( p_module_name => 'frigosped.dc', p_pattern => 'ai-costs', p_priority => 0, p_comments => 'KI-Kostenprotokoll: POST speichert Aufruf, GET liefert Auswertung' ); -- POST /api/dc/ai-costs ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'ai-costs', p_method => 'POST', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => '201 Created mit neuer cost_log id', p_source => ' DECLARE v_body CLOB; v_new_id NUMBER; BEGIN v_body := :body_text; INSERT INTO dc_ai_cost_log ( provider, model_name, operation, project_id, catalog_id, prompt_tokens, completion_tokens, total_tokens, cost_eur ) VALUES ( JSON_VALUE(v_body, ''$.provider''), JSON_VALUE(v_body, ''$.model_name''), JSON_VALUE(v_body, ''$.operation''), TO_NUMBER(JSON_VALUE(v_body, ''$.project_id'')), TO_NUMBER(JSON_VALUE(v_body, ''$.catalog_id'')), NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.prompt_tokens'')), 0), NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.completion_tokens'')), 0), NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.total_tokens'')), 0), NVL(TO_NUMBER(JSON_VALUE(v_body, ''$.cost_eur'')), 0) ) RETURNING id INTO v_new_id; :status_code := 201; HTP.P(''{"id":'' || v_new_id || ''}''); EXCEPTION WHEN OTHERS THEN ROLLBACK; :status_code := 500; HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'' ); END; ' ); -- GET /api/dc/ai-costs ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'ai-costs', p_method => 'GET', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => 'Kostenauswertung: total, current_month, monatliche Aufschluesselung via DC_COSTS_PKG', p_source => ' DECLARE v_total_eur NUMBER; v_total_tokens NUMBER; v_total_calls NUMBER; v_month_eur NUMBER; v_month_tokens NUMBER; v_month_calls NUMBER; v_monthly_json CLOB; BEGIN DC_COSTS_PKG.get_summary( v_total_eur, v_total_tokens, v_total_calls, v_month_eur, v_month_tokens, v_month_calls ); SELECT JSON_ARRAYAGG( JSON_OBJECT( ''month'' VALUE month, ''cost_eur'' VALUE ROUND(cost_eur, 6), ''prompt_tokens'' VALUE prompt_tokens, ''completion_tokens'' VALUE completion_tokens, ''total_tokens'' VALUE total_tokens, ''call_count'' VALUE call_count ) ORDER BY month DESC RETURNING CLOB ) INTO v_monthly_json FROM ( SELECT TO_CHAR(called_at, ''YYYY-MM'') AS month, SUM(cost_eur) AS cost_eur, SUM(prompt_tokens) AS prompt_tokens, SUM(completion_tokens) AS completion_tokens, SUM(total_tokens) AS total_tokens, COUNT(*) AS call_count FROM dc_ai_cost_log WHERE called_at >= ADD_MONTHS(TRUNC(SYSDATE, ''MM''), -11) GROUP BY TO_CHAR(called_at, ''YYYY-MM'') ); :status_code := 200; HTP.P( ''{"total":{'' || '' "cost_eur":'' || ROUND(NVL(v_total_eur, 0), 6) || '', "total_tokens":'' || NVL(v_total_tokens, 0) || '', "call_count":'' || NVL(v_total_calls, 0) || ''}, "current_month":{'' || '' "month":"'' || TO_CHAR(SYSDATE, ''YYYY-MM'') || ''"'' || '', "cost_eur":'' || ROUND(NVL(v_month_eur, 0), 6) || '', "total_tokens":'' || NVL(v_month_tokens, 0) || '', "call_count":'' || NVL(v_month_calls, 0) || ''}, "monthly":'' || NVL(v_monthly_json, ''[]'') || ''}'' ); EXCEPTION WHEN OTHERS THEN :status_code := 500; HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'' ); END; ' ); COMMIT; END; /