AI usage and cost attribution across agents, CoWork, Cortex Code, and AI functions: code

Every file in the AI usage and cost attribution across agents, CoWork, Cortex Code, and AI functions recipe. To run the recipe, download ai-usage-cost-attribution.zip, unzip it, and run the scripts from the ai-usage-cost-attribution/ folder as the recipe describes. Comments in the files that say “see README” refer to the recipe page; the zip includes it as README.md.

sql/

sql/01_setup.sql

-- 01_setup.sql
-- Creates the recipe database, warehouse, and a read-only analyst role that can
-- read the AI usage views in SNOWFLAKE.ACCOUNT_USAGE. Run as ACCOUNTADMIN once;
-- everything after this file runs as the analyst role.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';
SET cookbook_user = '"' || CURRENT_USER() || '"';

USE ROLE ACCOUNTADMIN;

CREATE ROLE IF NOT EXISTS IDENTIFIER($cookbook_role)
  COMMENT = 'Cookbook: reads AI usage views and owns the attribution views';
GRANT ROLE IDENTIFIER($cookbook_role) TO USER IDENTIFIER($cookbook_user);

-- Two SNOWFLAKE database roles instead of IMPORTED PRIVILEGES on the whole database:
--   USAGE_VIEWER    -> the Cortex AI usage views and METERING_DAILY_HISTORY
--   SECURITY_VIEWER -> ACCOUNT_USAGE.USERS, used to turn USER_ID into a user name
--                      (CORTEX_AI_FUNCTIONS_USAGE_HISTORY and CORTEX_REST_API_USAGE_HISTORY
--                      carry USER_ID only)
GRANT DATABASE ROLE SNOWFLAKE.USAGE_VIEWER    TO ROLE IDENTIFIER($cookbook_role);
GRANT DATABASE ROLE SNOWFLAKE.SECURITY_VIEWER TO ROLE IDENTIFIER($cookbook_role);

CREATE WAREHOUSE IF NOT EXISTS IDENTIFIER($cookbook_wh)
  WAREHOUSE_SIZE = 'XSMALL' AUTO_SUSPEND = 60 AUTO_RESUME = TRUE INITIALLY_SUSPENDED = TRUE
  COMMENT = 'Cookbook: ai-usage-cost-attribution';
GRANT USAGE ON WAREHOUSE IDENTIFIER($cookbook_wh) TO ROLE IDENTIFIER($cookbook_role);

CREATE DATABASE IF NOT EXISTS IDENTIFIER($cookbook_db)
  COMMENT = 'Cookbook: ai-usage-cost-attribution';
-- The analyst role owns the database so the views it creates run with its own
-- (read-only) privileges, not with ACCOUNTADMIN's.
GRANT OWNERSHIP ON DATABASE IDENTIFIER($cookbook_db) TO ROLE IDENTIFIER($cookbook_role) COPY CURRENT GRANTS;

USE ROLE IDENTIFIER($cookbook_role);
USE DATABASE IDENTIFIER($cookbook_db);
CREATE SCHEMA IF NOT EXISTS ATTRIBUTION COMMENT = 'Unified AI usage and cost attribution views';
CREATE SCHEMA IF NOT EXISTS GOVERNANCE  COMMENT = 'Tags, budgets, and quotas for this recipe';

-- Cost-center tag used for attribution (USER_TAGS / AGENT_TAGS) and budget/quota scoping.
CREATE TAG IF NOT EXISTS GOVERNANCE.COST_CENTER
  ALLOWED_VALUES 'cookbook', 'finance', 'support'
  COMMENT = 'Cookbook: cost center for AI usage attribution';

-- Smoke test: the role can read every view the recipe uses.
USE WAREHOUSE IDENTIFIER($cookbook_wh);
SELECT 'CORTEX_AGENT_USAGE_HISTORY'        AS view_name, COUNT(*) AS rows_last_30d FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AGENT_USAGE_HISTORY        WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'SNOWFLAKE_COWORK_USAGE_HISTORY',    COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COWORK_USAGE_HISTORY    WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'SNOWFLAKE_COCO_USAGE_HISTORY',      COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COCO_USAGE_HISTORY      WHERE USAGE_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'CORTEX_AI_FUNCTIONS_USAGE_HISTORY', COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'CORTEX_ANALYST_USAGE_HISTORY',      COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_ANALYST_USAGE_HISTORY      WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'CORTEX_REST_API_USAGE_HISTORY',     COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY     WHERE START_TIME >= DATEADD(day, -30, CURRENT_TIMESTAMP())
UNION ALL SELECT 'USERS',                             COUNT(*) FROM SNOWFLAKE.ACCOUNT_USAGE.USERS;

