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 100644 index 00000000000..6bdcd5af7ce --- /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)" json:"enterprise" gorm:"comment:Enterprise slug"` + Model string `gorm:"primaryKey;type:varchar(255)" json:"model" gorm:"comment:AI model name (e.g., gpt-4.1)"` + Organization string `gorm:"index;type:varchar(255)" json:"organization" gorm:"comment:Organization within enterprise, if specified"` + User string `gorm:"index;type:varchar(255)" json:"user" gorm:"comment:Username, if specified"` + + Product string `gorm:"type:varchar(32)" json:"product" gorm:"comment:Product name (e.g., copilot)"` + CostCenterId string `gorm:"index;type:varchar(255)" json:"costCenterId" gorm:"comment:Cost center identifier"` + CostCenterName string `gorm:"type:varchar(255)" json:"costCenterName" gorm:"comment:Cost center display name"` + + // 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 100644 index 00000000000..f935918fef2 --- /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/core/models/migrationscripts/archived" + "github.com/apache/incubator-devlake/helpers/migrationhelper" + "github.com/apache/incubator-devlake/plugins/gh-copilot/models" +) + +type addAiCreditUsageMetrics struct{} + +func (u *addAiCreditUsageMetrics) Up(baseCtx context.BasicContext) errors.Error { + return migrationhelper.AutoMigrate( + baseCtx, + &models.GhCopilotEnterpriseAiCreditUsage{}, + &models.GhCopilotOrgAiCreditUsage{}, + &models.GhCopilotUserAiCreditUsage{}, + ) +} + +func (u *addAiCreditUsageMetrics) Version() string { + return "20260708_add_ai_credit_usage_metrics" +} + +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 100644 index 00000000000..3cbe515acf8 --- /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)" json:"organization" gorm:"comment:Organization name"` + Model string `gorm:"primaryKey;type:varchar(255)" json:"model" gorm:"comment:AI model name (e.g., gpt-4.1)"` + User string `gorm:"index;type:varchar(255)" json:"user" gorm:"comment:Username consuming the credits"` + + Product string `gorm:"type:varchar(32)" json:"product" gorm:"comment:Product name (e.g., copilot)"` + + // 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 100644 index 00000000000..25ba54a0f40 --- /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)" json:"user" gorm:"comment:GitHub username"` + Model string `gorm:"primaryKey;type:varchar(255)" json:"model" gorm:"comment:AI model name (e.g., gpt-4.1)"` + + Product string `gorm:"type:varchar(32)" json:"product" gorm:"comment:Product name (e.g., copilot)"` + + // 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/tasks/ai_credit_collector.go b/backend/plugins/gh-copilot/tasks/ai_credit_collector.go new file mode 100644 index 00000000000..61e0d463201 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/ai_credit_collector.go @@ -0,0 +1,118 @@ +/* +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" + "io" + "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" + +// CollectAiCreditUsage collects AI credit usage data from the billing API endpoints. +// Routes requests to enterprise, organization, or user endpoints based on connection configuration. +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() + + 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 { + // User-level credits are scoped to current authenticated user + urlTemplate = "user/settings/billing/ai_credit/usage" + scope = "user" + } + + // Build query parameters for date range + now := time.Now().UTC() + params := url.Values{} + params.Set("year", strconv.Itoa(now.Year())) + params.Set("month", strconv.Itoa(int(now.Month()))) + params.Set("day", strconv.Itoa(now.Day())) + + apiCollector, err := helper.NewApiCollector(taskCtx, rawAiCreditUsageTable) + if err != nil { + return err + } + + err = apiCollector.CollectPages( + func(col *helper.ApiCollector) errors.Error { + col.GetAttr("scope", scope) + col.SetRelation(fmt.Sprintf("%s-%s", data.Connection.ID, scope)) + col.SetQuery("pageSize", "100") + for key, vals := range params { + col.SetQuery(key, vals[0]) + } + col.GetNextPageCustomerizedPath(fmt.Sprintf("%s?%s", urlTemplate, params.Encode()), nil) + return nil + }, + func(col *helper.ApiCollector, page int, res *http.Response) errors.Error { + if res.StatusCode == http.StatusNotFound { + taskCtx.GetLogger().Warnf("AI credit usage endpoint not found (404) for scope %s", scope) + return nil + } + if res.StatusCode != http.StatusOK { + return errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("failed to collect AI credit usage for %s", scope)) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return errors.Convert(err) + } + defer res.Body.Close() + + // Parse response - endpoint returns usageItems array + var response struct { + UsageItems []map[string]interface{} `json:"usageItems"` + } + err = json.Unmarshal(body, &response) + if err != nil { + return errors.Convert(err) + } + + // Store raw response for extraction + for _, item := range response.UsageItems { + col.SaveRaw(item) + } + + return nil + }, + ) + + return err +} 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 100644 index 00000000000..024f72b7215 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/ai_credit_extractor.go @@ -0,0 +1,180 @@ +/* +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" + "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" + "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(taskCtx, rawAiCreditUsageTable) + if err != nil { + return err + } + + err = extractor.Extract( + rawAiCreditUsageTable, + 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 scopeAttr, ok := row.Params["scope"]; ok { + if connection.HasEnterprise() { + wrapper.Enterprise = scopeAttr.(string) + } else if connection.Organization != "" { + wrapper.Organization = scopeAttr.(string) + } + } + wrapper.Product = record.Product + wrapper.Model = record.Model + + // Parse date from row (need to extract from response context) + // For now, use current date - this should be enhanced to parse from API response + 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.GetEmail(), // Use authenticated user + 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 + }, + ) + + return err +} 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}, +}