Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions backend/plugins/gh-copilot/models/enterprise_ai_credit_usage.go
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ func All() []plugin.MigrationScript {
new(addPRFieldsToEnterpriseMetrics),
new(addOrganizationIdToUserMetrics),
new(addCopilotMetricsGaps),
new(addAiCreditUsageMetrics),
}
}
4 changes: 4 additions & 0 deletions backend/plugins/gh-copilot/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,9 @@ func GetTablesInfo() []dal.Tabler {
&GhCopilotSeat{},
// User-team mappings
&GhCopilotUserTeam{},
// AI credit usage billing (enterprise, org, user levels)
&GhCopilotEnterpriseAiCreditUsage{},
&GhCopilotOrgAiCreditUsage{},
&GhCopilotUserAiCreditUsage{},
}
}
3 changes: 3 additions & 0 deletions backend/plugins/gh-copilot/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
53 changes: 53 additions & 0 deletions backend/plugins/gh-copilot/models/org_ai_credit_usage.go
Original file line number Diff line number Diff line change
@@ -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"
}
52 changes: 52 additions & 0 deletions backend/plugins/gh-copilot/models/user_ai_credit_usage.go
Original file line number Diff line number Diff line change
@@ -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"
}
118 changes: 118 additions & 0 deletions backend/plugins/gh-copilot/tasks/ai_credit_collector.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading