diff --git a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_ai_credit_usage.csv b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_ai_credit_usage.csv new file mode 100644 index 00000000000..d9e0e72c229 --- /dev/null +++ b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_ai_credit_usage.csv @@ -0,0 +1,4 @@ +connection_id,scope_id,year,month,day,enterprise,model,organization,user,product,cost_center_id,cost_center_name,gross_quantity,discount_quantity,net_quantity,price_per_unit,gross_amount,discount_amount,net_amount +1,octodemo,2025,12,10,octodemo,gpt-4.1,,,copilot,,,100.5,10.0,90.5,1.0,100.5,10.0,90.5 +1,octodemo,2025,12,10,octodemo,gpt-4o,,,copilot,cc-eng,Engineering,50.0,5.0,45.0,1.0,50.0,5.0,45.0 +1,octodemo,2025,12,10,octodemo,claude-3,,,copilot,cc-ml,Machine Learning,30.0,2.0,28.0,1.0,30.0,2.0,28.0 diff --git a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_org_ai_credit_usage.csv b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_org_ai_credit_usage.csv new file mode 100644 index 00000000000..caf4f77cfed --- /dev/null +++ b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_org_ai_credit_usage.csv @@ -0,0 +1,4 @@ +connection_id,scope_id,year,month,day,organization,model,user,product,gross_quantity,discount_quantity,net_quantity,price_per_unit,gross_amount,discount_amount,net_amount +1,octodemo,2025,12,10,octodemo,gpt-4.1,alice,copilot,50.0,5.0,45.0,1.0,50.0,5.0,45.0 +1,octodemo,2025,12,10,octodemo,gpt-4o,bob,copilot,30.0,3.0,27.0,1.0,30.0,3.0,27.0 +1,octodemo,2025,12,10,octodemo,claude-3,charlie,copilot,20.0,0.0,20.0,1.0,20.0,0.0,20.0 diff --git a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_user_ai_credit_usage.csv b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_user_ai_credit_usage.csv new file mode 100644 index 00000000000..7e098273481 --- /dev/null +++ b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_user_ai_credit_usage.csv @@ -0,0 +1,4 @@ +connection_id,scope_id,year,month,day,user,model,product,gross_quantity,discount_quantity,net_quantity,price_per_unit,gross_amount,discount_amount,net_amount +1,octodemo,2025,12,10,alice,gpt-4.1,copilot,50.0,5.0,45.0,1.0,50.0,5.0,45.0 +1,octodemo,2025,12,10,alice,gpt-4o,copilot,30.0,3.0,27.0,1.0,30.0,3.0,27.0 +1,octodemo,2025,12,10,bob,claude-3,copilot,20.0,2.0,18.0,1.0,20.0,2.0,18.0 diff --git a/backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go b/backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go new file mode 100755 index 00000000000..8b01a8d4e12 --- /dev/null +++ b/backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go @@ -0,0 +1,56 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/models/common" +) + +// GhCopilotEnterpriseAiCreditUsage tracks AI credit consumption at the enterprise level. +// One row per time period per model per entity (user, org, or cost center). +type GhCopilotEnterpriseAiCreditUsage struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"` + Year int `gorm:"primaryKey" json:"year"` + Month int `gorm:"primaryKey" json:"month"` + Day int `gorm:"primaryKey" json:"day"` + + Enterprise string `gorm:"primaryKey;type:varchar(255);comment:Enterprise slug" json:"enterprise"` + Model string `gorm:"primaryKey;type:varchar(255);comment:AI model name (e.g., gpt-4.1)" json:"model"` + Organization string `gorm:"index;type:varchar(255);comment:Organization within enterprise, if specified" json:"organization"` + User string `gorm:"index;type:varchar(255);comment:Username, if specified" json:"user"` + + Product string `gorm:"type:varchar(32);comment:Product name (e.g., copilot)" json:"product"` + CostCenterId string `gorm:"index;type:varchar(255);comment:Cost center identifier" json:"costCenterId"` + CostCenterName string `gorm:"type:varchar(255);comment:Cost center display name" json:"costCenterName"` + + // Credit usage breakdown + GrossQuantity float64 `json:"grossQuantity" gorm:"comment:Raw credits consumed"` + DiscountQuantity float64 `json:"discountQuantity" gorm:"comment:Credits discounted"` + NetQuantity float64 `json:"netQuantity" gorm:"comment:Credits after discount"` + PricePerUnit float64 `json:"pricePerUnit" gorm:"comment:Price per credit unit"` + GrossAmount float64 `json:"grossAmount" gorm:"comment:Gross cost before discount"` + DiscountAmount float64 `json:"discountAmount" gorm:"comment:Discount amount"` + NetAmount float64 `json:"netAmount" gorm:"comment:Net cost after discount"` + + common.NoPKModel +} + +func (GhCopilotEnterpriseAiCreditUsage) TableName() string { + return "_tool_copilot_enterprise_ai_credit_usage" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/20260708_add_ai_credit_usage_metrics.go b/backend/plugins/gh-copilot/models/migrationscripts/20260708_add_ai_credit_usage_metrics.go new file mode 100755 index 00000000000..6b52dd0940a --- /dev/null +++ b/backend/plugins/gh-copilot/models/migrationscripts/20260708_add_ai_credit_usage_metrics.go @@ -0,0 +1,45 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/helpers/migrationhelper" + "github.com/apache/incubator-devlake/plugins/gh-copilot/models" +) + +type addAiCreditUsageMetrics struct{} + +func (u *addAiCreditUsageMetrics) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables( + basicRes, + &models.GhCopilotEnterpriseAiCreditUsage{}, + &models.GhCopilotOrgAiCreditUsage{}, + &models.GhCopilotUserAiCreditUsage{}, + &models.GhCopilotUserDailyMetrics{}, + ) +} + +func (u *addAiCreditUsageMetrics) Version() uint64 { + return 20260708000000 +} + +func (u *addAiCreditUsageMetrics) Name() string { + return "add AI credit usage billing tables" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go b/backend/plugins/gh-copilot/models/migrationscripts/register.go index 399735695e0..6bf50a37193 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(addAiCreditUsageMetrics), } } diff --git a/backend/plugins/gh-copilot/models/models.go b/backend/plugins/gh-copilot/models/models.go index 5143ce5f8b7..814e45f4a51 100644 --- a/backend/plugins/gh-copilot/models/models.go +++ b/backend/plugins/gh-copilot/models/models.go @@ -47,5 +47,9 @@ func GetTablesInfo() []dal.Tabler { &GhCopilotSeat{}, // User-team mappings &GhCopilotUserTeam{}, + // AI credit usage billing (enterprise, org, user levels) + &GhCopilotEnterpriseAiCreditUsage{}, + &GhCopilotOrgAiCreditUsage{}, + &GhCopilotUserAiCreditUsage{}, } } diff --git a/backend/plugins/gh-copilot/models/models_test.go b/backend/plugins/gh-copilot/models/models_test.go index ef5b3eff6f5..7e9a2f2e979 100644 --- a/backend/plugins/gh-copilot/models/models_test.go +++ b/backend/plugins/gh-copilot/models/models_test.go @@ -41,6 +41,9 @@ func TestGetTablesInfo(t *testing.T) { (&GhCopilotUserMetricsByModelFeature{}).TableName(): false, (&GhCopilotSeat{}).TableName(): false, (&GhCopilotUserTeam{}).TableName(): false, + (&GhCopilotEnterpriseAiCreditUsage{}).TableName(): false, + (&GhCopilotOrgAiCreditUsage{}).TableName(): false, + (&GhCopilotUserAiCreditUsage{}).TableName(): false, } if len(tables) != len(expected) { diff --git a/backend/plugins/gh-copilot/models/org_ai_credit_usage.go b/backend/plugins/gh-copilot/models/org_ai_credit_usage.go new file mode 100755 index 00000000000..a5deb6bede4 --- /dev/null +++ b/backend/plugins/gh-copilot/models/org_ai_credit_usage.go @@ -0,0 +1,53 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/models/common" +) + +// GhCopilotOrgAiCreditUsage tracks AI credit consumption at the organization level. +// One row per time period per model per user (within the organization). +type GhCopilotOrgAiCreditUsage struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"` + Year int `gorm:"primaryKey" json:"year"` + Month int `gorm:"primaryKey" json:"month"` + Day int `gorm:"primaryKey" json:"day"` + + Organization string `gorm:"primaryKey;type:varchar(255);comment:Organization name" json:"organization"` + Model string `gorm:"primaryKey;type:varchar(255);comment:AI model name (e.g., gpt-4.1)" json:"model"` + User string `gorm:"index;type:varchar(255);comment:Username consuming the credits" json:"user"` + + Product string `gorm:"type:varchar(32);comment:Product name (e.g., copilot)" json:"product"` + + // Credit usage breakdown + GrossQuantity float64 `json:"grossQuantity" gorm:"comment:Raw credits consumed"` + DiscountQuantity float64 `json:"discountQuantity" gorm:"comment:Credits discounted"` + NetQuantity float64 `json:"netQuantity" gorm:"comment:Credits after discount"` + PricePerUnit float64 `json:"pricePerUnit" gorm:"comment:Price per credit unit"` + GrossAmount float64 `json:"grossAmount" gorm:"comment:Gross cost before discount"` + DiscountAmount float64 `json:"discountAmount" gorm:"comment:Discount amount"` + NetAmount float64 `json:"netAmount" gorm:"comment:Net cost after discount"` + + common.NoPKModel +} + +func (GhCopilotOrgAiCreditUsage) TableName() string { + return "_tool_copilot_org_ai_credit_usage" +} diff --git a/backend/plugins/gh-copilot/models/user_ai_credit_usage.go b/backend/plugins/gh-copilot/models/user_ai_credit_usage.go new file mode 100755 index 00000000000..a2066c2f36d --- /dev/null +++ b/backend/plugins/gh-copilot/models/user_ai_credit_usage.go @@ -0,0 +1,52 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/models/common" +) + +// GhCopilotUserAiCreditUsage tracks AI credit consumption at the individual user level. +// One row per time period per model per authenticated user. +type GhCopilotUserAiCreditUsage struct { + ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"` + ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"` + Year int `gorm:"primaryKey" json:"year"` + Month int `gorm:"primaryKey" json:"month"` + Day int `gorm:"primaryKey" json:"day"` + + User string `gorm:"primaryKey;type:varchar(255);comment:GitHub username" json:"user"` + Model string `gorm:"primaryKey;type:varchar(255);comment:AI model name (e.g., gpt-4.1)" json:"model"` + + Product string `gorm:"type:varchar(32);comment:Product name (e.g., copilot)" json:"product"` + + // Credit usage breakdown + GrossQuantity float64 `json:"grossQuantity" gorm:"comment:Raw credits consumed"` + DiscountQuantity float64 `json:"discountQuantity" gorm:"comment:Credits discounted"` + NetQuantity float64 `json:"netQuantity" gorm:"comment:Credits after discount"` + PricePerUnit float64 `json:"pricePerUnit" gorm:"comment:Price per credit unit"` + GrossAmount float64 `json:"grossAmount" gorm:"comment:Gross cost before discount"` + DiscountAmount float64 `json:"discountAmount" gorm:"comment:Discount amount"` + NetAmount float64 `json:"netAmount" gorm:"comment:Net cost after discount"` + + common.NoPKModel +} + +func (GhCopilotUserAiCreditUsage) TableName() string { + return "_tool_copilot_user_ai_credit_usage" +} diff --git a/backend/plugins/gh-copilot/models/user_metrics.go b/backend/plugins/gh-copilot/models/user_metrics.go old mode 100644 new mode 100755 index 18e9134c226..ad4ce83551d --- a/backend/plugins/gh-copilot/models/user_metrics.go +++ b/backend/plugins/gh-copilot/models/user_metrics.go @@ -38,6 +38,7 @@ type GhCopilotUserDailyMetrics struct { UsedCli bool `json:"usedCli" gorm:"comment:Whether user used Copilot CLI"` UsedCopilotCodeReviewActive bool `json:"usedCopilotCodeReviewActive" gorm:"comment:Whether user actively used code review"` UsedCopilotCodeReviewPassive bool `json:"usedCopilotCodeReviewPassive" gorm:"comment:Whether user passively used code review"` + AiCreditsUsed float64 `json:"aiCreditsUsed" gorm:"comment:AI credits consumed on this day"` CopilotActivityMetrics `mapstructure:",squash"` CopilotCliMetrics `mapstructure:",squash"` diff --git a/backend/plugins/gh-copilot/tasks/ai_credit_collector.go b/backend/plugins/gh-copilot/tasks/ai_credit_collector.go new file mode 100755 index 00000000000..f7831942289 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/ai_credit_collector.go @@ -0,0 +1,109 @@ +/* +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" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" +) + +const rawAiCreditUsageTable = "copilot_ai_credit_usage" + +func CollectAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error { + data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData) + if !ok { + return errors.Default.New("task data is not GhCopilotTaskData") + } + connection := data.Connection + connection.Normalize() + + apiClient, err := CreateApiClient(taskCtx.TaskContext(), connection) + if err != nil { + return err + } + + var urlTemplate string + var scope string + + if connection.HasEnterprise() { + urlTemplate = fmt.Sprintf("enterprises/%s/settings/billing/ai_credit/usage", connection.Enterprise) + scope = connection.Enterprise + } else if connection.Organization != "" { + urlTemplate = fmt.Sprintf("organizations/%s/settings/billing/ai_credit/usage", connection.Organization) + scope = connection.Organization + } else { + urlTemplate = "user/settings/billing/ai_credit/usage" + scope = "user" + } + + now := time.Now().UTC() + queryParam := url.Values{} + queryParam.Set("year", strconv.Itoa(now.Year())) + queryParam.Set("month", strconv.Itoa(int(now.Month()))) + queryParam.Set("day", strconv.Itoa(now.Day())) + + collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Params: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: connection.Organization, + Endpoint: connection.Endpoint, + }, + Table: rawAiCreditUsageTable, + }, + ApiClient: apiClient, + UrlTemplate: urlTemplate, + Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { + return queryParam, nil + }, + ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { + if res.StatusCode == http.StatusNotFound { + taskCtx.GetLogger().Warn(nil, "AI credit usage endpoint not found (404) for scope %s", scope) + return nil, nil + } + if res.StatusCode != http.StatusOK { + return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to collect AI credit usage for %s", scope)) + } + + var response struct { + UsageItems []json.RawMessage `json:"usageItems"` + } + err := helper.UnmarshalResponse(res, &response) + if err != nil { + return nil, err + } + + return response.UsageItems, nil + }, + }) + if err != nil { + return err + } + + return collector.Execute() +} diff --git a/backend/plugins/gh-copilot/tasks/ai_credit_extractor.go b/backend/plugins/gh-copilot/tasks/ai_credit_extractor.go new file mode 100755 index 00000000000..d777b1a682e --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/ai_credit_extractor.go @@ -0,0 +1,183 @@ +/* +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" +) + +// aiCreditUsageRecord represents a single usage item from the AI credit usage API. +type aiCreditUsageRecord 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"` + DiscountQuantity float64 `json:"discountQuantity"` + NetQuantity float64 `json:"netQuantity"` + GrossAmount float64 `json:"grossAmount"` + DiscountAmount float64 `json:"discountAmount"` + NetAmount float64 `json:"netAmount"` +} + +// aiCreditResponseWrapper represents the wrapper around the API response containing time period and usage items. +type aiCreditResponseWrapper 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"` + Product string `json:"product"` + Model string `json:"model"` + CostCenter struct { + Id string `json:"id"` + Name string `json:"name"` + } `json:"costCenter"` +} + +// ExtractAiCreditUsage parses AI credit usage records into the appropriate model tables. +func ExtractAiCreditUsage(taskCtx plugin.SubTaskContext) errors.Error { + data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData) + if !ok { + return errors.Default.New("task data is not GhCopilotTaskData") + } + connection := data.Connection + connection.Normalize() + + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: rawAiCreditUsageTable, + Options: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: connection.Organization, + Endpoint: connection.Endpoint, + }, + }, + Extract: func(row *helper.RawData) ([]interface{}, errors.Error) { + // Parse raw data + var record aiCreditUsageRecord + err := json.Unmarshal(row.Data, &record) + if err != nil { + return nil, errors.Convert(err) + } + + // Extract wrapper info from row context + var wrapper aiCreditResponseWrapper + if connection.HasEnterprise() { + wrapper.Enterprise = connection.Enterprise + } else if connection.Organization != "" { + wrapper.Organization = connection.Organization + } + wrapper.Product = record.Product + wrapper.Model = record.Model + + // Parse date from row (use current time or parsed period) + now := time.Now().UTC() + wrapper.TimePeriod.Year = now.Year() + wrapper.TimePeriod.Month = int(now.Month()) + wrapper.TimePeriod.Day = now.Day() + + var results []interface{} + + // Route to appropriate table based on connection type + if connection.HasEnterprise() { + toolRecord := &models.GhCopilotEnterpriseAiCreditUsage{ + ConnectionId: data.Connection.ID, + ScopeId: data.Options.ScopeId, + Year: wrapper.TimePeriod.Year, + Month: wrapper.TimePeriod.Month, + Day: wrapper.TimePeriod.Day, + Enterprise: wrapper.Enterprise, + Model: record.Model, + Organization: wrapper.Organization, + User: wrapper.User, + Product: record.Product, + CostCenterId: wrapper.CostCenter.Id, + CostCenterName: wrapper.CostCenter.Name, + GrossQuantity: record.GrossQuantity, + DiscountQuantity: record.DiscountQuantity, + NetQuantity: record.NetQuantity, + PricePerUnit: record.PricePerUnit, + GrossAmount: record.GrossAmount, + DiscountAmount: record.DiscountAmount, + NetAmount: record.NetAmount, + } + results = append(results, toolRecord) + } else if connection.Organization != "" { + toolRecord := &models.GhCopilotOrgAiCreditUsage{ + ConnectionId: data.Connection.ID, + ScopeId: data.Options.ScopeId, + Year: wrapper.TimePeriod.Year, + Month: wrapper.TimePeriod.Month, + Day: wrapper.TimePeriod.Day, + Organization: wrapper.Organization, + Model: record.Model, + User: wrapper.User, + Product: record.Product, + GrossQuantity: record.GrossQuantity, + DiscountQuantity: record.DiscountQuantity, + NetQuantity: record.NetQuantity, + PricePerUnit: record.PricePerUnit, + GrossAmount: record.GrossAmount, + DiscountAmount: record.DiscountAmount, + NetAmount: record.NetAmount, + } + results = append(results, toolRecord) + } else { + // User-level credits + toolRecord := &models.GhCopilotUserAiCreditUsage{ + ConnectionId: data.Connection.ID, + ScopeId: data.Options.ScopeId, + Year: wrapper.TimePeriod.Year, + Month: wrapper.TimePeriod.Month, + Day: wrapper.TimePeriod.Day, + User: connection.Name, // Use connection display name + Model: record.Model, + Product: record.Product, + GrossQuantity: record.GrossQuantity, + DiscountQuantity: record.DiscountQuantity, + NetQuantity: record.NetQuantity, + PricePerUnit: record.PricePerUnit, + GrossAmount: record.GrossAmount, + DiscountAmount: record.DiscountAmount, + NetAmount: record.NetAmount, + } + results = append(results, toolRecord) + } + + return results, nil + }, + }) + if err != nil { + return err + } + + return extractor.Execute() +} diff --git a/backend/plugins/gh-copilot/tasks/register.go b/backend/plugins/gh-copilot/tasks/register.go index 3c7e5b1eeb9..23d592304e9 100644 --- a/backend/plugins/gh-copilot/tasks/register.go +++ b/backend/plugins/gh-copilot/tasks/register.go @@ -28,11 +28,13 @@ func GetSubTaskMetas() []plugin.SubTaskMeta { CollectEnterpriseMetricsMeta, CollectUserMetricsMeta, CollectUserTeamsMeta, + CollectAiCreditUsageMeta, // Extractors ExtractSeatsMeta, ExtractOrgMetricsMeta, ExtractEnterpriseMetricsMeta, ExtractUserMetricsMeta, ExtractUserTeamsMeta, + ExtractAiCreditUsageMeta, } } diff --git a/backend/plugins/gh-copilot/tasks/subtasks.go b/backend/plugins/gh-copilot/tasks/subtasks.go index 61ed5799525..69255287026 100644 --- a/backend/plugins/gh-copilot/tasks/subtasks.go +++ b/backend/plugins/gh-copilot/tasks/subtasks.go @@ -105,3 +105,20 @@ var ExtractUserTeamsMeta = plugin.SubTaskMeta{ Description: "Extract Copilot user-team mappings into tool-layer table", Dependencies: []*plugin.SubTaskMeta{&CollectUserTeamsMeta}, } + +var CollectAiCreditUsageMeta = plugin.SubTaskMeta{ + Name: "collectAiCreditUsage", + EntryPoint: CollectAiCreditUsage, + EnabledByDefault: true, + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + Description: "Collect GitHub Copilot AI credit usage billing data from billing API", +} + +var ExtractAiCreditUsageMeta = plugin.SubTaskMeta{ + Name: "extractAiCreditUsage", + EntryPoint: ExtractAiCreditUsage, + EnabledByDefault: true, + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + Description: "Extract Copilot AI credit usage into tool-layer billing tables", + Dependencies: []*plugin.SubTaskMeta{&CollectAiCreditUsageMeta}, +} diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go old mode 100644 new mode 100755 index 72992194063..7671fab86a9 --- a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go +++ b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go @@ -49,6 +49,7 @@ type userDailyReport struct { UsedCli bool `json:"used_cli"` UsedCopilotCodeReviewActive bool `json:"used_copilot_code_review_active"` UsedCopilotCodeReviewPassive bool `json:"used_copilot_code_review_passive"` + AiCreditsUsed float64 `json:"ai_credits_used"` TotalsByIde []userTotalsByIde `json:"totals_by_ide"` TotalsByFeature []totalsByFeature `json:"totals_by_feature"` TotalsByLanguageFeature []totalsByLangFeature `json:"totals_by_language_feature"` @@ -123,6 +124,7 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { UsedCli: u.UsedCli, UsedCopilotCodeReviewActive: u.UsedCopilotCodeReviewActive, UsedCopilotCodeReviewPassive: u.UsedCopilotCodeReviewPassive, + AiCreditsUsed: u.AiCreditsUsed, CopilotActivityMetrics: models.CopilotActivityMetrics{ UserInitiatedInteractionCount: u.UserInitiatedInteractionCount, CodeGenerationActivityCount: u.CodeGenerationActivityCount,