mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-18 14:30:35 +01:00
Compare commits
2 Commits
issue_5325
...
issue-5388
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b91e664e14 | ||
|
|
04ec681eda |
@@ -4889,19 +4889,6 @@ components:
|
||||
- offset
|
||||
- limit
|
||||
type: object
|
||||
LlmpricingruletypesGettableUnmappedModels:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUnmappedModel'
|
||||
nullable: true
|
||||
type: array
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingCacheCosts:
|
||||
properties:
|
||||
mode:
|
||||
@@ -4991,19 +4978,6 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
LlmpricingruletypesUnmappedModel:
|
||||
properties:
|
||||
modelName:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
spanCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- modelName
|
||||
- spanCount
|
||||
type: object
|
||||
LlmpricingruletypesUpdatableLLMPricingRule:
|
||||
properties:
|
||||
enabled:
|
||||
@@ -10477,60 +10451,6 @@ paths:
|
||||
summary: Get a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/llm_pricing_rules/unmapped_models:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns models seen in the last hour of trace data (gen_ai.request.model)
|
||||
that no pricing rule pattern matches, so the user can add them to an existing
|
||||
rule or create a new one.
|
||||
operationId: ListUnmappedLLMModels
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesGettableUnmappedModels'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List unmapped models
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/logs/promote_paths:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {
|
||||
GetLLMPricingRulePathParameters,
|
||||
ListLLMPricingRules200,
|
||||
ListLLMPricingRulesParams,
|
||||
ListUnmappedLLMModels200,
|
||||
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -394,87 +393,3 @@ export const invalidateGetLLMPricingRule = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
|
||||
url: `/api/v1/llm_pricing_rules/unmapped_models`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListUnmappedLLMModelsQueryKey = () => {
|
||||
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
|
||||
};
|
||||
|
||||
export const getListUnmappedLLMModelsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>
|
||||
> = ({ signal }) => listUnmappedLLMModels(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListUnmappedLLMModelsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>
|
||||
>;
|
||||
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
|
||||
export function useListUnmappedLLMModels<
|
||||
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List unmapped models
|
||||
*/
|
||||
export const invalidateListUnmappedLLMModels = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListUnmappedLLMModelsQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
@@ -6537,33 +6537,6 @@ export interface LlmpricingruletypesGettablePricingRulesDTO {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUnmappedModelDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
spanCount: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesGettableUnmappedModelsDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
items: LlmpricingruletypesUnmappedModelDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
@@ -9501,14 +9474,6 @@ export type GetLLMPricingRule200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListUnmappedLLMModels200 = {
|
||||
data: LlmpricingruletypesGettableUnmappedModelsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPromotedAndIndexedPaths200 = {
|
||||
/**
|
||||
* @type array,null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Skeleton } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -123,14 +124,24 @@ function ServiceOverview({
|
||||
/>
|
||||
<Card data-testid="service_latency">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
onDragSelect={onDragSelect}
|
||||
widget={latencyWidget}
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
{topLevelOperationsIsLoading && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!topLevelOperationsIsLoading && (
|
||||
<Graph
|
||||
onDragSelect={onDragSelect}
|
||||
widget={latencyWidget}
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
@@ -28,14 +29,24 @@ function TopLevelOperation({
|
||||
</Typography>
|
||||
) : (
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
widget={widget}
|
||||
onClickHandler={handleGraphClick(opName)}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
{topLevelOperationsIsLoading && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!topLevelOperationsIsLoading && (
|
||||
<Graph
|
||||
widget={widget}
|
||||
onClickHandler={handleGraphClick(opName)}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -49,26 +49,6 @@ func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/unmapped_models", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.llmPricingRuleHandler.ListUnmappedModels),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListUnmappedLLMModels",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "List unmapped models",
|
||||
Description: "Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(llmpricingruletypes.GettableUnmappedModels),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.llmPricingRuleHandler.Get),
|
||||
handler.OpenAPIDef{
|
||||
|
||||
@@ -22,7 +22,7 @@ func newConfig() factory.Config {
|
||||
Agent: AgentConfig{
|
||||
// we will maintain the latest version of cloud integration agent from here,
|
||||
// till we automate it externally or figure out a way to validate it.
|
||||
Version: "v0.0.13",
|
||||
Version: "v0.0.12",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,28 +118,6 @@ func (h *handler) CreateOrUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// ListUnmappedModels handles GET /api/v1/llm_pricing_rules/unmapped_models.
|
||||
func (h *handler) ListUnmappedModels(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
models, err := h.module.ListUnmappedModels(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, llmpricingruletypes.NewGettableUnmappedModels(models))
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/llm_pricing_rules/{id}.
|
||||
func (h *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
|
||||
@@ -3,30 +3,22 @@ package impllmpricingrule
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// unmappedModelsLookback is the trace data window scanned to discover models in use.
|
||||
const unmappedModelsLookback = time.Hour
|
||||
|
||||
type module struct {
|
||||
store llmpricingruletypes.Store
|
||||
querier querier.Querier
|
||||
store llmpricingruletypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store llmpricingruletypes.Store, querier querier.Querier) llmpricingrule.Module {
|
||||
return &module{store: store, querier: querier}
|
||||
func NewModule(store llmpricingruletypes.Store) llmpricingrule.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
@@ -37,28 +29,6 @@ func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID
|
||||
return module.store.Get(ctx, orgID, id)
|
||||
}
|
||||
|
||||
// ListUnmappedModels discovers the models present in the last hour of trace data
|
||||
// (gen_ai.request.model) and returns the ones that no pricing rule pattern matches.
|
||||
func (module *module) ListUnmappedModels(ctx context.Context, orgID valuer.UUID) ([]*llmpricingruletypes.UnmappedModel, error) {
|
||||
models, err := module.discoverModels(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, _, err := module.store.List(ctx, orgID, 0, 10000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unmapped := make([]*llmpricingruletypes.UnmappedModel, 0, len(models))
|
||||
for _, m := range models {
|
||||
if !llmpricingruletypes.ModelMatchesAnyRule(m.ModelName, rules) {
|
||||
unmapped = append(unmapped, m)
|
||||
}
|
||||
}
|
||||
return unmapped, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdate applies a batch of pricing rule changes:
|
||||
// - ID set → match by id, overwrite fields.
|
||||
// - SourceID set → match by source_id; if found overwrite, else insert.
|
||||
@@ -165,108 +135,3 @@ func (module *module) findExisting(ctx context.Context, orgID valuer.UUID, u *ll
|
||||
return nil, errors.Newf(errors.TypeNotFound, llmpricingruletypes.ErrCodePricingRuleNotFound, "rule has neither id nor sourceId")
|
||||
}
|
||||
}
|
||||
|
||||
// discoverModels runs a QBv5 traces aggregation grouped by gen_ai.request.model
|
||||
// over the lookback window and returns each distinct model with its span count.
|
||||
func (module *module) discoverModels(ctx context.Context, orgID valuer.UUID) ([]*llmpricingruletypes.UnmappedModel, error) {
|
||||
now := time.Now()
|
||||
req := &qbtypes.QueryRangeRequest{
|
||||
Start: uint64(now.Add(-unmappedModelsLookback).UnixMilli()),
|
||||
End: uint64(now.UnixMilli()),
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: fmt.Sprintf("%s EXISTS", llmpricingruletypes.GenAIRequestModel)},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "count()", Alias: "spanCount"},
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: llmpricingruletypes.GenAIRequestModel,
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: llmpricingruletypes.GenAIProviderName,
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}},
|
||||
},
|
||||
Limit: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := module.querier.QueryRange(ctx, orgID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseModels(resp), nil
|
||||
}
|
||||
|
||||
// parseModels extracts the grouped model names and their span counts from a scalar response.
|
||||
func parseModels(resp *qbtypes.QueryRangeResponse) []*llmpricingruletypes.UnmappedModel {
|
||||
if resp == nil || len(resp.Data.Results) == 0 {
|
||||
return nil
|
||||
}
|
||||
sd, ok := resp.Data.Results[0].(*qbtypes.ScalarData)
|
||||
if !ok || sd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
modelIdx, providerIdx, countIdx := -1, -1, -1
|
||||
for i, c := range sd.Columns {
|
||||
switch c.Type {
|
||||
case qbtypes.ColumnTypeGroup:
|
||||
switch c.Name {
|
||||
case llmpricingruletypes.GenAIRequestModel:
|
||||
modelIdx = i
|
||||
case llmpricingruletypes.GenAIProviderName:
|
||||
providerIdx = i
|
||||
}
|
||||
case qbtypes.ColumnTypeAggregation:
|
||||
countIdx = i
|
||||
}
|
||||
}
|
||||
if modelIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*llmpricingruletypes.UnmappedModel, 0, len(sd.Data))
|
||||
for _, row := range sd.Data {
|
||||
name, _ := row[modelIdx].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
provider := ""
|
||||
if providerIdx != -1 {
|
||||
provider, _ = row[providerIdx].(string)
|
||||
}
|
||||
models = append(models, &llmpricingruletypes.UnmappedModel{ModelName: name, Provider: provider, SpanCount: toUint64(row, countIdx)})
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func toUint64(row []any, idx int) uint64 {
|
||||
if idx < 0 || idx >= len(row) {
|
||||
return 0
|
||||
}
|
||||
switch v := row[idx].(type) {
|
||||
case uint64:
|
||||
return v
|
||||
case int64:
|
||||
return uint64(v)
|
||||
case float64:
|
||||
return uint64(v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ type Module interface {
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
|
||||
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []*llmpricingruletypes.UpdatableLLMPricingRule) (err error)
|
||||
Delete(ctx context.Context, orgID, id valuer.UUID) error
|
||||
ListUnmappedModels(ctx context.Context, orgID valuer.UUID) ([]*llmpricingruletypes.UnmappedModel, error)
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for pricing rule endpoints.
|
||||
@@ -26,5 +25,4 @@ type Handler interface {
|
||||
Get(rw http.ResponseWriter, r *http.Request)
|
||||
CreateOrUpdate(rw http.ResponseWriter, r *http.Request)
|
||||
Delete(rw http.ResponseWriter, r *http.Request)
|
||||
ListUnmappedModels(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func NewModules(
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
|
||||
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore)),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package llmpricingruletypes
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -17,7 +16,6 @@ const (
|
||||
LLMCostFeatureType agentConf.AgentFeatureType = "llm_pricing"
|
||||
|
||||
GenAIRequestModel = "gen_ai.request.model"
|
||||
GenAIProviderName = "gen_ai.provider.name"
|
||||
GenAIUsageInputTokens = "gen_ai.usage.input_tokens"
|
||||
GenAIUsageOutputTokens = "gen_ai.usage.output_tokens"
|
||||
GenAIUsageCacheReadInputTokens = "gen_ai.usage.cache_read.input_tokens"
|
||||
@@ -138,32 +136,6 @@ type GettablePricingRules struct {
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
// UnmappedModel is a model observed in trace data (gen_ai.request.model) that
|
||||
// no pricing rule pattern matches, so no cost is being computed for it.
|
||||
type UnmappedModel struct {
|
||||
ModelName string `json:"modelName" required:"true"`
|
||||
Provider string `json:"provider"`
|
||||
SpanCount uint64 `json:"spanCount" required:"true"`
|
||||
}
|
||||
|
||||
type GettableUnmappedModels struct {
|
||||
Items []*UnmappedModel `json:"items" required:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
}
|
||||
|
||||
// ModelMatchesAnyRule reports whether model matches any rule's glob pattern,
|
||||
// mirroring the path.Match semantics the signozllmpricing OTel processor uses.
|
||||
func ModelMatchesAnyRule(model string, rules []*LLMPricingRule) bool {
|
||||
for _, r := range rules {
|
||||
for _, pattern := range r.ModelPattern {
|
||||
if ok, err := path.Match(pattern, model); err == nil && ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (LLMPricingRuleUnit) Enum() []any {
|
||||
return []any{UnitPerMillionTokens}
|
||||
}
|
||||
@@ -232,13 +204,6 @@ func NewGettableLLMPricingRulesFromLLMPricingRules(items []*LLMPricingRule, tota
|
||||
}
|
||||
}
|
||||
|
||||
func NewGettableUnmappedModels(items []*UnmappedModel) *GettableUnmappedModels {
|
||||
return &GettableUnmappedModels{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
}
|
||||
|
||||
func NewLLMPricingRuleFromUpdatable(u *UpdatableLLMPricingRule, orgID valuer.UUID, userEmail string, now time.Time) *LLMPricingRule {
|
||||
isOverride := true
|
||||
if u.IsOverride != nil {
|
||||
|
||||
@@ -338,6 +338,7 @@ func isValidLabelValue(v string) bool {
|
||||
// validate runs during UnmarshalJSON (read + write path).
|
||||
// Preserves the original pre-existing checks only so that stored rules
|
||||
// continue to load without errors.
|
||||
// TODO(srikanthccv): remove this once v1 is deprecated and removed.
|
||||
func (r *PostableRule) validate() error {
|
||||
var errs []error
|
||||
|
||||
@@ -366,9 +367,13 @@ func (r *PostableRule) validate() error {
|
||||
|
||||
errs = append(errs, testTemplateParsing(r)...)
|
||||
|
||||
joined := errors.Join(errs...)
|
||||
if joined != nil {
|
||||
return errors.WrapInvalidInputf(joined, errors.CodeInvalidInput, "validation failed")
|
||||
if len(errs) > 0 {
|
||||
messages := make([]string, len(errs))
|
||||
for i, e := range errs {
|
||||
messages[i] = e.Error()
|
||||
}
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "alert rule definition is not valid").
|
||||
WithAdditional(messages...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -466,9 +471,13 @@ func (r *PostableRule) Validate() error {
|
||||
|
||||
errs = append(errs, testTemplateParsing(r)...)
|
||||
|
||||
joined := errors.Join(errs...)
|
||||
if joined != nil {
|
||||
return errors.WrapInvalidInputf(joined, errors.CodeInvalidInput, "validation failed")
|
||||
if len(errs) > 0 {
|
||||
messages := make([]string, len(errs))
|
||||
for i, e := range errs {
|
||||
messages[i] = e.Error()
|
||||
}
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "alert rule is not valid").
|
||||
WithAdditional(messages...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,8 +4,23 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
func errorContains(err error, substr string) bool {
|
||||
j := errors.AsJSON(err)
|
||||
if strings.Contains(j.Message, substr) {
|
||||
return true
|
||||
}
|
||||
for _, e := range j.Errors {
|
||||
if strings.Contains(e.Message, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validV1Builder returns a minimal valid v1 builder rule JSON.
|
||||
func validV1Builder() string {
|
||||
return `{
|
||||
@@ -494,7 +509,7 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errSubstr)
|
||||
} else if tt.errSubstr != "" && !strings.Contains(err.Error(), tt.errSubstr) {
|
||||
} else if tt.errSubstr != "" && !errorContains(err, tt.errSubstr) {
|
||||
t.Errorf("expected error containing %q, got: %v", tt.errSubstr, err)
|
||||
}
|
||||
} else {
|
||||
@@ -687,7 +702,7 @@ func TestValidate_V1_ConditionFields(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
if validateErr == nil {
|
||||
t.Errorf("expected Validate() error containing %q, got nil", tt.errSubstr)
|
||||
} else if tt.errSubstr != "" && !strings.Contains(validateErr.Error(), tt.errSubstr) {
|
||||
} else if tt.errSubstr != "" && !errorContains(validateErr, tt.errSubstr) {
|
||||
t.Errorf("expected error containing %q, got: %v", tt.errSubstr, validateErr)
|
||||
}
|
||||
} else {
|
||||
@@ -1029,7 +1044,7 @@ func TestValidate_V2Alpha1(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errSubstr)
|
||||
} else if tt.errSubstr != "" && !strings.Contains(err.Error(), tt.errSubstr) {
|
||||
} else if tt.errSubstr != "" && !errorContains(err, tt.errSubstr) {
|
||||
t.Errorf("expected error containing %q, got: %v", tt.errSubstr, err)
|
||||
}
|
||||
} else {
|
||||
@@ -1337,7 +1352,7 @@ func TestValidate_MultipleErrors(t *testing.T) {
|
||||
t.Fatal("expected unmarshal error for wrong version")
|
||||
}
|
||||
// The error should mention version
|
||||
if !strings.Contains(err.Error(), "version") {
|
||||
if !errorContains(err, "version") {
|
||||
t.Errorf("expected error to mention version, got: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -1355,10 +1370,9 @@ func TestValidate_MultipleErrors(t *testing.T) {
|
||||
if validateErr == nil {
|
||||
t.Fatal("expected Validate() error")
|
||||
}
|
||||
errStr := validateErr.Error()
|
||||
// Should contain errors for thresholds, evaluation, notificationSettings
|
||||
for _, substr := range []string{"evaluation", "notificationSettings"} {
|
||||
if !strings.Contains(errStr, substr) {
|
||||
if !errorContains(validateErr, substr) {
|
||||
t.Errorf("expected error to mention %q, got: %v", substr, validateErr)
|
||||
}
|
||||
}
|
||||
@@ -1469,7 +1483,7 @@ func TestValidate_V2Alpha1_CumulativeEvaluation(t *testing.T) {
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errSubstr)
|
||||
} else if !strings.Contains(err.Error(), tt.errSubstr) {
|
||||
} else if !errorContains(err, tt.errSubstr) {
|
||||
t.Errorf("expected error containing %q, got: %v", tt.errSubstr, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user