You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by el...@apache.org on 2026/06/30 16:48:05 UTC
(devlake) 01/01: feat(gh-copilot): ingest AI credit usage and add credit consumption dashboard panels
This is an automated email from the ASF dual-hosted git repository.
ewega pushed a commit to branch ewega-copilot-ai-credit-usage
in repository https://gitbox.apache.org/repos/asf/devlake.git
commit b072652bf34f4ed3dd1c551180d64ae2b7b2ba83
Author: ewega <ew...@github.com>
AuthorDate: Tue Jun 30 19:47:47 2026 +0300
feat(gh-copilot): ingest AI credit usage and add credit consumption dashboard panels
Add first-class GitHub Copilot AI credit billing ingestion to the gh-copilot
plugin and surface credit-consumption KPIs on the Copilot Adoption dashboard.
- New tool model _tool_copilot_ai_credit_usage (+ migration, GetTablesInfo)
- Enterprise + organization daily collectors and extractors for the Billing
Usage AI credit endpoints (API version 2026-03-10), reusing the existing
stateful daily-iterator pattern
- Wire 4 new subtask metas (collect/extract x enterprise/org)
- Add "AI Credit Consumption" row (stats, time series, by-model and
by-product/SKU tables) to mysql + postgresql adoption dashboards
- e2e fixtures + extractor unit tests
- Docs: plugin README + dashboard README
Resolves #8961
Co-authored-by: Copilot <22...@users.noreply.github.com>
---
backend/plugins/gh-copilot/README.md | 14 +
.../_raw_copilot_ai_credit_usage_org.csv | 2 +
.../_tool_copilot_ai_credit_usage.csv | 3 +
backend/plugins/gh-copilot/e2e/metrics_test.go | 8 +
.../plugins/gh-copilot/models/ai_credit_usage.go | 76 ++++
.../20260701_add_copilot_ai_credit_usage.go | 78 ++++
.../gh-copilot/models/migrationscripts/register.go | 1 +
backend/plugins/gh-copilot/models/models.go | 2 +
.../gh-copilot/tasks/ai_credit_usage_collector.go | 172 +++++++++
.../gh-copilot/tasks/ai_credit_usage_extractor.go | 162 ++++++++
.../tasks/ai_credit_usage_extractor_test.go | 128 +++++++
backend/plugins/gh-copilot/tasks/register.go | 4 +
backend/plugins/gh-copilot/tasks/subtasks.go | 34 ++
grafana/dashboards/mysql/GithubCopilotREADME.md | 2 +
.../dashboards/mysql/github-copilot-adoption.json | 2 +-
.../postgresql/github-copilot-adoption.json | 423 +++++++++++++++++++++
16 files changed, 1110 insertions(+), 1 deletion(-)
diff --git a/backend/plugins/gh-copilot/README.md b/backend/plugins/gh-copilot/README.md
index 1c4aeda24..5d36e6aae 100644
--- a/backend/plugins/gh-copilot/README.md
+++ b/backend/plugins/gh-copilot/README.md
@@ -28,11 +28,22 @@ It follows the same structure/patterns as other DevLake data-source plugins (not
- `GET /orgs/{org}/copilot/billing/seats`
- `GET /orgs/{org}/copilot/metrics`
+**AI credit usage endpoints** (GitHub Billing Usage REST API, version `2026-03-10`):
+
+- `GET /enterprises/{enterprise}/settings/billing/ai_credit/usage`
+- `GET /organizations/{org}/settings/billing/ai_credit/usage` (note: `organizations/`, not `orgs/`)
+
+These are collected **daily** (one request per day, iterating `year`/`month`/`day`) and report Copilot
+**AI credit consumption** broken down by product, SKU and model. Quantities and amounts are floats and the
+`unit_type` is `ai-credits`.
+
**Stored data (tool layer)**:
- `_tool_copilot_org_metrics` (daily aggregates)
- `_tool_copilot_language_metrics` (editor/language breakdown)
- `_tool_copilot_seats` (seat assignments)
+- `_tool_copilot_ai_credit_usage` (daily AI credit consumption: gross/net quantity and amount per
+ product/sku/model, at `enterprise` or `organization` level)
## Data flow (high level)
@@ -61,6 +72,9 @@ flowchart LR
- GitHub Copilot Business or Enterprise enabled for the target organization
- A token that can access GitHub Copilot billing/metrics (classic PAT with `manage_billing:copilot` works)
+- For **AI credit usage**, the token additionally needs billing read access:
+ `manage_billing:copilot` (or `admin:enterprise` for the enterprise endpoint / `admin:org` for the
+ organization endpoint)
### 1) Create a connection
diff --git a/backend/plugins/gh-copilot/e2e/metrics/raw_tables/_raw_copilot_ai_credit_usage_org.csv b/backend/plugins/gh-copilot/e2e/metrics/raw_tables/_raw_copilot_ai_credit_usage_org.csv
new file mode 100644
index 000000000..6d59ff43b
--- /dev/null
+++ b/backend/plugins/gh-copilot/e2e/metrics/raw_tables/_raw_copilot_ai_credit_usage_org.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"": 1, ""ScopeId"": ""octodemo"", ""Organization"": ""octodemo"", ""Endpoint"": ""https://api.github.com""}","{""timePeriod"": {""year"": 2026, ""month"": 6, ""day"": 8}, ""organization"": ""octodemo"", ""usageItems"": [{""product"": ""Copilot"", ""sku"": ""Copilot AI Credits"", ""model"": ""Claude Opus 4.8"", ""unitType"": ""ai-credits"", ""pricePerUnit"": 0.01, ""grossQuantity"": 5445.77595, ""grossAmount"": 54.4577595, ""discountQuantity"": 5445.77595, ""discountAmou [...]
diff --git a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_ai_credit_usage.csv b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_ai_credit_usage.csv
new file mode 100644
index 000000000..3fbe908d9
--- /dev/null
+++ b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_ai_credit_usage.csv
@@ -0,0 +1,3 @@
+connection_id,scope_id,level,year,month,day,product,sku,model,unit_type,cost_center_id,date,enterprise,organization,user_login,cost_center_name,price_per_unit,gross_quantity,gross_amount,discount_quantity,discount_amount,net_quantity,net_amount
+1,octodemo,organization,2026,6,8,Copilot,Copilot AI Credits,Claude Opus 4.8,ai-credits,,2026-06-08T00:00:00.000+00:00,,octodemo,,,0.01,5445.77595,54.4577595,5445.77595,54.4577595,0,0
+1,octodemo,organization,2026,6,8,Copilot,Copilot AI Credits,Claude Sonnet 4.6,ai-credits,,2026-06-08T00:00:00.000+00:00,,octodemo,,,0.01,176.97417,1.7697417,175.3533,1.753533,1.62087,0.0162087
diff --git a/backend/plugins/gh-copilot/e2e/metrics_test.go b/backend/plugins/gh-copilot/e2e/metrics_test.go
index 2f339f95d..03b9c265d 100644
--- a/backend/plugins/gh-copilot/e2e/metrics_test.go
+++ b/backend/plugins/gh-copilot/e2e/metrics_test.go
@@ -60,13 +60,16 @@ func TestCopilotMetricsDataFlow(t *testing.T) {
dataflowTester.ImportCsvIntoRawTable("./metrics/raw_tables/_raw_copilot_metrics.csv", "_raw_copilot_org_metrics")
dataflowTester.ImportCsvIntoRawTable("./metrics/raw_tables/_raw_copilot_seats.csv", "_raw_copilot_seats")
+ dataflowTester.ImportCsvIntoRawTable("./metrics/raw_tables/_raw_copilot_ai_credit_usage_org.csv", "_raw_copilot_ai_credit_usage_org")
dataflowTester.FlushTabler(&models.GhCopilotSeat{})
dataflowTester.FlushTabler(&models.GhCopilotEnterpriseDailyMetrics{})
dataflowTester.FlushTabler(&models.GhCopilotMetricsByLanguageFeature{})
+ dataflowTester.FlushTabler(&models.GhCopilotAiCreditUsage{})
dataflowTester.Subtask(tasks.ExtractSeatsMeta, taskData)
dataflowTester.Subtask(tasks.ExtractOrgMetricsMeta, taskData)
+ dataflowTester.Subtask(tasks.ExtractOrgAiCreditUsageMeta, taskData)
dataflowTester.VerifyTableWithOptions(&models.GhCopilotEnterpriseDailyMetrics{}, e2ehelper.TableOptions{
CSVRelPath: "./metrics/snapshot_tables/_tool_copilot_enterprise_daily_metrics.csv",
@@ -84,4 +87,9 @@ func TestCopilotMetricsDataFlow(t *testing.T) {
CSVRelPath: "./metrics/snapshot_tables/_tool_copilot_metrics_by_language_feature.csv",
IgnoreTypes: []interface{}{common.NoPKModel{}},
})
+
+ dataflowTester.VerifyTableWithOptions(&models.GhCopilotAiCreditUsage{}, e2ehelper.TableOptions{
+ CSVRelPath: "./metrics/snapshot_tables/_tool_copilot_ai_credit_usage.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
}
diff --git a/backend/plugins/gh-copilot/models/ai_credit_usage.go b/backend/plugins/gh-copilot/models/ai_credit_usage.go
new file mode 100644
index 000000000..6cb794080
--- /dev/null
+++ b/backend/plugins/gh-copilot/models/ai_credit_usage.go
@@ -0,0 +1,76 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// AI credit usage "level" identifies which billing endpoint a usage row came from.
+const (
+ AiCreditUsageLevelEnterprise = "enterprise"
+ AiCreditUsageLevelOrganization = "organization"
+)
+
+// GhCopilotAiCreditUsage stores a single AI credit billing usage line item for a
+// time period, sourced from the GitHub Billing Usage "AI credit usage" reports
+// (enterprise and organization endpoints). Each row corresponds to one usageItem
+// (a product/sku/model/unitType combination) within a single day.
+type GhCopilotAiCreditUsage struct {
+ ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
+ // Level distinguishes enterprise vs organization sourced usage.
+ Level string `gorm:"primaryKey;type:varchar(20)" json:"level"`
+ // Time period of the usage line (daily granularity).
+ Year int `gorm:"primaryKey" json:"year"`
+ Month int `gorm:"primaryKey" json:"month"`
+ Day int `gorm:"primaryKey" json:"day"`
+ // Usage line dimensions.
+ Product string `gorm:"primaryKey;type:varchar(100)" json:"product"`
+ Sku string `gorm:"primaryKey;type:varchar(150)" json:"sku"`
+ Model string `gorm:"primaryKey;type:varchar(150)" json:"model"`
+ UnitType string `gorm:"primaryKey;type:varchar(50)" json:"unitType"`
+ CostCenterId string `gorm:"primaryKey;type:varchar(100)" json:"costCenterId"`
+
+ // Date is the time period expressed as a date (year-month-day) for easier
+ // time-series querying in dashboards.
+ Date time.Time `gorm:"type:date;index" json:"date"`
+
+ // Top-level context (may be empty for the unfiltered aggregate report).
+ Enterprise string `gorm:"type:varchar(100)" json:"enterprise"`
+ Organization string `gorm:"type:varchar(100)" json:"organization"`
+ UserLogin string `gorm:"type:varchar(255)" json:"userLogin"`
+ CostCenterName string `gorm:"type:varchar(255)" json:"costCenterName"`
+
+ // Billing metrics. GitHub returns fractional values, so these are float64.
+ PricePerUnit float64 `json:"pricePerUnit" gorm:"comment:Price per unit for the line item"`
+ GrossQuantity float64 `json:"grossQuantity" gorm:"comment:Gross quantity consumed (e.g. credits)"`
+ GrossAmount float64 `json:"grossAmount" gorm:"comment:Gross billed amount"`
+ DiscountQuantity float64 `json:"discountQuantity" gorm:"comment:Discounted quantity"`
+ DiscountAmount float64 `json:"discountAmount" gorm:"comment:Discounted amount"`
+ NetQuantity float64 `json:"netQuantity" gorm:"comment:Net quantity after discounts"`
+ NetAmount float64 `json:"netAmount" gorm:"comment:Net billed amount after discounts"`
+
+ common.NoPKModel
+}
+
+func (GhCopilotAiCreditUsage) TableName() string {
+ return "_tool_copilot_ai_credit_usage"
+}
diff --git a/backend/plugins/gh-copilot/models/migrationscripts/20260701_add_copilot_ai_credit_usage.go b/backend/plugins/gh-copilot/models/migrationscripts/20260701_add_copilot_ai_credit_usage.go
new file mode 100644
index 000000000..7c8f946da
--- /dev/null
+++ b/backend/plugins/gh-copilot/models/migrationscripts/20260701_add_copilot_ai_credit_usage.go
@@ -0,0 +1,78 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type addCopilotAiCreditUsage struct{}
+
+type aiCreditUsage20260701 struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"`
+ Level string `gorm:"primaryKey;type:varchar(20)"`
+ Year int `gorm:"primaryKey"`
+ Month int `gorm:"primaryKey"`
+ Day int `gorm:"primaryKey"`
+ Product string `gorm:"primaryKey;type:varchar(100)"`
+ Sku string `gorm:"primaryKey;type:varchar(150)"`
+ Model string `gorm:"primaryKey;type:varchar(150)"`
+ UnitType string `gorm:"primaryKey;type:varchar(50)"`
+ CostCenterId string `gorm:"primaryKey;type:varchar(100)"`
+
+ Date time.Time `gorm:"type:date;index"`
+
+ Enterprise string `gorm:"type:varchar(100)"`
+ Organization string `gorm:"type:varchar(100)"`
+ UserLogin string `gorm:"type:varchar(255)"`
+ CostCenterName string `gorm:"type:varchar(255)"`
+
+ PricePerUnit float64
+ GrossQuantity float64
+ GrossAmount float64
+ DiscountQuantity float64
+ DiscountAmount float64
+ NetQuantity float64
+ NetAmount float64
+
+ archived.NoPKModel
+}
+
+func (aiCreditUsage20260701) TableName() string {
+ return "_tool_copilot_ai_credit_usage"
+}
+
+func (script *addCopilotAiCreditUsage) Up(basicRes context.BasicRes) errors.Error {
+ return migrationhelper.AutoMigrateTables(basicRes,
+ &aiCreditUsage20260701{},
+ )
+}
+
+func (*addCopilotAiCreditUsage) Version() uint64 {
+ return 20260701000000
+}
+
+func (*addCopilotAiCreditUsage) Name() string {
+ return "Add Copilot AI credit usage table"
+}
diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go b/backend/plugins/gh-copilot/models/migrationscripts/register.go
index 399735695..0529532e2 100644
--- a/backend/plugins/gh-copilot/models/migrationscripts/register.go
+++ b/backend/plugins/gh-copilot/models/migrationscripts/register.go
@@ -31,5 +31,6 @@ func All() []plugin.MigrationScript {
new(addPRFieldsToEnterpriseMetrics),
new(addOrganizationIdToUserMetrics),
new(addCopilotMetricsGaps),
+ new(addCopilotAiCreditUsage),
}
}
diff --git a/backend/plugins/gh-copilot/models/models.go b/backend/plugins/gh-copilot/models/models.go
index 5143ce5f8..2e43a1af6 100644
--- a/backend/plugins/gh-copilot/models/models.go
+++ b/backend/plugins/gh-copilot/models/models.go
@@ -47,5 +47,7 @@ func GetTablesInfo() []dal.Tabler {
&GhCopilotSeat{},
// User-team mappings
&GhCopilotUserTeam{},
+ // AI credit billing usage
+ &GhCopilotAiCreditUsage{},
}
}
diff --git a/backend/plugins/gh-copilot/tasks/ai_credit_usage_collector.go b/backend/plugins/gh-copilot/tasks/ai_credit_usage_collector.go
new file mode 100644
index 000000000..dded8f4f2
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/ai_credit_usage_collector.go
@@ -0,0 +1,172 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/log"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+const (
+ rawEnterpriseAiCreditUsageTable = "copilot_ai_credit_usage_enterprise"
+ rawOrgAiCreditUsageTable = "copilot_ai_credit_usage_org"
+)
+
+// CollectEnterpriseAiCreditUsage collects enterprise-level daily AI credit billing usage.
+func CollectEnterpriseAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+ return collectAiCreditUsage(taskCtx, models.AiCreditUsageLevelEnterprise)
+}
+
+// CollectOrgAiCreditUsage collects organization-level daily AI credit billing usage.
+func CollectOrgAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+ return collectAiCreditUsage(taskCtx, models.AiCreditUsageLevelOrganization)
+}
+
+// collectAiCreditUsage iterates day-by-day over the GitHub Billing Usage
+// "AI credit usage" endpoint, passing year/month/day query parameters, and stores
+// each daily report as a raw record. Shared by the enterprise and organization subtasks.
+func collectAiCreditUsage(taskCtx plugin.SubTaskContext, level string) errors.Error {
+ data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+ if !ok {
+ return errors.Default.New("task data is not GhCopilotTaskData")
+ }
+ connection := data.Connection
+ connection.Normalize()
+ logger := taskCtx.GetLogger()
+
+ var urlTemplate, rawTable string
+ switch level {
+ case models.AiCreditUsageLevelEnterprise:
+ if !connection.HasEnterprise() {
+ logger.Info("No enterprise configured, skipping enterprise AI credit usage collection")
+ return nil
+ }
+ urlTemplate = fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", connection.Enterprise)
+ rawTable = rawEnterpriseAiCreditUsageTable
+ case models.AiCreditUsageLevelOrganization:
+ if connection.Organization == "" {
+ logger.Info("No organization configured, skipping org AI credit usage collection")
+ return nil
+ }
+ // Note: the billing usage endpoint uses "organizations/{org}", not "orgs/{org}".
+ urlTemplate = fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", connection.Organization)
+ rawTable = rawOrgAiCreditUsageTable
+ default:
+ return errors.Default.New(fmt.Sprintf("unknown AI credit usage level: %s", level))
+ }
+
+ apiClient, err := CreateApiClient(taskCtx.TaskContext(), connection)
+ if err != nil {
+ return err
+ }
+
+ rawArgs := helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Table: rawTable,
+ Options: copilotRawParams{
+ ConnectionId: data.Options.ConnectionId,
+ ScopeId: data.Options.ScopeId,
+ Organization: connection.Organization,
+ Endpoint: connection.Endpoint,
+ },
+ }
+
+ collector, err := helper.NewStatefulApiCollector(rawArgs)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now().UTC()
+ start, until := computeReportDateRange(now, collector.GetSince())
+ start = clampDailyMetricsStartForBackfill(start, until)
+
+ dayIter := newDayIterator(start, until)
+
+ err = collector.InitCollector(helper.ApiCollectorArgs{
+ ApiClient: apiClient,
+ Input: dayIter,
+ UrlTemplate: urlTemplate,
+ Query: func(reqData *helper.RequestData) (url.Values, errors.Error) {
+ input := reqData.Input.(*dayInput)
+ y, m, d, parseErr := splitDay(input.Day)
+ if parseErr != nil {
+ return nil, parseErr
+ }
+ q := url.Values{}
+ q.Set("year", strconv.Itoa(y))
+ q.Set("month", strconv.Itoa(m))
+ q.Set("day", strconv.Itoa(d))
+ return q, nil
+ },
+ Incremental: true,
+ Concurrency: 1,
+ AfterResponse: ignoreNoContent,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
+ return parseAiCreditUsageResponse(res, logger)
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return collector.Execute()
+}
+
+// splitDay parses a YYYY-MM-DD string into year, month and day integers.
+func splitDay(day string) (int, int, int, errors.Error) {
+ t, parseErr := time.Parse("2006-01-02", day)
+ if parseErr != nil {
+ return 0, 0, 0, errors.Default.Wrap(parseErr, fmt.Sprintf("invalid day %q", day))
+ }
+ return t.Year(), int(t.Month()), t.Day(), nil
+}
+
+// parseAiCreditUsageResponse reads the single JSON report object returned for one day.
+// Empty bodies (no usage) are skipped; the whole object is stored as one raw record
+// and exploded into per-usageItem rows by the extractor.
+func parseAiCreditUsageResponse(res *http.Response, logger log.Logger) ([]json.RawMessage, errors.Error) {
+ body, readErr := io.ReadAll(res.Body)
+ res.Body.Close()
+ if readErr != nil {
+ return nil, errors.Default.Wrap(readErr, "failed to read AI credit usage response")
+ }
+ if isEmptyReport(body) {
+ return nil, nil
+ }
+ trimmed := bytes.TrimSpace(body)
+ // Defensive: only forward objects that actually carry usage items.
+ if !strings.Contains(string(trimmed), "usageItems") {
+ if logger != nil {
+ logger.Info("AI credit usage response had no usageItems, skipping")
+ }
+ return nil, nil
+ }
+ return []json.RawMessage{json.RawMessage(trimmed)}, nil
+}
diff --git a/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor.go b/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor.go
new file mode 100644
index 000000000..f94cdf600
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor.go
@@ -0,0 +1,162 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// aiCreditUsageReport mirrors the GitHub Billing Usage "AI credit usage" response.
+type aiCreditUsageReport struct {
+ TimePeriod struct {
+ Year int `json:"year"`
+ Month int `json:"month"`
+ Day int `json:"day"`
+ } `json:"timePeriod"`
+ Enterprise string `json:"enterprise"`
+ Organization string `json:"organization"`
+ User string `json:"user"`
+ CostCenter *struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ } `json:"costCenter"`
+ UsageItems []aiCreditUsageItem `json:"usageItems"`
+}
+
+type aiCreditUsageItem struct {
+ Product string `json:"product"`
+ Sku string `json:"sku"`
+ Model string `json:"model"`
+ UnitType string `json:"unitType"`
+ PricePerUnit float64 `json:"pricePerUnit"`
+ GrossQuantity float64 `json:"grossQuantity"`
+ GrossAmount float64 `json:"grossAmount"`
+ DiscountQuantity float64 `json:"discountQuantity"`
+ DiscountAmount float64 `json:"discountAmount"`
+ NetQuantity float64 `json:"netQuantity"`
+ NetAmount float64 `json:"netAmount"`
+}
+
+// ExtractEnterpriseAiCreditUsage parses raw enterprise AI credit usage reports.
+func ExtractEnterpriseAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+ return extractAiCreditUsage(taskCtx, models.AiCreditUsageLevelEnterprise, rawEnterpriseAiCreditUsageTable)
+}
+
+// ExtractOrgAiCreditUsage parses raw organization AI credit usage reports.
+func ExtractOrgAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error {
+ return extractAiCreditUsage(taskCtx, models.AiCreditUsageLevelOrganization, rawOrgAiCreditUsageTable)
+}
+
+// extractAiCreditUsage explodes each daily report's usageItems into one
+// GhCopilotAiCreditUsage row per product/sku/model/unitType. Shared by the
+// enterprise and organization extractor subtasks.
+func extractAiCreditUsage(taskCtx plugin.SubTaskContext, level, rawTable string) errors.Error {
+ data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+ if !ok {
+ return errors.Default.New("task data is not GhCopilotTaskData")
+ }
+ connection := data.Connection
+ connection.Normalize()
+
+ params := copilotRawParams{
+ ConnectionId: data.Options.ConnectionId,
+ ScopeId: data.Options.ScopeId,
+ Organization: connection.Organization,
+ Endpoint: connection.Endpoint,
+ }
+
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Table: rawTable,
+ Options: params,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, errors.Error) {
+ rows, err := mapAiCreditUsageReport(level, data.Options.ConnectionId, data.Options.ScopeId, row.Data)
+ if err != nil {
+ return nil, err
+ }
+ results := make([]interface{}, len(rows))
+ for i, r := range rows {
+ results[i] = r
+ }
+ return results, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return extractor.Execute()
+}
+
+// mapAiCreditUsageReport unmarshals a single AI credit usage report and explodes its
+// usageItems into one GhCopilotAiCreditUsage row per product/sku/model/unitType.
+func mapAiCreditUsageReport(level string, connectionId uint64, scopeId string, raw []byte) ([]*models.GhCopilotAiCreditUsage, errors.Error) {
+ var report aiCreditUsageReport
+ if err := errors.Convert(json.Unmarshal(raw, &report)); err != nil {
+ return nil, err
+ }
+
+ tp := report.TimePeriod
+ var date time.Time
+ if tp.Year > 0 && tp.Month > 0 && tp.Day > 0 {
+ date = time.Date(tp.Year, time.Month(tp.Month), tp.Day, 0, 0, 0, 0, time.UTC)
+ }
+
+ costCenterId, costCenterName := "", ""
+ if report.CostCenter != nil {
+ costCenterId = report.CostCenter.Id
+ costCenterName = report.CostCenter.Name
+ }
+
+ results := make([]*models.GhCopilotAiCreditUsage, 0, len(report.UsageItems))
+ for _, item := range report.UsageItems {
+ results = append(results, &models.GhCopilotAiCreditUsage{
+ ConnectionId: connectionId,
+ ScopeId: scopeId,
+ Level: level,
+ Year: tp.Year,
+ Month: tp.Month,
+ Day: tp.Day,
+ Product: item.Product,
+ Sku: item.Sku,
+ Model: item.Model,
+ UnitType: item.UnitType,
+ CostCenterId: costCenterId,
+ Date: date,
+ Enterprise: report.Enterprise,
+ Organization: report.Organization,
+ UserLogin: report.User,
+ CostCenterName: costCenterName,
+ PricePerUnit: item.PricePerUnit,
+ GrossQuantity: item.GrossQuantity,
+ GrossAmount: item.GrossAmount,
+ DiscountQuantity: item.DiscountQuantity,
+ DiscountAmount: item.DiscountAmount,
+ NetQuantity: item.NetQuantity,
+ NetAmount: item.NetAmount,
+ })
+ }
+ return results, nil
+}
diff --git a/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor_test.go b/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor_test.go
new file mode 100644
index 000000000..8292f4fdd
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/ai_credit_usage_extractor_test.go
@@ -0,0 +1,128 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "testing"
+ "time"
+
+ "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSplitDay(t *testing.T) {
+ y, m, d, err := splitDay("2026-06-08")
+ require.Nil(t, err)
+ assert.Equal(t, 2026, y)
+ assert.Equal(t, 6, m)
+ assert.Equal(t, 8, d)
+
+ _, _, _, err = splitDay("not-a-date")
+ assert.NotNil(t, err)
+}
+
+func TestMapAiCreditUsageReport(t *testing.T) {
+ raw := []byte(`{
+ "timePeriod": {"year": 2026, "month": 6, "day": 8},
+ "enterprise": "Avocado Corp",
+ "usageItems": [
+ {
+ "product": "Copilot",
+ "sku": "Copilot AI Credits",
+ "model": "Claude Opus 4.8",
+ "unitType": "ai-credits",
+ "pricePerUnit": 0.01,
+ "grossQuantity": 5445.77595,
+ "grossAmount": 54.4577595,
+ "discountQuantity": 5445.77595,
+ "discountAmount": 54.4577595,
+ "netQuantity": 0.0,
+ "netAmount": 0.0
+ },
+ {
+ "product": "Copilot",
+ "sku": "Copilot AI Credits",
+ "model": "Claude Sonnet 4.6",
+ "unitType": "ai-credits",
+ "pricePerUnit": 0.01,
+ "grossQuantity": 176.97417,
+ "grossAmount": 1.7697417,
+ "discountQuantity": 175.3533,
+ "discountAmount": 1.753533,
+ "netQuantity": 1.62087,
+ "netAmount": 0.0162087
+ }
+ ]
+ }`)
+
+ rows, err := mapAiCreditUsageReport(models.AiCreditUsageLevelEnterprise, 1, "avocado", raw)
+ require.Nil(t, err)
+ require.Len(t, rows, 2)
+
+ first := rows[0]
+ assert.Equal(t, uint64(1), first.ConnectionId)
+ assert.Equal(t, "avocado", first.ScopeId)
+ assert.Equal(t, models.AiCreditUsageLevelEnterprise, first.Level)
+ assert.Equal(t, 2026, first.Year)
+ assert.Equal(t, 6, first.Month)
+ assert.Equal(t, 8, first.Day)
+ assert.Equal(t, time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC), first.Date)
+ assert.Equal(t, "Avocado Corp", first.Enterprise)
+ assert.Equal(t, "Claude Opus 4.8", first.Model)
+ assert.Equal(t, "ai-credits", first.UnitType)
+ assert.InDelta(t, 5445.77595, first.GrossQuantity, 1e-9)
+ assert.InDelta(t, 0.0, first.NetQuantity, 1e-9)
+
+ second := rows[1]
+ assert.Equal(t, "Claude Sonnet 4.6", second.Model)
+ assert.InDelta(t, 1.62087, second.NetQuantity, 1e-9)
+ assert.InDelta(t, 0.0162087, second.NetAmount, 1e-9)
+}
+
+func TestMapAiCreditUsageReportWithCostCenter(t *testing.T) {
+ raw := []byte(`{
+ "timePeriod": {"year": 2025, "month": 6, "day": 10},
+ "enterprise": "acme",
+ "organization": "platform",
+ "user": "octocat",
+ "costCenter": {"id": "cc-1", "name": "Engineering"},
+ "usageItems": [
+ {"product": "Copilot", "sku": "AI-CREDITS", "model": "gpt-4.1", "unitType": "credits",
+ "pricePerUnit": 1, "grossQuantity": 10, "grossAmount": 10, "discountQuantity": 0,
+ "discountAmount": 0, "netQuantity": 10, "netAmount": 10}
+ ]
+ }`)
+
+ rows, err := mapAiCreditUsageReport(models.AiCreditUsageLevelOrganization, 2, "platform", raw)
+ require.Nil(t, err)
+ require.Len(t, rows, 1)
+
+ row := rows[0]
+ assert.Equal(t, "cc-1", row.CostCenterId)
+ assert.Equal(t, "Engineering", row.CostCenterName)
+ assert.Equal(t, "platform", row.Organization)
+ assert.Equal(t, "octocat", row.UserLogin)
+ assert.InDelta(t, 10.0, row.NetAmount, 1e-9)
+}
+
+func TestMapAiCreditUsageReportEmpty(t *testing.T) {
+ rows, err := mapAiCreditUsageReport(models.AiCreditUsageLevelEnterprise, 1, "x", []byte(`{"timePeriod":{"year":2026,"month":6,"day":8},"usageItems":[]}`))
+ require.Nil(t, err)
+ assert.Len(t, rows, 0)
+}
diff --git a/backend/plugins/gh-copilot/tasks/register.go b/backend/plugins/gh-copilot/tasks/register.go
index 3c7e5b1ee..fe6724cf1 100644
--- a/backend/plugins/gh-copilot/tasks/register.go
+++ b/backend/plugins/gh-copilot/tasks/register.go
@@ -28,11 +28,15 @@ func GetSubTaskMetas() []plugin.SubTaskMeta {
CollectEnterpriseMetricsMeta,
CollectUserMetricsMeta,
CollectUserTeamsMeta,
+ CollectEnterpriseAiCreditUsageMeta,
+ CollectOrgAiCreditUsageMeta,
// Extractors
ExtractSeatsMeta,
ExtractOrgMetricsMeta,
ExtractEnterpriseMetricsMeta,
ExtractUserMetricsMeta,
ExtractUserTeamsMeta,
+ ExtractEnterpriseAiCreditUsageMeta,
+ ExtractOrgAiCreditUsageMeta,
}
}
diff --git a/backend/plugins/gh-copilot/tasks/subtasks.go b/backend/plugins/gh-copilot/tasks/subtasks.go
index 61ed57995..fd58766d5 100644
--- a/backend/plugins/gh-copilot/tasks/subtasks.go
+++ b/backend/plugins/gh-copilot/tasks/subtasks.go
@@ -105,3 +105,37 @@ var ExtractUserTeamsMeta = plugin.SubTaskMeta{
Description: "Extract Copilot user-team mappings into tool-layer table",
Dependencies: []*plugin.SubTaskMeta{&CollectUserTeamsMeta},
}
+
+var CollectEnterpriseAiCreditUsageMeta = plugin.SubTaskMeta{
+ Name: "collectEnterpriseAiCreditUsage",
+ EntryPoint: CollectEnterpriseAiCreditUsage,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Collect GitHub Copilot enterprise AI credit billing usage",
+}
+
+var CollectOrgAiCreditUsageMeta = plugin.SubTaskMeta{
+ Name: "collectOrgAiCreditUsage",
+ EntryPoint: CollectOrgAiCreditUsage,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Collect GitHub Copilot organization AI credit billing usage",
+}
+
+var ExtractEnterpriseAiCreditUsageMeta = plugin.SubTaskMeta{
+ Name: "extractEnterpriseAiCreditUsage",
+ EntryPoint: ExtractEnterpriseAiCreditUsage,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Extract Copilot enterprise AI credit usage into tool-layer table",
+ Dependencies: []*plugin.SubTaskMeta{&CollectEnterpriseAiCreditUsageMeta},
+}
+
+var ExtractOrgAiCreditUsageMeta = plugin.SubTaskMeta{
+ Name: "extractOrgAiCreditUsage",
+ EntryPoint: ExtractOrgAiCreditUsage,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Extract Copilot organization AI credit usage into tool-layer table",
+ Dependencies: []*plugin.SubTaskMeta{&CollectOrgAiCreditUsageMeta},
+}
diff --git a/grafana/dashboards/mysql/GithubCopilotREADME.md b/grafana/dashboards/mysql/GithubCopilotREADME.md
index 642ff4252..92f424824 100644
--- a/grafana/dashboards/mysql/GithubCopilotREADME.md
+++ b/grafana/dashboards/mysql/GithubCopilotREADME.md
@@ -30,6 +30,8 @@ Tracks GitHub Copilot usage metrics across your organization:
- IDE distribution (VS Code, JetBrains, Neovim, etc.)
- Acceptance rates for code suggestions
- Chat and PR summary feature usage
+- **AI Credit Consumption**: gross/net credits and billed amount over time, broken down by model and
+ product/SKU (sourced from `_tool_copilot_ai_credit_usage`)
### 2. GitHub Copilot Impact Dashboard (`GithubCopilotImpact.json`)
diff --git a/grafana/dashboards/mysql/github-copilot-adoption.json b/grafana/dashboards/mysql/github-copilot-adoption.json
index e018b6ede..8c8347120 100644
--- a/grafana/dashboards/mysql/github-copilot-adoption.json
+++ b/grafana/dashboards/mysql/github-copilot-adoption.json
@@ -1 +1 @@
-{"annotations":{"list":[{"builtIn":1,"datasource":{"type":"datasource","uid":"grafana"},"enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","type":"dashboard"}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":0,"links":[],"liveNow":false,"panels":[{"datasource":"mysql","description":"Latest daily active users count","fieldConfig":{"defaults":{"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yel [...]
\ No newline at end of file
+{"annotations":{"list":[{"builtIn":1,"datasource":{"type":"datasource","uid":"grafana"},"enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","type":"dashboard"}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":0,"links":[],"liveNow":false,"panels":[{"datasource":"mysql","description":"Latest daily active users count","fieldConfig":{"defaults":{"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yel [...]
\ No newline at end of file
diff --git a/grafana/dashboards/postgresql/github-copilot-adoption.json b/grafana/dashboards/postgresql/github-copilot-adoption.json
index 978d263b8..a503ee4a4 100644
--- a/grafana/dashboards/postgresql/github-copilot-adoption.json
+++ b/grafana/dashboards/postgresql/github-copilot-adoption.json
@@ -2255,6 +2255,429 @@
],
"title": "Data Volume by Table",
"type": "table"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 112
+ },
+ "id": 40,
+ "panels": [],
+ "title": "AI Credit Consumption",
+ "type": "row"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "Total gross AI credits consumed in the selected period",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short",
+ "decimals": 2
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 0,
+ "y": 113
+ },
+ "id": 41,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT SUM(gross_quantity) AS \"Gross Credits\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}'",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Gross Credits",
+ "type": "stat"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "Total net AI credits (after discounts) in the selected period",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short",
+ "decimals": 2
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 6,
+ "y": 113
+ },
+ "id": 42,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT SUM(net_quantity) AS \"Net Credits\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}'",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Net Credits",
+ "type": "stat"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "Total net billed amount (after discounts) in the selected period",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "currencyUSD",
+ "decimals": 2
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 12,
+ "y": 113
+ },
+ "id": 43,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT SUM(net_amount) AS \"Net Amount\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}'",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Net Amount",
+ "type": "stat"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "Total gross billed amount in the selected period",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "currencyUSD",
+ "decimals": 2
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 18,
+ "y": 113
+ },
+ "id": 44,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT SUM(gross_amount) AS \"Gross Amount\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}'",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Gross Amount",
+ "type": "stat"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "Gross vs net AI credits consumed per day",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "Credits",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "unit": "short",
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 117
+ },
+ "id": 45,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT \"date\" AS time, SUM(gross_quantity) AS \"Gross Credits\", SUM(net_quantity) AS \"Net Credits\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}' GROUP BY \"date\" ORDER BY 1 NULLS LAST",
+ "refId": "A"
+ }
+ ],
+ "title": "AI Credits Consumed Over Time",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "AI credit consumption broken down by model",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 125
+ },
+ "id": 46,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT model AS \"Model\", SUM(gross_quantity) AS \"Gross Credits\", SUM(net_quantity) AS \"Net Credits\", SUM(net_amount) AS \"Net Amount\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}' GROUP BY \"model\" ORDER BY SUM(gross_quantity) DESC NULLS LAST LIMIT 20",
+ "refId": "A"
+ }
+ ],
+ "title": "AI Credits by Model",
+ "type": "table"
+ },
+ {
+ "datasource": "postgresql",
+ "description": "AI credit consumption broken down by product and SKU",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 125
+ },
+ "id": 47,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true
+ },
+ "pluginVersion": "11.0.0",
+ "targets": [
+ {
+ "datasource": "postgresql",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT product AS \"Product\", sku AS \"SKU\", SUM(gross_quantity) AS \"Gross Credits\", SUM(net_quantity) AS \"Net Credits\", SUM(net_amount) AS \"Net Amount\" FROM _tool_copilot_ai_credit_usage WHERE $__timeFilter(\"date\") AND ('${connection_id}' = '' OR connection_id::text = '${connection_id}') AND scope_id = '${scope_id}' GROUP BY \"product\", \"sku\" ORDER BY SUM(gross_quantity) DESC NULLS LAST LIMIT 20",
+ "refId": "A"
+ }
+ ],
+ "title": "AI Credits by Product / SKU",
+ "type": "table"
}
],
"refresh": "",