sql/02_views.sql

-- 02_views.sql
-- Attribution views over SNOWFLAKE.ACCOUNT_USAGE. Run as the analyst role created in 01.
-- Rules the views follow:
--   * One source view per billing service type, so totals never double count
--     (CORTEX_AGENTS, SNOWFLAKE_COWORK, SNOWFLAKE_COCO*, AI_FUNCTIONS, AI_SERVICES for Analyst).
--   * SNOWFLAKE_COCO_USAGE_HISTORY already covers the CLI, Desktop, and Snowsight
--     interfaces, so the per-interface CORTEX_CODE_*_USAGE_HISTORY views are not added.
--   * Warehouse credits reported by agents (METADATA:sql_query_credits) are kept in
--     their own column. They are billed as warehouse compute, not as AI credits.
--   * CORTEX_REST_API_USAGE_HISTORY reports tokens only (billed in currency), so it gets
--     its own view and is never added to credit totals.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';

USE ROLE IDENTIFIER($cookbook_role);
USE WAREHOUSE IDENTIFIER($cookbook_wh);
USE DATABASE IDENTIFIER($cookbook_db);
USE SCHEMA ATTRIBUTION;

-- ---------------------------------------------------------------------------
-- 1. One row per Cortex Agent or CoWork request.
--    The two source views are mutually exclusive: CoWork requests are only in
--    SNOWFLAKE_COWORK_USAGE_HISTORY, direct agent requests only in
--    CORTEX_AGENT_USAGE_HISTORY.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_AGENT_REQUESTS
  COMMENT = 'Cookbook: one row per Cortex Agent or CoWork request'
AS
SELECT
    'CORTEX_AGENT'                                        AS surface,
    COALESCE(METADATA:interaction_interface::STRING, 'unknown') AS interface,
    START_TIME,
    END_TIME,
    USER_ID,
    USER_NAME,
    METADATA:role_name::STRING                            AS role_name,
    REQUEST_ID,
    PARENT_REQUEST_ID,
    COALESCE(PARENT_REQUEST_ID, REQUEST_ID)               AS root_request_id,
    AGENT_DATABASE_NAME || '.' || AGENT_SCHEMA_NAME || '.' || AGENT_NAME AS agent_fqn,
    NULL::STRING                                          AS cowork_name,
    TOKENS,
    TOKEN_CREDITS,
    METADATA:sql_query_credits::NUMBER(38, 9)             AS sql_query_credits,
    METADATA:sql_query_warehouses                         AS sql_query_warehouses,
    METADATA:ai_functions_credits::NUMBER(38, 9)          AS ai_functions_credits,
    METADATA:inference_region::STRING                     AS inference_region,
    USER_TAGS,
    AGENT_TAGS,
    TOKENS_GRANULAR,
    CREDITS_GRANULAR
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AGENT_USAGE_HISTORY
UNION ALL
SELECT
    'SNOWFLAKE_COWORK',
    'cowork',
    START_TIME,
    END_TIME,
    USER_ID,
    USER_NAME,
    METADATA:role_name::STRING,
    REQUEST_ID,
    PARENT_REQUEST_ID,
    COALESCE(PARENT_REQUEST_ID, REQUEST_ID),
    AGENT_DATABASE_NAME || '.' || AGENT_SCHEMA_NAME || '.' || AGENT_NAME,
    SNOWFLAKE_COWORK_NAME,
    TOKENS,
    TOKEN_CREDITS,
    METADATA:sql_query_credits::NUMBER(38, 9),
    METADATA:sql_query_warehouses,
    METADATA:ai_functions_credits::NUMBER(38, 9),
    METADATA:inference_region::STRING,
    USER_TAGS,
    AGENT_TAGS,
    TOKENS_GRANULAR,
    CREDITS_GRANULAR
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COWORK_USAGE_HISTORY;

