Compare commits

..

15 Commits

Author SHA1 Message Date
nityanandagohain
c946cfbdfd Merge remote-tracking branch 'origin/main' into issue_4360 2026-04-24 15:51:44 +05:30
nityanandagohain
32391acfc8 fix: correct table name 2026-04-24 15:47:51 +05:30
nityanandagohain
437ce412c6 fix: use mustnewuuid 2026-04-24 00:19:02 +05:30
nityanandagohain
040872fa41 fix: address comments 2026-04-23 19:08:42 +05:30
nityanandagohain
c28f6cd1a3 fix: types 2026-04-23 16:51:30 +05:30
nityanandagohain
70e37817b9 fix: remove nullable 2026-04-23 16:41:26 +05:30
nityanandagohain
c5645c38c4 Merge remote-tracking branch 'origin/main' into issue_4360 2026-04-23 16:37:17 +05:30
nityanandagohain
5c7c262d4e fix: address comments 2026-04-23 16:34:45 +05:30
nityanandagohain
07cb56c548 fix: new updates 2026-04-22 22:27:24 +05:30
nityanandagohain
6e382aa363 fix: more changes 2026-04-22 14:02:44 +05:30
nityanandagohain
115ee70a9a fix: minor changes 2026-04-22 00:00:51 +05:30
Nityananda Gohain
a58a3d4a68 Merge branch 'main' into issue_4360 2026-04-20 17:54:38 +05:30
nityanandagohain
6899eb0124 fix: changes 2026-04-20 17:53:42 +05:30
nityanandagohain
de5bec0195 Merge remote-tracking branch 'origin/main' into issue_4360 2026-04-20 16:41:58 +05:30
nityanandagohain
e359b03c25 feat: 1.Types for ai-o11y ricing rules 2026-04-12 17:14:18 +05:30
31 changed files with 1514 additions and 293 deletions

View File

@@ -2508,6 +2508,155 @@ components:
- list
- grouped_list
type: string
LlmpricingruletypesGettablePricingRules:
properties:
items:
items:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
total:
type: integer
required:
- items
- total
- offset
- limit
type: object
LlmpricingruletypesLLMPricingRule:
properties:
cacheMode:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
costCacheRead:
format: double
type: number
costCacheWrite:
format: double
type: number
costInput:
format: double
type: number
costOutput:
format: double
type: number
createdAt:
format: date-time
type: string
createdBy:
type: string
enabled:
type: boolean
id:
type: string
isOverride:
type: boolean
modelName:
type: string
modelPattern:
items:
type: string
nullable: true
type: array
orgId:
type: string
sourceId:
type: string
syncedAt:
format: date-time
nullable: true
type: string
unit:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- orgId
- modelName
- modelPattern
- unit
- cacheMode
- costInput
- costOutput
- costCacheRead
- costCacheWrite
- isOverride
- enabled
type: object
LlmpricingruletypesLLMPricingRuleCacheMode:
enum:
- subtract
- additive
- unknown
type: string
LlmpricingruletypesLLMPricingRuleUnit:
enum:
- per_million_tokens
type: string
LlmpricingruletypesUpdatableLLMPricingRule:
properties:
cacheMode:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
costCacheRead:
format: double
type: number
costCacheWrite:
format: double
type: number
costInput:
format: double
type: number
costOutput:
format: double
type: number
enabled:
type: boolean
id:
nullable: true
type: string
isOverride:
nullable: true
type: boolean
modelName:
type: string
modelPattern:
items:
type: string
nullable: true
type: array
sourceId:
nullable: true
type: string
unit:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
required:
- modelName
- modelPattern
- unit
- cacheMode
- costInput
- costOutput
- costCacheRead
- costCacheWrite
- enabled
type: object
LlmpricingruletypesUpdatableLLMPricingRules:
properties:
rules:
items:
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRule'
nullable: true
type: array
required:
- rules
type: object
MetricsexplorertypesInspectMetricsRequest:
properties:
end:
@@ -7332,6 +7481,218 @@ paths:
summary: Create bulk invite
tags:
- users
/api/v1/llm_pricing_rules:
get:
deprecated: false
description: Returns all LLM pricing rules for the authenticated org, with pagination.
operationId: ListLLMPricingRules
parameters:
- in: query
name: offset
schema:
type: integer
- in: query
name: limit
schema:
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesGettablePricingRules'
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 pricing rules
tags:
- llmpricingrules
put:
deprecated: false
description: Single write endpoint used by both the user and the Zeus sync job.
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
are fully preserved when the request does not provide isOverride; only synced_at
is stamped.
operationId: UpdateLLMPricingRules
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRules'
responses:
"204":
description: No Content
"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:
- ADMIN
- tokenizer:
- ADMIN
summary: Bulk update pricing rules
tags:
- llmpricingrules
/api/v1/llm_pricing_rules/{id}:
delete:
deprecated: false
description: Hard-deletes a pricing rule. If auto-synced, it will be recreated
on the next sync cycle.
operationId: DeleteLLMPricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete a pricing rule
tags:
- llmpricingrules
get:
deprecated: false
description: Returns a single LLM pricing rule by ID.
operationId: GetLLMPricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get a pricing rule
tags:
- llmpricingrules
/api/v1/logs/promote_paths:
get:
deprecated: false

View File