-- ---------------------------------------------------------------------------
-- 2. Token and credit breakdown per sub-request, service type, and model.
--    TOKENS_GRANULAR / CREDITS_GRANULAR are arrays of
--      { "<sub_request_id>": { "<service_type>": { "<model>": {input, cache_read_input,
--        cache_write_input, output} }, "start_time": "..." } }
--    so the flatten goes: array element -> sub-request -> service type -> model.
--    TOKENS on the parent row equals input + cache_read_input + cache_write_input + output.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_AGENT_TOKENS_BY_MODEL
  COMMENT = 'Cookbook: agent and CoWork tokens and credits per sub-request, service type, and model'
AS
WITH tok AS (
    SELECT
        r.surface, r.interface, r.request_id, r.start_time, r.user_name, r.agent_fqn, r.cowork_name,
        sub.key                                 AS sub_request_id,
        sub.value:start_time::TIMESTAMP_LTZ     AS sub_request_start_time,
        svc.key                                 AS service_type,
        mdl.key                                 AS model_name,
        COALESCE(mdl.value:input::NUMBER, 0)             AS input_tokens,
        COALESCE(mdl.value:cache_read_input::NUMBER, 0)  AS cache_read_input_tokens,
        COALESCE(mdl.value:cache_write_input::NUMBER, 0) AS cache_write_input_tokens,
        COALESCE(mdl.value:output::NUMBER, 0)            AS output_tokens
    FROM V_AGENT_REQUESTS r,
         LATERAL FLATTEN(input => r.tokens_granular) el,
         LATERAL FLATTEN(input => el.value)          sub,
         LATERAL FLATTEN(input => sub.value)         svc,
         LATERAL FLATTEN(input => svc.value)         mdl
    WHERE IS_OBJECT(svc.value)          -- skips the sibling "start_time" key
),
cred AS (
    SELECT
        r.surface, r.request_id,
        sub.key AS sub_request_id,
        svc.key AS service_type,
        mdl.key AS model_name,
        COALESCE(mdl.value:input::FLOAT, 0)
          + COALESCE(mdl.value:cache_read_input::FLOAT, 0)
          + COALESCE(mdl.value:cache_write_input::FLOAT, 0)
          + COALESCE(mdl.value:output::FLOAT, 0)         AS credits,
        COALESCE(mdl.value:cache_read_input::FLOAT, 0)   AS cache_read_input_credits
    FROM V_AGENT_REQUESTS r,
         LATERAL FLATTEN(input => r.credits_granular) el,
         LATERAL FLATTEN(input => el.value)           sub,
         LATERAL FLATTEN(input => sub.value)          svc,
         LATERAL FLATTEN(input => svc.value)          mdl
    WHERE IS_OBJECT(svc.value)
)
SELECT
    t.*,
    t.input_tokens + t.cache_read_input_tokens + t.cache_write_input_tokens AS total_input_tokens,
    c.credits,
    c.cache_read_input_credits
FROM tok t
LEFT JOIN cred c
  ON  c.surface        = t.surface
  AND c.request_id     = t.request_id
  AND c.sub_request_id = t.sub_request_id
  AND c.service_type   = t.service_type
  AND c.model_name     = t.model_name;

-- ---------------------------------------------------------------------------
-- 3. Cortex Code token breakdown. Here TOKENS_GRANULAR is an OBJECT keyed by model name.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_COCO_TOKENS_BY_MODEL
  COMMENT = 'Cookbook: Cortex Code tokens and credits per request and model'
AS
SELECT
    u.USAGE_TIME::TIMESTAMP_LTZ                          AS usage_time,
    u.INTERFACE                                          AS interface,
    u.USER_ID,
    u.USER_NAME,
    u.REQUEST_ID,
    m.key                                                AS model_name,
    COALESCE(m.value:input::NUMBER, 0)                   AS input_tokens,
    COALESCE(m.value:cache_read_input::NUMBER, 0)        AS cache_read_input_tokens,
    COALESCE(m.value:cache_write_input::NUMBER, 0)       AS cache_write_input_tokens,
    COALESCE(m.value:output::NUMBER, 0)                  AS output_tokens,
    COALESCE(m.value:input::NUMBER, 0)
      + COALESCE(m.value:cache_read_input::NUMBER, 0)
      + COALESCE(m.value:cache_write_input::NUMBER, 0)   AS total_input_tokens,
    COALESCE(u.CREDITS_GRANULAR[m.key]:input::FLOAT, 0)
      + COALESCE(u.CREDITS_GRANULAR[m.key]:cache_read_input::FLOAT, 0)
      + COALESCE(u.CREDITS_GRANULAR[m.key]:cache_write_input::FLOAT, 0)
      + COALESCE(u.CREDITS_GRANULAR[m.key]:output::FLOAT, 0) AS credits
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COCO_USAGE_HISTORY u,
     LATERAL FLATTEN(input => u.TOKENS_GRANULAR) m;

-- ---------------------------------------------------------------------------
-- 4. Unified AI credit usage across surfaces, one source per service type.
--    CORTEX_AI_FUNCTIONS_USAGE_HISTORY has USER_ID only, so it joins USERS for the name.
--    CORTEX_ANALYST_USAGE_HISTORY is an hourly roll-up by USERNAME (no request id).
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_AI_USAGE_UNIFIED
  COMMENT = 'Cookbook: AI credit usage across agents, CoWork, Cortex Code, AI functions, and Analyst'
AS
SELECT
    surface,
    interface,
    start_time                    AS usage_time,
    user_id,
    user_name,
    COALESCE(cowork_name, agent_fqn) AS object_name,
    NULL::STRING                  AS model_name,
    request_id,
    1                             AS request_count,
    tokens,
    token_credits                 AS ai_credits,
    sql_query_credits,
    'CORTEX_AGENT_USAGE_HISTORY / SNOWFLAKE_COWORK_USAGE_HISTORY' AS source_view
FROM V_AGENT_REQUESTS
UNION ALL
SELECT
    'SNOWFLAKE_COCO',
    INTERFACE,
    USAGE_TIME::TIMESTAMP_LTZ,
    USER_ID,
    USER_NAME,
    NULL,
    NULL,
    REQUEST_ID,
    1,
    TOKENS,
    TOKEN_CREDITS,
    NULL,
    'SNOWFLAKE_COCO_USAGE_HISTORY'
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COCO_USAGE_HISTORY
UNION ALL
SELECT
    'AI_FUNCTIONS',
    'sql',
    f.START_TIME,
    f.USER_ID,
    u.NAME,
    f.FUNCTION_NAME,
    NULLIF(f.MODEL_NAME, ''),
    f.QUERY_ID,
    1,
    NULL,
    f.CREDITS,
    NULL,
    'CORTEX_AI_FUNCTIONS_USAGE_HISTORY'
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY f
LEFT JOIN SNOWFLAKE.ACCOUNT_USAGE.USERS u
  ON u.USER_ID = f.USER_ID
UNION ALL
SELECT
    'CORTEX_ANALYST',
    'hourly_rollup',
    START_TIME,
    NULL,
    USERNAME,
    NULL,
    NULL,
    NULL,
    REQUEST_COUNT,
    NULL,
    CREDITS,
    NULL,
    'CORTEX_ANALYST_USAGE_HISTORY'
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_ANALYST_USAGE_HISTORY;

-- ---------------------------------------------------------------------------
-- 5. REST API inference: tokens only (billed in currency, not credits). USER_ID only.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_REST_API_TOKENS
  COMMENT = 'Cookbook: Cortex REST API tokens per request (currency-billed; no credits)'
AS
SELECT
    r.START_TIME::TIMESTAMP_LTZ                             AS usage_time,
    r.USER_ID,
    u.NAME                                                  AS user_name,
    r.REQUEST_ID,
    r.MODEL_NAME,
    r.INFERENCE_REGION,
    r.QUERY_TAG,
    r.TOKENS,
    COALESCE(r.TOKENS_GRANULAR:input::NUMBER, 0)            AS input_tokens,
    COALESCE(r.TOKENS_GRANULAR:cache_read_input::NUMBER, 0) AS cache_read_input_tokens,
    COALESCE(r.TOKENS_GRANULAR:cache_write_input::NUMBER, 0) AS cache_write_input_tokens,
    COALESCE(r.TOKENS_GRANULAR:output::NUMBER, 0)           AS output_tokens
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY r
LEFT JOIN SNOWFLAKE.ACCOUNT_USAGE.USERS u
  ON u.USER_ID = r.USER_ID;

-- ---------------------------------------------------------------------------
-- 6. Reconciliation against billing. Compares each surface's daily credits (UTC day)
--    with METERING_DAILY_HISTORY for the matching service type. Analyst is excluded
--    because AI_SERVICES also contains Search, fine-tuning, and other services.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW V_RECONCILE_METERING
  COMMENT = 'Cookbook: unified view credits vs METERING_DAILY_HISTORY by UTC day'