@@ -0,0 +1,398 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
DeleteLLMPricingRulePathParameters,
GetLLMPricingRule200,
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns all LLM pricing rules for the authenticated org, with pagination.
* @summary List pricing rules
*/
export const listLLMPricingRules = (
params?: ListLLMPricingRulesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListLLMPricingRules200>({
url: `/api/v1/llm_pricing_rules`,
method: 'GET',
params,
signal,
});
};
export const getListLLMPricingRulesQueryKey = (
params?: ListLLMPricingRulesParams,
) => {
return [`/api/v1/llm_pricing_rules`, ...(params ? [params] : [])] as const;
};
export const getListLLMPricingRulesQueryOptions = <
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListLLMPricingRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListLLMPricingRulesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listLLMPricingRules>>
> = ({ signal }) => listLLMPricingRules(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListLLMPricingRulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listLLMPricingRules>>
>;
export type ListLLMPricingRulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List pricing rules
*/
export function useListLLMPricingRules<
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListLLMPricingRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLLMPricingRules>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListLLMPricingRulesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List pricing rules
*/
export const invalidateListLLMPricingRules = async (
queryClient: QueryClient,
params?: ListLLMPricingRulesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListLLMPricingRulesQueryKey(params) },
options,
);
return queryClient;
};
/**
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
* @summary Bulk update pricing rules
*/
export const updateLLMPricingRules = (
llmpricingruletypesUpdatableLLMPricingRulesDTO: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/llm_pricing_rules`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: llmpricingruletypesUpdatableLLMPricingRulesDTO,
});
};
export const getUpdateLLMPricingRulesMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
> => {
const mutationKey = ['updateLLMPricingRules'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateLLMPricingRules>>,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> }
> = (props) => {
const { data } = props ?? {};
return updateLLMPricingRules(data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateLLMPricingRulesMutationResult = NonNullable<
Awaited<ReturnType<typeof updateLLMPricingRules>>
>;
export type UpdateLLMPricingRulesMutationBody =
BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>;
export type UpdateLLMPricingRulesMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Bulk update pricing rules
*/
export const useUpdateLLMPricingRules = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateLLMPricingRules>>,
TError,
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
TContext
> => {
const mutationOptions = getUpdateLLMPricingRulesMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.
* @summary Delete a pricing rule
*/
export const deleteLLMPricingRule = ({
id,
}: DeleteLLMPricingRulePathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/llm_pricing_rules/${id}`,
method: 'DELETE',
});
};
export const getDeleteLLMPricingRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
> => {
const mutationKey = ['deleteLLMPricingRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
{ pathParams: DeleteLLMPricingRulePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteLLMPricingRule(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteLLMPricingRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteLLMPricingRule>>
>;
export type DeleteLLMPricingRuleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a pricing rule
*/
export const useDeleteLLMPricingRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
TError,
{ pathParams: DeleteLLMPricingRulePathParameters },
TContext
> => {
const mutationOptions = getDeleteLLMPricingRuleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns a single LLM pricing rule by ID.
* @summary Get a pricing rule
*/
export const getLLMPricingRule = (
{ id }: GetLLMPricingRulePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetLLMPricingRule200>({
url: `/api/v1/llm_pricing_rules/${id}`,
method: 'GET',
signal,
});
};
export const getGetLLMPricingRuleQueryKey = ({
id,
}: GetLLMPricingRulePathParameters) => {
return [`/api/v1/llm_pricing_rules/${id}`] as const;
};
export const getGetLLMPricingRuleQueryOptions = <
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLLMPricingRulePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetLLMPricingRuleQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getLLMPricingRule>>
> = ({ signal }) => getLLMPricingRule({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetLLMPricingRuleQueryResult = NonNullable<
Awaited<ReturnType<typeof getLLMPricingRule>>
>;
export type GetLLMPricingRuleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a pricing rule
*/
export function useGetLLMPricingRule<
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLLMPricingRulePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetLLMPricingRuleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get a pricing rule
*/
export const invalidateGetLLMPricingRule = async (
queryClient: QueryClient,
{ id }: GetLLMPricingRulePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetLLMPricingRuleQueryKey({ id }) },
options,
);
return queryClient;
};

View File

@@ -3278,6 +3278,173 @@ export enum InframonitoringtypesResponseTypeDTO {
list = 'list',
grouped_list = 'grouped_list',
}
export interface LlmpricingruletypesGettablePricingRulesDTO {
/**
* @type array
* @nullable true
*/
items: LlmpricingruletypesLLMPricingRuleDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset: number;
/**
* @type integer
*/
total: number;
}
export interface LlmpricingruletypesLLMPricingRuleDTO {
cacheMode: LlmpricingruletypesLLMPricingRuleCacheModeDTO;
/**
* @type number
* @format double
*/
costCacheRead: number;
/**
* @type number
* @format double
*/
costCacheWrite: number;
/**
* @type number
* @format double
*/
costInput: number;
/**
* @type number
* @format double
*/
costOutput: number;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isOverride: boolean;
/**
* @type string
*/
modelName: string;
/**
* @type array
* @nullable true
*/
modelPattern: string[] | null;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
sourceId?: string;
/**
* @type string
* @format date-time
* @nullable true
*/
syncedAt?: Date | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export enum LlmpricingruletypesLLMPricingRuleCacheModeDTO {
subtract = 'subtract',
additive = 'additive',
unknown = 'unknown',
}
export enum LlmpricingruletypesLLMPricingRuleUnitDTO {
per_million_tokens = 'per_million_tokens',
}
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
cacheMode: LlmpricingruletypesLLMPricingRuleCacheModeDTO;
/**
* @type number
* @format double
*/
costCacheRead: number;
/**
* @type number
* @format double
*/
costCacheWrite: number;
/**
* @type number
* @format double
*/
costInput: number;
/**
* @type number
* @format double
*/
costOutput: number;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
* @nullable true
*/
id?: string | null;
/**
* @type boolean
* @nullable true
*/
isOverride?: boolean | null;
/**
* @type string
*/
modelName: string;
/**
* @type array
* @nullable true
*/
modelPattern: string[] | null;
/**
* @type string
* @nullable true
*/
sourceId?: string | null;
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
}
export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
/**
* @type array
* @nullable true
*/
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
@@ -6654,6 +6821,41 @@ export type CreateInvite201 = {
status: string;
};
export type ListLLMPricingRulesParams = {
/**
* @type integer
* @description undefined
*/
offset?: number;
/**
* @type integer
* @description undefined
*/
limit?: number;
};
export type ListLLMPricingRules200 = {
data: LlmpricingruletypesGettablePricingRulesDTO;
/**
* @type string
*/
status: string;
};
export type DeleteLLMPricingRulePathParameters = {
id: string;
};
export type GetLLMPricingRulePathParameters = {
id: string;
};
export type GetLLMPricingRule200 = {
data: LlmpricingruletypesLLMPricingRuleDTO;
/**
* @type string
*/
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
/**
* @type array

View File

@@ -33,7 +33,6 @@ export default function ChartWrapper({
children,
layoutChildren,
yAxisUnit,
groupBy,
customTooltip,
pinnedTooltipElement,
'data-testid': testId,
@@ -69,9 +68,8 @@ export default function ChartWrapper({
const syncMetadata = useMemo(
() => ({
yAxisUnit,
groupBy,
}),
[yAxisUnit, groupBy],
[yAxisUnit],
);
return (

View File

@@ -6,7 +6,6 @@ import {
DashboardCursorSync,
TooltipClickData,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
interface BaseChartProps {
width: number;
@@ -39,7 +38,6 @@ interface UPlotBasedChartProps {
interface UPlotChartDataProps {
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
groupBy?: BaseAutocompleteData[];
}
export interface TimeSeriesChartProps

View File

@@ -113,10 +113,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
uPlotRef.current = plot;
}, []);
const groupBy = useMemo(() => {
return widget.query.builder.queryData[0].groupBy;
}, [widget.query]);
return (
<div className="panel-container" ref={graphRef}>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
@@ -132,7 +128,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
groupBy={groupBy}
isStackedBarChart={widget.stackedBarChart ?? false}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}

View File

@@ -105,7 +105,6 @@ export function prepareBarPanelConfig({
colorMapping: widget.customLegendColors ?? {},
isDarkMode,
stepInterval: currentStepInterval,
metric: series.metric,
});
});

View File

@@ -104,10 +104,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
widget.decimalPrecision,
]);
const groupBy = useMemo(() => {
return widget.query.builder.queryData[0].groupBy;
}, [widget.query]);
return (
<div className="panel-container" ref={graphRef}>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
@@ -121,7 +117,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
data={chartData as uPlot.AlignedData}
groupBy={groupBy}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}

View File

@@ -131,7 +131,6 @@ export const prepareUPlotConfig = ({
pointSize: 5,
fillMode: widget.fillMode || FillMode.None,
isDarkMode,
metric: series.metric,
});
});

View File

@@ -16,7 +16,6 @@ export default function BarChartTooltip(props: BarTooltipProps): JSX.Element {
yAxisUnit: props.yAxisUnit ?? '',
decimalPrecision: props.decimalPrecision,
isStackedBarChart: props.isStackedBarChart,
syncedSeriesIndexes: props.syncedSeriesIndexes,
}),
[
props.uPlotInstance,
@@ -25,7 +24,6 @@ export default function BarChartTooltip(props: BarTooltipProps): JSX.Element {
props.yAxisUnit,
props.decimalPrecision,
props.isStackedBarChart,
props.syncedSeriesIndexes,
],
);

View File

@@ -17,7 +17,6 @@ export default function HistogramTooltip(
uPlotInstance: props.uPlotInstance,
yAxisUnit: props.yAxisUnit ?? '',
decimalPrecision: props.decimalPrecision,
syncedSeriesIndexes: props.syncedSeriesIndexes,
}),
[
props.uPlotInstance,
@@ -25,7 +24,6 @@ export default function HistogramTooltip(
props.dataIndexes,
props.yAxisUnit,
props.decimalPrecision,
props.syncedSeriesIndexes,
],
);

View File

@@ -17,7 +17,6 @@ export default function TimeSeriesTooltip(
uPlotInstance: props.uPlotInstance,
yAxisUnit: props.yAxisUnit ?? '',
decimalPrecision: props.decimalPrecision,
syncedSeriesIndexes: props.syncedSeriesIndexes,
}),
[
props.uPlotInstance,
@@ -25,7 +24,6 @@ export default function TimeSeriesTooltip(
props.dataIndexes,
props.yAxisUnit,
props.decimalPrecision,
props.syncedSeriesIndexes,
],
);

View File

@@ -62,7 +62,6 @@ export function buildTooltipContent({
yAxisUnit,
decimalPrecision,
isStackedBarChart,
syncedSeriesIndexes,
}: {
data: AlignedData;
series: Series[];
@@ -72,34 +71,18 @@ export function buildTooltipContent({
yAxisUnit: string;
decimalPrecision?: PrecisionOption;
isStackedBarChart?: boolean;
syncedSeriesIndexes?: number[] | null;
}): TooltipContentItem[] {
const items: TooltipContentItem[] = [];
const allowedIndexes =
syncedSeriesIndexes != null ? new Set(syncedSeriesIndexes) : null;
for (let seriesIndex = 1; seriesIndex < series.length; seriesIndex += 1) {
const seriesItem = series[seriesIndex];
if (!seriesItem?.show) {
continue;
}
if (allowedIndexes != null && !allowedIndexes.has(seriesIndex)) {
continue;
}
const dataIndex = dataIndexes[seriesIndex];
const isSync = allowedIndexes != null;
// Skip series with no data at the current cursor position
if (dataIndex === null) {
if (isSync) {
items.push({
label: String(seriesItem.label ?? ''),
value: 0,
tooltipValue: 'No Data',
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
isActive: false,
});
}
continue;
}
@@ -119,14 +102,6 @@ export function buildTooltipContent({
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
isActive: seriesIndex === activeSeriesIndex,
});
} else if (isSync) {
items.push({
label: String(seriesItem.label ?? ''),
value: 0,
tooltipValue: 'No Data',
color: resolveSeriesColor(seriesItem.stroke, uPlotInstance, seriesIndex),
isActive: false,
});
}
}

View File

@@ -58,9 +58,6 @@ export interface TooltipRenderArgs {
isPinned: boolean;
dismiss: () => void;
viaSync: boolean;
/** In Tooltip sync mode, limits which series are rendered in the receiver tooltip.
* null = no filtering; [] = no matches (tooltip hidden upstream); [...] = allowed indexes */
syncedSeriesIndexes?: number[] | null;
}
export interface BaseTooltipProps {

View File

@@ -9,7 +9,6 @@ import {
BarAlignment,
ConfigBuilder,
DrawStyle,
ExtendedSeries,
FillMode,
LineInterpolation,
LineStyle,
@@ -28,10 +27,7 @@ let builders: PathBuilders | null = null;
const DEFAULT_LINE_WIDTH = 2;
export const POINT_SIZE_FACTOR = 2.5;
export class UPlotSeriesBuilder extends ConfigBuilder<
SeriesProps,
ExtendedSeries
> {
export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
constructor(props: SeriesProps) {
super(props);
const pathBuilders = uPlot.paths;
@@ -209,8 +205,8 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
);
}
getConfig(): ExtendedSeries {
const { scaleKey, label, spanGaps, show = true, metric } = this.props;
getConfig(): Series {
const { scaleKey, label, spanGaps, show = true } = this.props;
const resolvedLineColor = this.getLineColor();
@@ -237,7 +233,6 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
...lineConfig,
...pathConfig,
points: Object.keys(pointsConfig).length > 0 ? pointsConfig : undefined,
metric,
};
}
}

View File

@@ -171,10 +171,6 @@ export enum FillMode {
None = 'none',
}
export type ExtendedSeries = Series & {
metric?: { [key: string]: string };
};
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
scaleKey: string;
label?: string;
@@ -198,7 +194,6 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
fillMode?: FillMode;
isDarkMode?: boolean;
stepInterval?: number;
metric?: { [key: string]: string };
}
export interface LegendItem {

View File

@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
import cx from 'classnames';
import uPlot from 'uplot';
import { createSyncDisplayHook } from './syncDisplayHook';
import { syncCursorRegistry } from './syncCursorRegistry';
import {
createInitialControllerState,
createSetCursorHandler,
@@ -104,16 +104,32 @@ export default function TooltipPlugin({
// Enable uPlot's built-in cursor sync when requested so that
// crosshair / tooltip can follow the dashboard-wide cursor.
let removeSyncDisplayHook: (() => void) | null = null;
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
config.setCursor({
sync: { key: syncKey, scales: ['x', 'y'] },
});
removeSyncDisplayHook = config.addHook(
'setCursor',
createSyncDisplayHook(syncKey, syncMetadata, controller),
);
// Show the horizontal crosshair only when the receiving panel shares
// the same y-axis unit as the source panel. When this panel is the
// source (cursor.event != null) the line is always shown and this
// panel's metadata is written to the registry so receivers can read it.
config.addHook('setCursor', (u: uPlot): void => {
const yCursorEl = u.root.querySelector<HTMLElement>('.u-cursor-y');
if (!yCursorEl) {
return;
}
if (u.cursor.event != null) {
// This panel is the source — publish metadata and always show line.
syncCursorRegistry.setMetadata(syncKey, syncMetadata);
yCursorEl.style.display = '';
} else {
// This panel is receiving sync — show only if units match.
const sourceMeta = syncCursorRegistry.getMetadata(syncKey);
yCursorEl.style.display =
sourceMeta?.yAxisUnit === syncMetadata?.yAxisUnit ? '' : 'none';
}
});
}
// Dismiss the tooltip when the user clicks / presses a key
@@ -121,12 +137,7 @@ export default function TooltipPlugin({
const onOutsideInteraction = (event: Event): void => {
const target = event.target as Node;
if (!containerRef.current?.contains(target)) {
// Don't dismiss if the click landed inside any other pinned tooltip.
const isInsideAnyPinnedTooltip =
(target as Element).closest?.('[data-pinned="true"]') != null;
if (!isInsideAnyPinnedTooltip) {
dismissTooltip();
}
dismissTooltip();
}
};
@@ -145,7 +156,7 @@ export default function TooltipPlugin({
function updateCursorLock(): void {
const plot = getPlot(controller);
if (plot) {
// @ts-expect-error uPlot cursor lock is not working as expected
// @ts-ignore uPlot cursor lock is not working as expected
plot.cursor._lock = controller.pinned;
}
}
@@ -192,16 +203,6 @@ export default function TooltipPlugin({
if (!controller.hoverActive || !plot) {
return null;
}
// In Tooltip sync mode, suppress the receiver tooltip entirely when
// no receiver series match the source panel's focused series.
if (
syncTooltipWithDashboard &&
controller.cursorDrivenBySync &&
Array.isArray(controller.syncedSeriesIndexes) &&
controller.syncedSeriesIndexes.length === 0
) {
return null;
}
return renderRef.current({
uPlotInstance: plot,
dataIndexes: controller.seriesIndexes,
@@ -209,7 +210,6 @@ export default function TooltipPlugin({
isPinned: controller.pinned,
dismiss: dismissTooltip,
viaSync: controller.cursorDrivenBySync,
syncedSeriesIndexes: controller.syncedSeriesIndexes,
});
}
@@ -431,7 +431,6 @@ export default function TooltipPlugin({
removeSetSeriesHook();
removeSetLegendHook();
removeSetCursorHook();
removeSyncDisplayHook?.();
if (overClickHandler) {
const plot = getPlot(controller);
plot?.over.removeEventListener('click', overClickHandler);
@@ -494,7 +493,7 @@ export default function TooltipPlugin({
isHovering,
contents,
]);
const isTooltipVisible = tooltipBody != null;
const isTooltipVisible = isHovering || tooltipBody != null;
if (!hasPlot) {
return null;

View File

@@ -9,13 +9,9 @@ import type { TooltipSyncMetadata } from './types';
*
* Receivers use this to make decisions such as:
* - Whether to show the horizontal crosshair line (matching yAxisUnit)
* - Which series to highlight when panels share the same groupBy
* - Future: what to render inside the tooltip (matching groupBy, etc.)
*/
const metadataBySyncKey = new Map<string, TooltipSyncMetadata | undefined>();
const activeSeriesMetricBySyncKey = new Map<
string,
Record<string, string> | null
>();
export const syncCursorRegistry = {
setMetadata(syncKey: string, metadata: TooltipSyncMetadata | undefined): void {
@@ -25,15 +21,4 @@ export const syncCursorRegistry = {
getMetadata(syncKey: string): TooltipSyncMetadata | undefined {
return metadataBySyncKey.get(syncKey);
},
setActiveSeriesMetric(
syncKey: string,
metric: Record<string, string> | null,
): void {
activeSeriesMetricBySyncKey.set(syncKey, metric);
},
getActiveSeriesMetric(syncKey: string): Record<string, string> | null {
return activeSeriesMetricBySyncKey.get(syncKey) ?? null;
},
};

View File

@@ -1,175 +0,0 @@
import uPlot from 'uplot';
import type { ExtendedSeries } from '../../config/types';
import { syncCursorRegistry } from './syncCursorRegistry';
import type { TooltipControllerState, TooltipSyncMetadata } from './types';
/**
* Returns the dimension keys present in both groupBy arrays.
* An empty result means no overlap — series highlighting should not run.
*
* exact [A, B] vs [A, B] → [A, B] one match
* subset [A] vs [A, B] → [A] multiple receiver series may match
* superset [A, B] vs [A] → [A] one receiver series matches
* partial [A, B] vs [B, C] → [B]
*/
function getCommonGroupByKeys(
a: TooltipSyncMetadata['groupBy'],
b: TooltipSyncMetadata['groupBy'],
): string[] {
if (!Array.isArray(a) || a.length === 0 || !Array.isArray(b) || b.length === 0) {
return [];
}
const bKeys = new Set(b.map((g) => g.key));
return a.filter((g) => bKeys.has(g.key)).map((g) => g.key);
}
/**
* Returns the 1-based indexes of every series whose metric matches
* sourceMetric on all commonKeys.
*/
function findMatchingSeriesIndexes(
series: uPlot.Series[],
sourceMetric: Record<string, string>,
commonKeys: string[],
): number[] {
return series.reduce<number[]>((acc, s, i) => {
if (i === 0) {return acc;}
const metric = (s as ExtendedSeries).metric;
if (
metric != null &&
commonKeys.every((key) => metric[key] === sourceMetric[key])
) {
acc.push(i);
}
return acc;
}, []);
}
function applySourceSync({
uPlotInstance,
syncKey,
syncMetadata,
focusedSeriesIndex,
}: {
uPlotInstance: uPlot;
syncKey: string;
syncMetadata: TooltipSyncMetadata | undefined;
focusedSeriesIndex: number | null;
}): void {
syncCursorRegistry.setMetadata(syncKey, syncMetadata);
const focusedSeries =
focusedSeriesIndex != null
? (uPlotInstance.series[focusedSeriesIndex] as ExtendedSeries)
: null;
syncCursorRegistry.setActiveSeriesMetric(syncKey, focusedSeries?.metric ?? null);
}
/**
* Returns:
* null no groupBy filtering configured or cursor off-chart (no-op for tooltip)
* [] groupBy configured but no receiver series match the source (hide synced tooltip)
* number[] 1-based indexes of matching receiver series (show only these)
*/
function applyReceiverSync({
uPlotInstance,
yCrosshairEl,
syncKey,
syncMetadata,
sourceMetadata,
commonKeys,
}: {
uPlotInstance: uPlot;
yCrosshairEl: HTMLElement;
syncKey: string;
syncMetadata: TooltipSyncMetadata | undefined;
sourceMetadata: TooltipSyncMetadata | undefined;
commonKeys: string[];
}): number[] | null {
yCrosshairEl.style.display =
sourceMetadata?.yAxisUnit === syncMetadata?.yAxisUnit ? '' : 'none';
if (commonKeys.length === 0) {
return null;
}
if ((uPlotInstance.cursor.left ?? -1) < 0) {
uPlotInstance.setSeries(null, { focus: false });
return null;
}
const sourceSeriesMetric = syncCursorRegistry.getActiveSeriesMetric(syncKey);
if (sourceSeriesMetric == null) {
uPlotInstance.setSeries(null, { focus: false });
return [];
}
const matchingIdxs = findMatchingSeriesIndexes(
uPlotInstance.series,
sourceSeriesMetric,
commonKeys,
);
if (matchingIdxs.length === 0) {
uPlotInstance.setSeries(null, { focus: false });
return [];
}
uPlotInstance.setSeries(matchingIdxs[0], { focus: true });
return matchingIdxs;
}
export function createSyncDisplayHook(
syncKey: string,
syncMetadata: TooltipSyncMetadata | undefined,
controller: TooltipControllerState,
): (u: uPlot) => void {
// Cached once — avoids a DOM query on every cursor move.
let yCrosshairEl: HTMLElement | null = null;
// groupBy on both panels is stable (set at config time). Recompute the
// intersection only when the source panel's groupBy reference changes.
let lastSourceGroupBy: TooltipSyncMetadata['groupBy'];
let cachedCommonKeys: string[] = [];
return (u: uPlot): void => {
yCrosshairEl ??= u.root.querySelector<HTMLElement>('.u-cursor-y');
if (!yCrosshairEl) {
return;
}
if (u.cursor.event != null) {
controller.syncedSeriesIndexes = null;
applySourceSync({
uPlotInstance: u,
syncKey,
syncMetadata,
focusedSeriesIndex: controller.focusedSeriesIndex,
});
yCrosshairEl.style.display = '';
return;
}
// Read metadata once and pass it down — avoids a second registry lookup
// inside applyReceiverSync.
const sourceMetadata = syncCursorRegistry.getMetadata(syncKey);
if (sourceMetadata?.groupBy !== lastSourceGroupBy) {
lastSourceGroupBy = sourceMetadata?.groupBy;
cachedCommonKeys = getCommonGroupByKeys(
sourceMetadata?.groupBy,
syncMetadata?.groupBy,
);
}
controller.syncedSeriesIndexes = applyReceiverSync({
uPlotInstance: u,
yCrosshairEl,
syncKey,
syncMetadata,
sourceMetadata,
commonKeys: cachedCommonKeys,
});
};
}

View File

@@ -27,7 +27,6 @@ export function createInitialControllerState(): TooltipControllerState {
verticalOffset: 0,
seriesIndexes: [],
focusedSeriesIndex: null,
syncedSeriesIndexes: null,
cursorDrivenBySync: false,
plotWithinViewport: false,
windowWidth: window.innerWidth - WINDOW_OFFSET,
@@ -185,7 +184,7 @@ export function createSetLegendHandler(
return;
}
const newSeriesIndexes = [...plot.cursor.idxs];
const newSeriesIndexes = plot.cursor.idxs.slice();
const isAnySeriesActive = newSeriesIndexes.some((v, i) => i > 0 && v != null);
const previousCursorDrivenBySync = controller.cursorDrivenBySync;

View File

@@ -4,7 +4,6 @@ import type {
ReactNode,
RefObject,
} from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type uPlot from 'uplot';
import type { TooltipRenderArgs } from '../../components/types';
@@ -40,7 +39,6 @@ export interface TooltipLayoutInfo {
export interface TooltipSyncMetadata {
yAxisUnit?: string;
groupBy?: BaseAutocompleteData[];
}
export interface TooltipPluginProps {
@@ -97,11 +95,6 @@ export interface TooltipControllerState {
verticalOffset: number;
seriesIndexes: Array<number | null>;
focusedSeriesIndex: number | null;
/** Receiver-side series filtering for Tooltip sync mode.
* null = no filtering (source panel or no groupBy configured)
* [] = no matching series found → hide the synced tooltip
* [...] = only these 1-based series indexes should appear in the synced tooltip */
syncedSeriesIndexes: number[] | null;
cursorDrivenBySync: boolean;
plotWithinViewport: boolean;
windowWidth: number;

View File

@@ -0,0 +1,93 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/gorilla/mux"
)
func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.List),
handler.OpenAPIDef{
ID: "ListLLMPricingRules",
Tags: []string{"llmpricingrules"},
Summary: "List pricing rules",
Description: "Returns all LLM pricing rules for the authenticated org, with pagination.",
Request: nil,
RequestContentType: "",
RequestQuery: new(llmpricingruletypes.ListPricingRulesQuery),
Response: new(llmpricingruletypes.GettablePricingRules),
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", handler.New(
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.Update),
handler.OpenAPIDef{
ID: "UpdateLLMPricingRules",
Tags: []string{"llmpricingrules"},
Summary: "Bulk update pricing rules",
Description: "Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.",
Request: new(llmpricingruletypes.UpdatableLLMPricingRules),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.Get),
handler.OpenAPIDef{
ID: "GetLLMPricingRule",
Tags: []string{"llmpricingrules"},
Summary: "Get a pricing rule",
Description: "Returns a single LLM pricing rule by ID.",
Request: nil,
RequestContentType: "",
Response: new(llmpricingruletypes.GettableLLMPricingRule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
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.authZ.AdminAccess(provider.llmPricingRuleHandler.Delete),
handler.OpenAPIDef{
ID: "DeleteLLMPricingRule",
Tags: []string{"llmpricingrules"},
Summary: "Delete a pricing rule",
Description: "Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -16,8 +16,9 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/fields"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
@@ -65,6 +66,7 @@ type provider struct {
alertmanagerHandler alertmanager.Handler
traceDetailHandler tracedetail.Handler
rulerHandler ruler.Handler
llmPricingRuleHandler llmpricingrule.Handler
}
func NewFactory(
@@ -93,6 +95,7 @@ func NewFactory(
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
llmPricingRuleHandler llmpricingrule.Handler,
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
@@ -126,6 +129,7 @@ func NewFactory(
cloudIntegrationHandler,
ruleStateHistoryHandler,
alertmanagerHandler,
llmPricingRuleHandler,
traceDetailHandler,
rulerHandler,
)
@@ -161,6 +165,7 @@ func newProvider(
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
llmPricingRuleHandler llmpricingrule.Handler,
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
) (apiserver.APIServer, error) {
@@ -196,6 +201,7 @@ func newProvider(
alertmanagerHandler: alertmanagerHandler,
traceDetailHandler: traceDetailHandler,
rulerHandler: rulerHandler,
llmPricingRuleHandler: llmPricingRuleHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -304,6 +310,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addLLMPricingRuleRoutes(router); err != nil {
return err
}
if err := provider.addTraceDetailRoutes(router); err != nil {
return err
}

View File

@@ -0,0 +1,158 @@
package impllmpricingrule
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
const maxLimit = 100
type handler struct {
module llmpricingrule.Module
providerSettings factory.ProviderSettings
}
func NewHandler(module llmpricingrule.Module, providerSettings factory.ProviderSettings) llmpricingrule.Handler {
return &handler{module: module, providerSettings: providerSettings}
}
// List handles GET /api/v1/llm_pricing_rules.
func (h *handler) List(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
var q llmpricingruletypes.ListPricingRulesQuery
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
render.Error(rw, err)
return
}
if q.Limit <= 0 {
q.Limit = 20
} else if q.Limit > maxLimit {
q.Limit = maxLimit
}
if q.Offset < 0 {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "offset must be a non-negative integer"))
return
}
rules, total, err := h.module.List(ctx, orgID, q.Offset, q.Limit)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, llmpricingruletypes.NewGettableLLMPricingRulesFromLLMPricingRules(rules, total, q.Offset, q.Limit))
}
// Get handles GET /api/v1/llm_pricing_rules/{id}.
func (h *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, err := ruleIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
rule, err := h.module.Get(ctx, orgID, id)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, rule)
}
func (h *handler) Update(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)
req := new(llmpricingruletypes.UpdatableLLMPricingRules)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
err = h.module.Update(ctx, orgID, claims.Email, req.Rules)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
// 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)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, err := ruleIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
if err := h.module.Delete(ctx, orgID, id); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
// ruleIDFromPath extracts and validates the {id} path variable.
func ruleIDFromPath(r *http.Request) (valuer.UUID, error) {
raw := mux.Vars(r)["id"]
id, err := valuer.NewUUID(raw)
if err != nil {
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "id is not a valid uuid")
}
return id, nil
}

View File

@@ -0,0 +1,24 @@
package llmpricingrule
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error)
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
Update(ctx context.Context, orgID valuer.UUID, userEmail string, rules []llmpricingruletypes.UpdatableLLMPricingRule) (err error)
Delete(ctx context.Context, orgID, id valuer.UUID) error
}
// Handler defines the HTTP handler interface for pricing rule endpoints.
type Handler interface {
List(rw http.ResponseWriter, r *http.Request)
Get(rw http.ResponseWriter, r *http.Request)
Update(rw http.ResponseWriter, r *http.Request)
Delete(rw http.ResponseWriter, r *http.Request)
}

View File

@@ -22,6 +22,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
@@ -74,6 +76,7 @@ type Handlers struct {
AlertmanagerHandler alertmanager.Handler
TraceDetail tracedetail.Handler
RulerHandler ruler.Handler
LLMPricingRuleHandler llmpricingrule.Handler
}
func NewHandlers(
@@ -117,5 +120,6 @@ func NewHandlers(
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
RulerHandler: signozruler.NewHandler(rulerService),
LLMPricingRuleHandler: impllmpricingrule.NewHandler(nil, providerSettings),
}
}

View File

@@ -21,8 +21,9 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/fields"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
@@ -75,6 +76,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ alertmanager.Handler }{},
struct{ llmpricingrule.Handler }{},
struct{ tracedetail.Handler }{},
struct{ ruler.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})

View File

@@ -282,6 +282,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AlertmanagerHandler,
handlers.LLMPricingRuleHandler,
handlers.TraceDetail,
handlers.RulerHandler,
),

View File

@@ -0,0 +1,144 @@
package llmpricingruletypes
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
ErrCodePricingRuleNotFound = errors.MustNewCode("pricing_rule_not_found")
ErrCodePricingRuleInvalidInput = errors.MustNewCode("pricing_rule_invalid_input")
)
type LLMPricingRuleUnit struct {
valuer.String
}
var (
UnitPerMillionTokens = LLMPricingRuleUnit{valuer.NewString("per_million_tokens")}
)
type LLMPricingRuleCacheMode struct {
valuer.String
}
var (
// LLMPricingRuleCacheModeSubtract: cached tokens are inside input_tokens (OpenAI-style).
LLMPricingRuleCacheModeSubtract = LLMPricingRuleCacheMode{valuer.NewString("subtract")}
// LLMPricingRuleCacheModeAdditive: cached tokens are reported separately (Anthropic-style).
LLMPricingRuleCacheModeAdditive = LLMPricingRuleCacheMode{valuer.NewString("additive")}
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown; falls back to subtract.
LLMPricingRuleCacheModeUnknown = LLMPricingRuleCacheMode{valuer.NewString("unknown")}
)
// LLMPricingRule is the domain model for an LLM pricing rule.
// It also doubles as the HTTP response shape; see GettablePricingRule.
type LLMPricingRule struct {
types.TimeAuditable
types.UserAuditable
ID valuer.UUID `json:"id" required:"true"`
OrgID valuer.UUID `json:"orgId" required:"true"`
SourceID *valuer.UUID `json:"sourceId,omitempty"`
Model string `json:"modelName" required:"true"`
ModelPattern []string `json:"modelPattern" required:"true"`
Unit LLMPricingRuleUnit `json:"unit" required:"true"`
CacheMode LLMPricingRuleCacheMode `json:"cacheMode" required:"true"`
CostInput float64 `json:"costInput" required:"true"`
CostOutput float64 `json:"costOutput" required:"true"`
CostCacheRead float64 `json:"costCacheRead" required:"true"`
CostCacheWrite float64 `json:"costCacheWrite" required:"true"`
IsOverride bool `json:"isOverride" required:"true"`
SyncedAt *time.Time `json:"syncedAt,omitempty"`
Enabled bool `json:"enabled" required:"true"`
}
// GettablePricingRule is a type alias for PricingRule — the response shape is
// identical to the core type, so per pkg/types conventions we do not mint a
// separate flavor.
type GettableLLMPricingRule = LLMPricingRule
// UpdatablePricingRule is one entry in the bulk upsert batch.
//
// Identification:
// - ID set → match by id (user editing a known row).
// - SourceID set → match by source_id (Zeus sync, or user editing a Zeus-synced row).
// - neither set → insert a new row with source_id = NULL (user-created custom rule).
//
// IsOverride is a pointer so the caller can distinguish "not sent" from "set to false".
// When IsOverride is nil AND the matched row has is_override = true, the row is fully
// preserved — only synced_at is stamped.
type UpdatableLLMPricingRule struct {
ID *valuer.UUID `json:"id,omitempty"`
SourceID *valuer.UUID `json:"sourceId,omitempty"`
Model string `json:"modelName" required:"true"`
ModelPattern []string `json:"modelPattern" required:"true"`
Unit LLMPricingRuleUnit `json:"unit" required:"true"`
CacheMode LLMPricingRuleCacheMode `json:"cacheMode" required:"true"`
CostInput float64 `json:"costInput" required:"true"`
CostOutput float64 `json:"costOutput" required:"true"`
CostCacheRead float64 `json:"costCacheRead" required:"true"`
CostCacheWrite float64 `json:"costCacheWrite" required:"true"`
IsOverride *bool `json:"isOverride,omitempty"`
Enabled bool `json:"enabled" required:"true"`
}
type UpdatableLLMPricingRules struct {
Rules []UpdatableLLMPricingRule `json:"rules" required:"true"`
}
type ListPricingRulesQuery struct {
Offset int `query:"offset" json:"offset"`
Limit int `query:"limit" json:"limit"`
}
type GettablePricingRules struct {
Items []*GettableLLMPricingRule `json:"items" required:"true"`
Total int `json:"total" required:"true"`
Offset int `json:"offset" required:"true"`
Limit int `json:"limit" required:"true"`
}
func (LLMPricingRuleUnit) Enum() []any {
return []any{UnitPerMillionTokens}
}
func (LLMPricingRuleCacheMode) Enum() []any {
return []any{LLMPricingRuleCacheModeSubtract, LLMPricingRuleCacheModeAdditive, LLMPricingRuleCacheModeUnknown}
}
func NewLLMPricingRuleFromStorable(s *StorableLLMPricingRule) *LLMPricingRule {
pattern := make([]string, len(s.ModelPattern))
copy(pattern, s.ModelPattern)
return &LLMPricingRule{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID,
OrgID: s.OrgID,
SourceID: s.SourceID,
Model: s.Model,
ModelPattern: pattern,
Unit: s.Unit,
CacheMode: s.CacheMode,
CostInput: s.CostInput,
CostOutput: s.CostOutput,
CostCacheRead: s.CostCacheRead,
CostCacheWrite: s.CostCacheWrite,
IsOverride: s.IsOverride,
SyncedAt: s.SyncedAt,
Enabled: s.Enabled,
}
}
func NewGettableLLMPricingRulesFromLLMPricingRules(items []*LLMPricingRule, total, offset, limit int) *GettablePricingRules {
return &GettablePricingRules{
Items: items,
Total: total,
Offset: offset,
Limit: limit,
}
}

View File

@@ -0,0 +1,67 @@
package llmpricingruletypes
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// StringSlice is a []string that is stored as a JSON text column.
// It is compatible with both SQLite and PostgreSQL.
type StringSlice []string
// StorableLLMPricingRule is the bun/DB representation of an LLM pricing rule.
type StorableLLMPricingRule struct {
bun.BaseModel `bun:"table:llm_pricing_rule,alias:llm_pricing_rule"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
SourceID *valuer.UUID `bun:"source_id,type:text"`
Model string `bun:"model,type:text,notnull"`
ModelPattern StringSlice `bun:"model_pattern,type:text,notnull"`
Unit LLMPricingRuleUnit `bun:"unit,type:text,notnull"`
CacheMode LLMPricingRuleCacheMode `bun:"cache_mode,type:text,notnull"`
CostInput float64 `bun:"cost_input,notnull"`
CostOutput float64 `bun:"cost_output,notnull"`
CostCacheRead float64 `bun:"cost_cache_read,notnull"`
CostCacheWrite float64 `bun:"cost_cache_write,notnull"`
// IsOverride marks the row as user-pinned. When true, Zeus skips it entirely.
IsOverride bool `bun:"is_override,notnull,default:false"`
SyncedAt *time.Time `bun:"synced_at"`
Enabled bool `bun:"enabled,notnull,default:true"`
}
func (s StringSlice) Value() (driver.Value, error) {
if s == nil {
return "[]", nil
}
b, err := json.Marshal(s)
if err != nil {
return nil, err
}
return string(b), nil
}
func (s *StringSlice) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case string:
raw = []byte(v)
case []byte:
raw = v
case nil:
*s = nil
return nil
default:
return errors.NewInternalf(errors.CodeInternal, "llmpricingruletypes: cannot scan %T into StringSlice", src)
}
return json.Unmarshal(raw, s)
}

View File

@@ -0,0 +1,16 @@
package llmpricingruletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*StorableLLMPricingRule, int, error)
Get(ctx context.Context, orgID, id valuer.UUID) (*StorableLLMPricingRule, error)
GetBySourceID(ctx context.Context, orgID, sourceID valuer.UUID) (*StorableLLMPricingRule, error)
Create(ctx context.Context, rule *StorableLLMPricingRule) error
Update(ctx context.Context, rule *StorableLLMPricingRule) error
Delete(ctx context.Context, orgID, id valuer.UUID) error
}