AS
WITH usage_daily AS (
    SELECT surface, CONVERT_TIMEZONE('UTC', usage_time)::DATE AS usage_date, SUM(ai_credits) AS view_credits
    FROM V_AI_USAGE_UNIFIED
    WHERE surface <> 'CORTEX_ANALYST'
    GROUP BY 1, 2
),
metering_daily AS (
    SELECT
        CASE
            WHEN SERVICE_TYPE = 'CORTEX_AGENTS'                              THEN 'CORTEX_AGENT'
            WHEN SERVICE_TYPE IN ('SNOWFLAKE_COWORK', 'SNOWFLAKE_INTELLIGENCE') THEN 'SNOWFLAKE_COWORK'
            WHEN SERVICE_TYPE LIKE 'SNOWFLAKE_COCO%' OR SERVICE_TYPE LIKE 'CORTEX_CODE%' THEN 'SNOWFLAKE_COCO'
            WHEN SERVICE_TYPE = 'AI_FUNCTIONS'                               THEN 'AI_FUNCTIONS'
        END AS surface,
        USAGE_DATE AS usage_date,
        SUM(CREDITS_USED) AS metered_credits
    FROM SNOWFLAKE.ACCOUNT_USAGE.METERING_DAILY_HISTORY
    GROUP BY 1, 2
    HAVING surface IS NOT NULL
)
SELECT
    COALESCE(u.surface, m.surface)       AS surface,
    COALESCE(u.usage_date, m.usage_date) AS usage_date,
    ROUND(COALESCE(u.view_credits, 0), 4)    AS view_credits,
    ROUND(COALESCE(m.metered_credits, 0), 4) AS metered_credits,
    ROUND(COALESCE(m.metered_credits, 0) - COALESCE(u.view_credits, 0), 4) AS delta
FROM usage_daily u
FULL OUTER JOIN metering_daily m
  ON m.surface = u.surface AND m.usage_date = u.usage_date;

sql/03_attribution_queries.sql

-- 03_attribution_queries.sql
-- Read-only attribution queries over the views from 02. Change the window in one place.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';
SET lookback_days = 30;

USE ROLE IDENTIFIER($cookbook_role);
USE WAREHOUSE IDENTIFIER($cookbook_wh);
USE DATABASE IDENTIFIER($cookbook_db);
USE SCHEMA ATTRIBUTION;

-- Q1. Surface mix: where AI credits come from. Agent rows split by interaction_interface.
--     sql_query_credits is warehouse compute run by the agent; it is billed as warehouse
--     metering, so it is shown next to AI credits, never added into them.
SELECT
    surface,
    interface,
    SUM(request_count)                  AS requests,
    COUNT(DISTINCT user_name)           AS users,
    ROUND(SUM(ai_credits), 4)           AS ai_credits,
    ROUND(SUM(sql_query_credits), 4)    AS agent_warehouse_credits,
    ROUND(100 * RATIO_TO_REPORT(SUM(ai_credits)) OVER (), 1) AS pct_of_ai_credits
FROM V_AI_USAGE_UNIFIED
WHERE usage_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY ai_credits DESC;

-- Q2. Top users across every surface.
SELECT
    user_name,
    ROUND(SUM(ai_credits), 4)                                               AS ai_credits,
    ROUND(SUM(IFF(surface = 'CORTEX_AGENT',     ai_credits, 0)), 4)         AS agent_credits,
    ROUND(SUM(IFF(surface = 'SNOWFLAKE_COWORK', ai_credits, 0)), 4)         AS cowork_credits,
    ROUND(SUM(IFF(surface = 'SNOWFLAKE_COCO',   ai_credits, 0)), 4)         AS coco_credits,
    ROUND(SUM(IFF(surface = 'AI_FUNCTIONS',     ai_credits, 0)), 4)         AS ai_function_credits,
    ROUND(SUM(IFF(surface = 'CORTEX_ANALYST',   ai_credits, 0)), 4)         AS analyst_credits
FROM V_AI_USAGE_UNIFIED
WHERE usage_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1
ORDER BY ai_credits DESC
LIMIT 20;

-- Q3. Per agent: token credits, warehouse credits from its SQL, and cost per request.
SELECT
    agent_fqn,
    surface,
    COUNT(*)                                        AS requests,
    ROUND(SUM(token_credits), 4)                    AS token_credits,
    ROUND(SUM(sql_query_credits), 6)                AS warehouse_credits,
    COUNT_IF(sql_query_credits IS NULL)             AS requests_without_sql_attribution,
    ROUND(AVG(token_credits), 6)                    AS avg_token_credits_per_request,
    ROUND(AVG(tokens), 0)                           AS avg_tokens_per_request
FROM V_AGENT_REQUESTS
WHERE start_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY token_credits DESC
LIMIT 20;

-- Q4. Model mix and prompt-cache ratio for agents and CoWork.
--     Cache ratio = cache_read_input / (input + cache_read_input + cache_write_input):
--     the share of all input tokens served from the prompt cache. Dividing by the
--     "input" field alone is wrong, because "input" counts only the uncached tokens.
SELECT
    surface,
    service_type,
    model_name,
    COUNT(DISTINCT request_id)                                AS requests,
    SUM(total_input_tokens)                                   AS total_input_tokens,
    SUM(cache_read_input_tokens)                              AS cache_read_input_tokens,
    SUM(output_tokens)                                        AS output_tokens,
    ROUND(SUM(cache_read_input_tokens) / NULLIF(SUM(total_input_tokens), 0), 3) AS cache_read_ratio,
    ROUND(SUM(credits), 4)                                    AS credits,
    ROUND(SUM(cache_read_input_credits) / NULLIF(SUM(credits), 0), 3)           AS cache_read_share_of_credits
FROM V_AGENT_TOKENS_BY_MODEL
WHERE start_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1, 2, 3
ORDER BY credits DESC;

-- Q5. Same breakdown for Cortex Code, by interface (cli, desktop, snowsight).
SELECT
    interface,
    model_name,
    COUNT(DISTINCT request_id)                                AS requests,
    SUM(total_input_tokens)                                   AS total_input_tokens,
    ROUND(SUM(cache_read_input_tokens) / NULLIF(SUM(total_input_tokens), 0), 3) AS cache_read_ratio,
    ROUND(SUM(credits), 4)                                    AS credits
FROM V_COCO_TOKENS_BY_MODEL
WHERE usage_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY credits DESC;

-- Q6. Cost center attribution from tags. USER_TAGS and AGENT_TAGS are arrays of
--     {level, tag_database, tag_schema, tag_name, tag_value}. A user or agent can inherit
--     the same tag from several levels, so take one value per request (the most specific
--     level wins) before summing, or the request is counted once per level.
WITH tagged AS (
    SELECT
        r.request_id,
        r.token_credits,
        t.value:tag_value::STRING AS cost_center,
        ROW_NUMBER() OVER (
            PARTITION BY r.request_id
            ORDER BY CASE t.value:level::STRING WHEN 'CORTEX_AGENT' THEN 1 WHEN 'SCHEMA' THEN 2
                                                WHEN 'DATABASE' THEN 3 ELSE 4 END
        ) AS rn
    FROM V_AGENT_REQUESTS r,
         LATERAL FLATTEN(input => r.agent_tags) t
    WHERE r.start_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
      AND UPPER(t.value:tag_name::STRING) = 'COST_CENTER'
)
SELECT cost_center, COUNT(*) AS requests, ROUND(SUM(token_credits), 4) AS token_credits
FROM tagged
WHERE rn = 1
GROUP BY 1
ORDER BY token_credits DESC;

-- Q7. REST API inference: tokens by model and user. Billed in currency, so no credits here.
SELECT
    model_name,
    user_name,
    COUNT(*)                     AS requests,
    SUM(tokens)                  AS tokens,
    SUM(input_tokens)            AS input_tokens,
    SUM(output_tokens)           AS output_tokens
FROM V_REST_API_TOKENS
WHERE usage_time >= DATEADD(day, -$lookback_days, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY tokens DESC
LIMIT 20;

-- Q8. Reconciliation: per surface, view totals against billed credits (complete UTC days only).
SELECT
    surface,
    ROUND(SUM(view_credits), 4)     AS view_credits,
    ROUND(SUM(metered_credits), 4)  AS metered_credits,
    ROUND(SUM(delta), 4)            AS delta
FROM V_RECONCILE_METERING
WHERE usage_date >= DATEADD(day, -$lookback_days, CONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP())::DATE)
  AND usage_date <  DATEADD(day, -1, CONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP())::DATE)
GROUP BY 1
ORDER BY 1;

sql/04_budget_and_quota.sql

-- 04_budget_and_quota.sql
-- Spend controls. This file changes account-level cost governance, so run it
-- deliberately and read each step. Budgets and quotas behave differently:
--
--   Budget (SNOWFLAKE.CORE.BUDGET)  Monitors a monthly credit limit for tagged objects
--                                   and runs your stored procedure at thresholds.
--                                   Evaluation is periodic: actions can take up to
--                                   8 hours (2 hours with the latency-optimized option).
--                                   It does not stop spend by itself.
--   Quota  (SNOWFLAKE.CORE.QUOTA)   Per-user limits (monthly, weekly, daily) with built-in
--                                   blocking for AI domains, applied within minutes.
--                                   Some overshoot is expected; limits are never pooled.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';
SET cost_tag      = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.COST_CENTER';
SET cost_center   = 'cookbook';
SET agent_budget_credits  = 50;   -- monthly, all agents tagged COST_CENTER = cookbook

-- Step 1 (ACCOUNTADMIN): let the recipe role create budgets and quotas.
USE ROLE ACCOUNTADMIN;
GRANT DATABASE ROLE SNOWFLAKE.BUDGET_CREATOR TO ROLE IDENTIFIER($cookbook_role);
GRANT DATABASE ROLE SNOWFLAKE.QUOTA_CREATOR  TO ROLE IDENTIFIER($cookbook_role);

USE ROLE IDENTIFIER($cookbook_role);
USE WAREHOUSE IDENTIFIER($cookbook_wh);
USE DATABASE IDENTIFIER($cookbook_db);
USE SCHEMA GOVERNANCE;

-- Step 2: a log table and a budget custom action. The action runs when the budget
-- crosses a threshold. This one only records the event; replace the INSERT with your
-- enforcement (for example, revoking USAGE on the tagged agents from an end-user role).
CREATE TABLE IF NOT EXISTS BUDGET_ACTION_LOG (
    fired_at      TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP(),
    cost_center   STRING,
    threshold_pct NUMBER
);

-- Owner's rights: the SNOWFLAKE application calls this procedure, and it must be able
-- to write the log without granting the application anything on the table itself.
CREATE OR REPLACE PROCEDURE ON_AGENT_BUDGET_THRESHOLD(cost_center STRING, threshold_pct NUMBER)
RETURNS STRING
LANGUAGE SQL
EXECUTE AS OWNER
AS
$$
BEGIN
    INSERT INTO BUDGET_ACTION_LOG (cost_center, threshold_pct) VALUES (:cost_center, :threshold_pct);
    RETURN 'logged ' || :cost_center || ' at ' || :threshold_pct || '%';
END;
$$;

GRANT USAGE ON DATABASE IDENTIFIER($cookbook_db) TO APPLICATION SNOWFLAKE;
GRANT USAGE ON SCHEMA GOVERNANCE TO APPLICATION SNOWFLAKE;
GRANT USAGE ON PROCEDURE ON_AGENT_BUDGET_THRESHOLD(STRING, NUMBER) TO APPLICATION SNOWFLAKE;

-- Step 3: resource budget for Cortex Agents tagged COST_CENTER = cookbook.
-- Tag an agent to bring it under the budget, for example:
--   ALTER AGENT <db>.<schema>.<agent> SET TAG COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.COST_CENTER = 'cookbook';
CREATE SNOWFLAKE.CORE.BUDGET IF NOT EXISTS AGENT_BUDGET();
CALL AGENT_BUDGET!SET_SPENDING_LIMIT($agent_budget_credits);
CALL AGENT_BUDGET!SET_RESOURCE_TAGS(
    [[(SELECT SYSTEM$REFERENCE('TAG', $cost_tag, 'SESSION', 'APPLYBUDGET')), $cost_center]],
    'UNION');
CALL AGENT_BUDGET!ADD_CUSTOM_ACTION(
    SYSTEM$REFERENCE('PROCEDURE', $cookbook_db || '.GOVERNANCE.ON_AGENT_BUDGET_THRESHOLD(STRING, NUMBER)'),
    ARRAY_CONSTRUCT($cost_center, 100),
    'ACTUAL',
    100);
CALL AGENT_BUDGET!GET_CUSTOM_ACTIONS();

-- Step 4: per-user quota for agents and AI functions, scoped to tagged users only.
-- CoWork and Cortex Code spend is not counted or blocked by this quota. To cover them, also
-- add the SNOWFLAKE INTELLIGENCE and CORTEX CODE domains (see the README).
-- A new quota covers ALL users until you narrow the scope, and nothing is enforced until
-- a limit is set. So: create, scope by tag, confirm the scope, and only then set the limit
-- and turn on blocking.
CREATE SNOWFLAKE.CORE.QUOTA IF NOT EXISTS AI_USER_QUOTA();
CALL AI_USER_QUOTA!ADD_SHARED_RESOURCE('CORTEX AGENT');
CALL AI_USER_QUOTA!ADD_SHARED_RESOURCE('AI FUNCTION');
CALL AI_USER_QUOTA!SET_USER_TAGS(
    [[(SELECT SYSTEM$REFERENCE('TAG', $cost_tag, 'SESSION', 'APPLYBUDGET')), $cost_center]],
    'UNION');
CALL AI_USER_QUOTA!GET_QUOTA_SCOPE();
-- Optional: put yourself in scope to see a block happen.
--   ALTER USER <your_user> SET TAG COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.COST_CENTER = 'cookbook';
CALL AI_USER_QUOTA!GET_USERS();   -- tag resolution can lag up to about 2 hours

-- Step 5 is in 05_quota_limit.sql. Run it only after GET_USERS() above returns the users
-- you expect, and at least 10 minutes after this file: scope changes take about 5 to 10
-- minutes to propagate, and until then the quota can still cover every user.

sql/05_quota_limit.sql

-- 05_quota_limit.sql
-- Step 5 of the spend controls: set the per-user limit on the quota from 04 and turn on
-- built-in blocking. Run this only after AI_USER_QUOTA!GET_USERS() in 04 returned the
-- users you expect, and at least 10 minutes after 04, because scope changes take about
-- 5 to 10 minutes to propagate. Limit changes propagate on the same delay.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';
SET per_user_daily_credits = 5;   -- daily, per tagged user

USE ROLE IDENTIFIER($cookbook_role);
USE WAREHOUSE IDENTIFIER($cookbook_wh);
USE DATABASE IDENTIFIER($cookbook_db);
USE SCHEMA GOVERNANCE;

-- Check the scope once more before enforcing.
CALL AI_USER_QUOTA!GET_QUOTA_SCOPE();
CALL AI_USER_QUOTA!GET_USERS();

CALL AI_USER_QUOTA!SET_PER_USER_LIMIT($per_user_daily_credits, 'DAILY');
-- Second argument: email blocked users. FALSE keeps the recipe quiet; set TRUE in production.
CALL AI_USER_QUOTA!SET_BLOCK_ENFORCEMENT_ENABLED(TRUE, FALSE);
CALL AI_USER_QUOTA!GET_CONFIG();
CALL AI_USER_QUOTA!GET_ACTIVE_BLOCKS_V2();

sql/99_teardown.sql

-- 99_teardown.sql
-- Removes everything the recipe created. Budgets, quotas, the custom-action procedure,
-- and the tag live inside the recipe database, so dropping the database removes them;
-- the explicit drops below make the order visible and are safe to rerun.

SET cookbook_db   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION';
SET cookbook_wh   = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_WH';
SET cookbook_role = 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION_ANALYST';

USE ROLE ACCOUNTADMIN;

-- If you tagged yourself or an agent with the recipe's COST_CENTER tag in 04, remove it
-- first (dropping the database would otherwise leave the assignment orphaned):
--   ALTER USER <your_user> UNSET TAG COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.COST_CENTER;
--   ALTER AGENT <db>.<schema>.<agent> UNSET TAG COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.COST_CENTER;

-- Drop the role first: Snowflake transfers ownership of the role's objects (the
-- database) to the role running DROP ROLE, so ACCOUNTADMIN can then drop them.
DROP ROLE      IF EXISTS IDENTIFIER($cookbook_role);
-- Spend controls from 04 (no-ops if 04 was never run).
DROP SNOWFLAKE.CORE.QUOTA  IF EXISTS COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.AI_USER_QUOTA;
DROP SNOWFLAKE.CORE.BUDGET IF EXISTS COOKBOOK_AI_USAGE_COST_ATTRIBUTION.GOVERNANCE.AGENT_BUDGET;
DROP DATABASE  IF EXISTS IDENTIFIER($cookbook_db);
DROP WAREHOUSE IF EXISTS IDENTIFIER($cookbook_wh);

SHOW DATABASES  LIKE 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION%';
SHOW WAREHOUSES LIKE 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION%';
SHOW ROLES      LIKE 'COOKBOOK_AI_USAGE_COST_ATTRIBUTION%';