From 5ace7951e645dffee9882852a32580c29504234e Mon Sep 17 00:00:00 2001 From: ido Date: Mon, 27 Oct 2025 10:14:09 +0200 Subject: [PATCH 1/2] provider pricing table --- tim-db/gen/db/models.go | 12 + tim-db/gen/db/pricing.sql.go | 220 ++++++++++++++++++ tim-db/gen/schema/schema.mmd | 12 + tim-db/gen/schema/schema.sql | 46 +++- ...1021114255_create_llm_provider_pricing.sql | 27 +++ tim-db/queries/pricing.sql | 41 ++++ tim-db/seeds/llm_pricing.sql | 35 +++ 7 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 tim-db/gen/db/pricing.sql.go create mode 100644 tim-db/migrations/20251021114255_create_llm_provider_pricing.sql create mode 100644 tim-db/queries/pricing.sql create mode 100644 tim-db/seeds/llm_pricing.sql diff --git a/tim-db/gen/db/models.go b/tim-db/gen/db/models.go index 6cc2b51f6..092719d67 100644 --- a/tim-db/gen/db/models.go +++ b/tim-db/gen/db/models.go @@ -555,6 +555,18 @@ type LlmMessageContent struct { UpdateTime pgtype.Timestamptz } +type LlmProviderPricing struct { + ModelID string + ProviderName string + InputPricePer1mTokensMicros int64 + OutputPricePer1mTokensMicros int64 + CachedInputPricePer1mTokensMicros pgtype.Int8 + CachedOutputPricePer1mTokensMicros pgtype.Int8 + Currency string + CreateTime pgtype.Timestamptz + UpdateTime pgtype.Timestamptz +} + type Organization struct { UID uuid.UUID StytchOrganizationID string diff --git a/tim-db/gen/db/pricing.sql.go b/tim-db/gen/db/pricing.sql.go new file mode 100644 index 000000000..609db41be --- /dev/null +++ b/tim-db/gen/db/pricing.sql.go @@ -0,0 +1,220 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: pricing.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countPricing = `-- name: CountPricing :one +SELECT COUNT(*) FROM llm_provider_pricing +` + +func (q *Queries) CountPricing(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countPricing) + var count int64 + err := row.Scan(&count) + return count, err +} + +const createPricing = `-- name: CreatePricing :one +INSERT INTO llm_provider_pricing ( + model_id, provider_name, + input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, + cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros +) VALUES ($1, $2, $3, $4, $5, $6) +RETURNING model_id, provider_name, input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros, currency, create_time, update_time +` + +type CreatePricingParams struct { + ModelID string + ProviderName string + InputPricePer1mTokensMicros int64 + OutputPricePer1mTokensMicros int64 + CachedInputPricePer1mTokensMicros pgtype.Int8 + CachedOutputPricePer1mTokensMicros pgtype.Int8 +} + +func (q *Queries) CreatePricing(ctx context.Context, arg CreatePricingParams) (LlmProviderPricing, error) { + row := q.db.QueryRow(ctx, createPricing, + arg.ModelID, + arg.ProviderName, + arg.InputPricePer1mTokensMicros, + arg.OutputPricePer1mTokensMicros, + arg.CachedInputPricePer1mTokensMicros, + arg.CachedOutputPricePer1mTokensMicros, + ) + var i LlmProviderPricing + err := row.Scan( + &i.ModelID, + &i.ProviderName, + &i.InputPricePer1mTokensMicros, + &i.OutputPricePer1mTokensMicros, + &i.CachedInputPricePer1mTokensMicros, + &i.CachedOutputPricePer1mTokensMicros, + &i.Currency, + &i.CreateTime, + &i.UpdateTime, + ) + return i, err +} + +const deletePricing = `-- name: DeletePricing :exec +DELETE FROM llm_provider_pricing +WHERE model_id = $1 +` + +func (q *Queries) DeletePricing(ctx context.Context, modelID string) error { + _, err := q.db.Exec(ctx, deletePricing, modelID) + return err +} + +const getAllPricing = `-- name: GetAllPricing :many +SELECT model_id, provider_name, input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros, currency, create_time, update_time FROM llm_provider_pricing +ORDER BY provider_name, model_id +` + +func (q *Queries) GetAllPricing(ctx context.Context) ([]LlmProviderPricing, error) { + rows, err := q.db.Query(ctx, getAllPricing) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LlmProviderPricing + for rows.Next() { + var i LlmProviderPricing + if err := rows.Scan( + &i.ModelID, + &i.ProviderName, + &i.InputPricePer1mTokensMicros, + &i.OutputPricePer1mTokensMicros, + &i.CachedInputPricePer1mTokensMicros, + &i.CachedOutputPricePer1mTokensMicros, + &i.Currency, + &i.CreateTime, + &i.UpdateTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPricingForModel = `-- name: GetPricingForModel :one +SELECT model_id, provider_name, input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros, currency, create_time, update_time FROM llm_provider_pricing +WHERE model_id = $1 +` + +func (q *Queries) GetPricingForModel(ctx context.Context, modelID string) (LlmProviderPricing, error) { + row := q.db.QueryRow(ctx, getPricingForModel, modelID) + var i LlmProviderPricing + err := row.Scan( + &i.ModelID, + &i.ProviderName, + &i.InputPricePer1mTokensMicros, + &i.OutputPricePer1mTokensMicros, + &i.CachedInputPricePer1mTokensMicros, + &i.CachedOutputPricePer1mTokensMicros, + &i.Currency, + &i.CreateTime, + &i.UpdateTime, + ) + return i, err +} + +const listPricing = `-- name: ListPricing :many +SELECT model_id, provider_name, input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros, currency, create_time, update_time FROM llm_provider_pricing +WHERE + ($1::timestamptz IS NULL OR + (create_time > $1::timestamptz OR + (create_time = $1::timestamptz AND model_id > $2::varchar))) +ORDER BY create_time ASC, model_id ASC +LIMIT $3::int +` + +type ListPricingParams struct { + CursorCreateTime pgtype.Timestamptz + CursorModelID pgtype.Text + PageLimit int32 +} + +func (q *Queries) ListPricing(ctx context.Context, arg ListPricingParams) ([]LlmProviderPricing, error) { + rows, err := q.db.Query(ctx, listPricing, arg.CursorCreateTime, arg.CursorModelID, arg.PageLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LlmProviderPricing + for rows.Next() { + var i LlmProviderPricing + if err := rows.Scan( + &i.ModelID, + &i.ProviderName, + &i.InputPricePer1mTokensMicros, + &i.OutputPricePer1mTokensMicros, + &i.CachedInputPricePer1mTokensMicros, + &i.CachedOutputPricePer1mTokensMicros, + &i.Currency, + &i.CreateTime, + &i.UpdateTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updatePricing = `-- name: UpdatePricing :one +UPDATE llm_provider_pricing +SET + input_price_per_1m_tokens_micros = $2, + output_price_per_1m_tokens_micros = $3, + cached_input_price_per_1m_tokens_micros = $4, + cached_output_price_per_1m_tokens_micros = $5 +WHERE model_id = $1 +RETURNING model_id, provider_name, input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros, currency, create_time, update_time +` + +type UpdatePricingParams struct { + ModelID string + InputPricePer1mTokensMicros int64 + OutputPricePer1mTokensMicros int64 + CachedInputPricePer1mTokensMicros pgtype.Int8 + CachedOutputPricePer1mTokensMicros pgtype.Int8 +} + +func (q *Queries) UpdatePricing(ctx context.Context, arg UpdatePricingParams) (LlmProviderPricing, error) { + row := q.db.QueryRow(ctx, updatePricing, + arg.ModelID, + arg.InputPricePer1mTokensMicros, + arg.OutputPricePer1mTokensMicros, + arg.CachedInputPricePer1mTokensMicros, + arg.CachedOutputPricePer1mTokensMicros, + ) + var i LlmProviderPricing + err := row.Scan( + &i.ModelID, + &i.ProviderName, + &i.InputPricePer1mTokensMicros, + &i.OutputPricePer1mTokensMicros, + &i.CachedInputPricePer1mTokensMicros, + &i.CachedOutputPricePer1mTokensMicros, + &i.Currency, + &i.CreateTime, + &i.UpdateTime, + ) + return i, err +} diff --git a/tim-db/gen/schema/schema.mmd b/tim-db/gen/schema/schema.mmd index 10ea97722..051e35b26 100644 --- a/tim-db/gen/schema/schema.mmd +++ b/tim-db/gen/schema/schema.mmd @@ -66,6 +66,18 @@ erDiagram timestamp_with_time_zone update_time } + llm_provider_pricing { + bigint cached_input_price_per_1m_tokens_micros + bigint cached_output_price_per_1m_tokens_micros + timestamp_with_time_zone create_time + character_varying currency + bigint input_price_per_1m_tokens_micros + character_varying model_id PK + bigint output_price_per_1m_tokens_micros + character_varying provider_name + timestamp_with_time_zone update_time + } + organization { timestamp_with_time_zone create_time character_varying name diff --git a/tim-db/gen/schema/schema.sql b/tim-db/gen/schema/schema.sql index 155bc30f4..4c93e373e 100644 --- a/tim-db/gen/schema/schema.sql +++ b/tim-db/gen/schema/schema.sql @@ -244,6 +244,27 @@ CREATE TABLE public.llm_message_content ( ); +-- +-- Name: llm_provider_pricing; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.llm_provider_pricing ( + model_id character varying(255) NOT NULL, + provider_name character varying(100) NOT NULL, + input_price_per_1m_tokens_micros bigint NOT NULL, + output_price_per_1m_tokens_micros bigint NOT NULL, + cached_input_price_per_1m_tokens_micros bigint, + cached_output_price_per_1m_tokens_micros bigint, + currency character varying(3) DEFAULT 'USD'::character varying NOT NULL, + create_time timestamp with time zone DEFAULT now() NOT NULL, + update_time timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT llm_provider_pricing_cached_input_price_per_1m_tokens_mic_check CHECK (((cached_input_price_per_1m_tokens_micros IS NULL) OR (cached_input_price_per_1m_tokens_micros >= 0))), + CONSTRAINT llm_provider_pricing_cached_output_price_per_1m_tokens_mi_check CHECK (((cached_output_price_per_1m_tokens_micros IS NULL) OR (cached_output_price_per_1m_tokens_micros >= 0))), + CONSTRAINT llm_provider_pricing_input_price_per_1m_tokens_micros_check CHECK ((input_price_per_1m_tokens_micros >= 0)), + CONSTRAINT llm_provider_pricing_output_price_per_1m_tokens_micros_check CHECK ((output_price_per_1m_tokens_micros >= 0)) +); + + -- -- Name: organization; Type: TABLE; Schema: public; Owner: - -- @@ -518,7 +539,15 @@ ALTER TABLE ONLY public.llm_message -- --- Name: organization_membership org_membership_stytch_member_id_org_key; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: llm_provider_pricing llm_provider_pricing_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.llm_provider_pricing + ADD CONSTRAINT llm_provider_pricing_pkey PRIMARY KEY (model_id); + + +-- +-- Name: organization_membership org_membership_stytch_user_id_org_key; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.organization_membership @@ -827,6 +856,20 @@ CREATE UNIQUE INDEX idx_unique_tool_result_per_message ON public.llm_message_con CREATE INDEX idx_user_status_create_time_uid ON public.users USING btree (status, create_time, uid); +-- +-- Name: llm_message trg_notify_message_completed; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_notify_message_completed AFTER INSERT OR UPDATE ON public.llm_message FOR EACH ROW EXECUTE FUNCTION public.notify_message_completed(); + + +-- +-- Name: llm_provider_pricing trg_update_llm_provider_pricing_update_time; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_update_llm_provider_pricing_update_time BEFORE UPDATE ON public.llm_provider_pricing FOR EACH ROW EXECUTE FUNCTION public.update_update_time_column(); + + -- -- Name: persona_revision trg_update_persona_revision_update_time; Type: TRIGGER; Schema: public; Owner: - -- @@ -1125,6 +1168,7 @@ INSERT INTO public.schema_migrations (version) VALUES ('20251016000000'), ('20251016000001'), ('20251016000002'), + ('20251021114255'), ('20251022000000'), ('20251022190449'), ('20251023194012'), diff --git a/tim-db/migrations/20251021114255_create_llm_provider_pricing.sql b/tim-db/migrations/20251021114255_create_llm_provider_pricing.sql new file mode 100644 index 000000000..e67ab4cd0 --- /dev/null +++ b/tim-db/migrations/20251021114255_create_llm_provider_pricing.sql @@ -0,0 +1,27 @@ +-- migrate:up +CREATE TABLE llm_provider_pricing ( + model_id varchar(255) NOT NULL, + provider_name varchar(100) NOT NULL, + input_price_per_1m_tokens_micros bigint NOT NULL, -- Price in micro-dollars (e.g., $3.00 = 3000000) + output_price_per_1m_tokens_micros bigint NOT NULL, + cached_input_price_per_1m_tokens_micros bigint, + cached_output_price_per_1m_tokens_micros bigint, + currency varchar(3) DEFAULT 'USD' NOT NULL, + create_time timestamp with time zone DEFAULT now() NOT NULL, + update_time timestamp with time zone DEFAULT now() NOT NULL, + + -- Constraints + PRIMARY KEY (model_id), + CHECK (input_price_per_1m_tokens_micros >= 0), + CHECK (output_price_per_1m_tokens_micros >= 0), + CHECK (cached_input_price_per_1m_tokens_micros IS NULL OR cached_input_price_per_1m_tokens_micros >= 0), + CHECK (cached_output_price_per_1m_tokens_micros IS NULL OR cached_output_price_per_1m_tokens_micros >= 0) +); + + +CREATE TRIGGER trg_update_llm_provider_pricing_update_time + BEFORE UPDATE ON llm_provider_pricing + FOR EACH ROW EXECUTE FUNCTION update_update_time_column(); + +-- migrate:down +DROP TABLE IF EXISTS llm_provider_pricing; diff --git a/tim-db/queries/pricing.sql b/tim-db/queries/pricing.sql new file mode 100644 index 000000000..0dade2ba5 --- /dev/null +++ b/tim-db/queries/pricing.sql @@ -0,0 +1,41 @@ +-- name: GetPricingForModel :one +SELECT * FROM llm_provider_pricing +WHERE model_id = $1; + +-- name: GetAllPricing :many +SELECT * FROM llm_provider_pricing +ORDER BY provider_name, model_id; + +-- name: ListPricing :many +SELECT * FROM llm_provider_pricing +WHERE + (sqlc.narg(cursor_create_time)::timestamptz IS NULL OR + (create_time > sqlc.narg(cursor_create_time)::timestamptz OR + (create_time = sqlc.narg(cursor_create_time)::timestamptz AND model_id > sqlc.narg(cursor_model_id)::varchar))) +ORDER BY create_time ASC, model_id ASC +LIMIT sqlc.arg(page_limit)::int; + +-- name: CountPricing :one +SELECT COUNT(*) FROM llm_provider_pricing; + +-- name: CreatePricing :one +INSERT INTO llm_provider_pricing ( + model_id, provider_name, + input_price_per_1m_tokens_micros, output_price_per_1m_tokens_micros, + cached_input_price_per_1m_tokens_micros, cached_output_price_per_1m_tokens_micros +) VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: UpdatePricing :one +UPDATE llm_provider_pricing +SET + input_price_per_1m_tokens_micros = $2, + output_price_per_1m_tokens_micros = $3, + cached_input_price_per_1m_tokens_micros = $4, + cached_output_price_per_1m_tokens_micros = $5 +WHERE model_id = $1 +RETURNING *; + +-- name: DeletePricing :exec +DELETE FROM llm_provider_pricing +WHERE model_id = $1; diff --git a/tim-db/seeds/llm_pricing.sql b/tim-db/seeds/llm_pricing.sql new file mode 100644 index 000000000..10082abca --- /dev/null +++ b/tim-db/seeds/llm_pricing.sql @@ -0,0 +1,35 @@ +-- Sample LLM provider pricing data for testing +-- This would typically be loaded by an admin or pricing sync process + +INSERT INTO llm_provider_pricing ( + model_id, + provider_name, + input_price_per_1m_tokens_micros, + output_price_per_1m_tokens_micros, + cached_input_price_per_1m_tokens_micros, + cached_output_price_per_1m_tokens_micros +) VALUES +-- Anthropic Claude models (prices in micro-dollars: $3.00 = 3000000) +('claude-3-5-sonnet-20241022', 'anthropic', 3000000, 15000000, 300000, 1500000), +('claude-3-5-haiku-20241022', 'anthropic', 1000000, 5000000, 100000, 500000), +('claude-3-opus-20240229', 'anthropic', 15000000, 75000000, 1500000, 7500000), + +-- OpenAI models +('gpt-4o', 'openai', 2500000, 10000000, NULL, NULL), +('gpt-4o-mini', 'openai', 150000, 600000, NULL, NULL), +('gpt-4-turbo', 'openai', 10000000, 30000000, NULL, NULL), + +-- Google models +('gemini-pro-1.5', 'google', 3500000, 10500000, NULL, NULL), +('gemini-pro', 'google', 500000, 1500000, NULL, NULL), + +-- xAI models +('grok-beta', 'xai', 5000000, 15000000, NULL, NULL) + +ON CONFLICT (model_id) DO UPDATE SET + provider_name = EXCLUDED.provider_name, + input_price_per_1m_tokens_micros = EXCLUDED.input_price_per_1m_tokens_micros, + output_price_per_1m_tokens_micros = EXCLUDED.output_price_per_1m_tokens_micros, + cached_input_price_per_1m_tokens_micros = EXCLUDED.cached_input_price_per_1m_tokens_micros, + cached_output_price_per_1m_tokens_micros = EXCLUDED.cached_output_price_per_1m_tokens_micros, + update_time = NOW(); \ No newline at end of file From a3706a2175944b04e4e6442757e940c9a1fbfa21 Mon Sep 17 00:00:00 2001 From: ido Date: Mon, 27 Oct 2025 10:14:43 +0200 Subject: [PATCH 2/2] integrate pricing into worker and report usage --- .../pricing/v1alpha1/pricing_service.proto | 151 +++++ .../api/pricing/v1alpha1/pricing_types.proto | 82 +++ tim-proto/gen/openapi.yaml | 256 ++++++++ .../pricing/v1alpha1/pricing_service.pb.go | 599 ++++++++++++++++++ .../v1alpha1/pricing_service.swagger.json | 425 +++++++++++++ .../api/pricing/v1alpha1/pricing_types.pb.go | 230 +++++++ .../v1alpha1/pricing_types.swagger.json | 46 ++ .../pricing_service.connect.go | 268 ++++++++ tim-worker/internal/apiclient/client.go | 63 ++ .../internal/worker/handle_llm_relay.go | 167 +++++ 10 files changed, 2287 insertions(+) create mode 100644 proto/tim-api/tim/api/pricing/v1alpha1/pricing_service.proto create mode 100644 proto/tim-api/tim/api/pricing/v1alpha1/pricing_types.proto create mode 100644 tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.pb.go create mode 100644 tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.swagger.json create mode 100644 tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.pb.go create mode 100644 tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.swagger.json create mode 100644 tim-proto/gen/tim/api/pricing/v1alpha1/pricingv1alpha1connect/pricing_service.connect.go diff --git a/proto/tim-api/tim/api/pricing/v1alpha1/pricing_service.proto b/proto/tim-api/tim/api/pricing/v1alpha1/pricing_service.proto new file mode 100644 index 000000000..ec832d8a7 --- /dev/null +++ b/proto/tim-api/tim/api/pricing/v1alpha1/pricing_service.proto @@ -0,0 +1,151 @@ +syntax = "proto3"; + +package tim.api.pricing.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "tim/api/pricing/v1alpha1/pricing_types.proto"; + +// PricingService provides operations for managing LLM model pricing +service PricingService { + option (google.api.default_host) = "tim-api.settlerlabs.com"; + + // Get pricing for a specific model + rpc GetModelPricing(GetModelPricingRequest) returns (ModelPricing) { + option (google.api.http) = {get: "/v1alpha1/{name=modelPricing/*}"}; + option (google.api.method_signature) = "name"; + } + + // List all model pricing + rpc ListModelPricing(ListModelPricingRequest) returns (ListModelPricingResponse) { + option (google.api.http) = {get: "/v1alpha1/modelPricing"}; + } + + // Create new model pricing + rpc CreateModelPricing(CreateModelPricingRequest) returns (ModelPricing) { + option (google.api.http) = { + post: "/v1alpha1/modelPricing" + body: "model_pricing" + }; + option (google.api.method_signature) = "model_pricing"; + } + + // Update existing model pricing + rpc UpdateModelPricing(UpdateModelPricingRequest) returns (ModelPricing) { + option (google.api.http) = { + patch: "/v1alpha1/{model_pricing.path=modelPricing/*}" + body: "model_pricing" + }; + option (google.api.method_signature) = "model_pricing"; + } + + // Delete model pricing + rpc DeleteModelPricing(DeleteModelPricingRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/v1alpha1/{name=modelPricing/*}"}; + option (google.api.method_signature) = "name"; + } + + // Get pricing by provider + rpc GetPricingByProvider(GetPricingByProviderRequest) returns (GetPricingByProviderResponse) { + option (google.api.http) = {get: "/v1alpha1/modelPricing:byProvider"}; + option (google.api.method_signature) = "provider_name"; + } +} + +// Request message for GetModelPricing +message GetModelPricingRequest { + // The resource name of the model pricing to retrieve + string name = 1 [ + (google.api.resource_reference) = {type: "tim.settlerlabs.com/model-pricing"}, + (buf.validate.field).required = true, + (buf.validate.field).string.pattern = "^modelPricing/.+$" + ]; +} + +// Request message for ListModelPricing +message ListModelPricingRequest { + // The maximum number of items to return + int32 page_size = 1 [(buf.validate.field).int32 = { + gte: 1 + lte: 1000 + }]; + + // The next_page_token value returned from a previous request, if any + string page_token = 2; + + // Filter by provider name (optional) + string filter_provider = 3 [(buf.validate.field).string.max_len = 100]; +} + +// Response message for ListModelPricing +message ListModelPricingResponse { + // The list of model pricing + repeated ModelPricing model_pricing = 1; + + // Token to retrieve the next page of results, or empty if there are no more results + string next_page_token = 2; + + // Total number of items available + int32 total_size = 3; +} + +// Request message for CreateModelPricing +message CreateModelPricingRequest { + // The model pricing to create + ModelPricing model_pricing = 1 [(buf.validate.field).required = true]; +} + +// Request message for UpdateModelPricing +message UpdateModelPricingRequest { + // The model pricing to update + ModelPricing model_pricing = 1 [(buf.validate.field).required = true]; + + // The list of fields to update + google.protobuf.FieldMask update_mask = 2; +} + +// Request message for DeleteModelPricing +message DeleteModelPricingRequest { + // The resource name of the model pricing to delete + string name = 1 [ + (google.api.resource_reference) = {type: "tim.settlerlabs.com/model-pricing"}, + (buf.validate.field).required = true, + (buf.validate.field).string.pattern = "^modelPricing/.+$" + ]; +} + +// Request message for GetPricingByProvider +message GetPricingByProviderRequest { + // The provider name to filter by + string provider_name = 1 [ + (buf.validate.field).required = true, + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 100 + ]; + + // The maximum number of items to return + int32 page_size = 2 [(buf.validate.field).int32 = { + gte: 1 + lte: 1000 + }]; + + // The next_page_token value returned from a previous request, if any + string page_token = 3; +} + +// Response message for GetPricingByProvider +message GetPricingByProviderResponse { + // The list of model pricing for the provider + repeated ModelPricing model_pricing = 1; + + // Token to retrieve the next page of results, or empty if there are no more results + string next_page_token = 2; + + // Total number of items available for this provider + int32 total_size = 3; +} diff --git a/proto/tim-api/tim/api/pricing/v1alpha1/pricing_types.proto b/proto/tim-api/tim/api/pricing/v1alpha1/pricing_types.proto new file mode 100644 index 000000000..d765fa7aa --- /dev/null +++ b/proto/tim-api/tim/api/pricing/v1alpha1/pricing_types.proto @@ -0,0 +1,82 @@ +syntax = "proto3"; + +package tim.api.pricing.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/timestamp.proto"; + +// ModelPricing represents the pricing information for an LLM model +message ModelPricing { + option (google.api.resource) = { + type: "tim.settlerlabs.com/model-pricing" + pattern: ["modelPricing/{model}"] + plural: "model-pricing" + singular: "model-pricing" + }; + + // The resource path identifier (model ID) + string path = 1 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (buf.validate.field).string.min_len = 1, + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Unique identifier for the model + string model_id = 2 [ + (buf.validate.field).required = true, + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 255 + ]; + + // Name of the LLM provider (e.g., "anthropic", "openai", "google") + string provider_name = 3 [ + (buf.validate.field).required = true, + (buf.validate.field).string.min_len = 1, + (buf.validate.field).string.max_len = 100 + ]; + + // Price per 1 million input tokens + string input_price_per_1m_tokens = 4 [ + (buf.validate.field).required = true, + (buf.validate.field).string.pattern = "^[0-9]+(\\.[0-9]{1,4})?$" + ]; + + // Price per 1 million output tokens + string output_price_per_1m_tokens = 5 [ + (buf.validate.field).required = true, + (buf.validate.field).string.pattern = "^[0-9]+(\\.[0-9]{1,4})?$" + ]; + + // Price per 1 million cached input tokens (optional) + optional string cached_input_price_per_1m_tokens = 6 [ + (buf.validate.field).string.pattern = "^[0-9]+(\\.[0-9]{1,4})?$", + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Price per 1 million cached output tokens (optional) + optional string cached_output_price_per_1m_tokens = 7 [ + (buf.validate.field).string.pattern = "^[0-9]+(\\.[0-9]{1,4})?$", + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE + ]; + + // Currency code (ISO 4217) + string currency = 8 [ + (buf.validate.field).required = true, + (buf.validate.field).string.len = 3, + (buf.validate.field).string.pattern = "^[A-Z]{3}$" + ]; + + // Timestamp when the pricing was created + google.protobuf.Timestamp create_time = 9 [ + (buf.validate.field).required = true, + (google.api.field_behavior) = OUTPUT_ONLY + ]; + + // Timestamp when the pricing was last updated + google.protobuf.Timestamp update_time = 10 [ + (buf.validate.field).required = true, + (google.api.field_behavior) = OUTPUT_ONLY + ]; +} diff --git a/tim-proto/gen/openapi.yaml b/tim-proto/gen/openapi.yaml index e75c88e32..fe3420458 100644 --- a/tim-proto/gen/openapi.yaml +++ b/tim-proto/gen/openapi.yaml @@ -5,6 +5,8 @@ openapi: 3.0.3 info: title: '"TIM API"' version: 0.0.1-alpha.1 +servers: + - url: https://tim-api.settlerlabs.com paths: /v1alpha1/auth/exchangeOauthToken: post: @@ -138,6 +140,187 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /v1alpha1/modelPricing: + get: + tags: + - PricingService + description: List all model pricing + operationId: PricingService_ListModelPricing + parameters: + - name: pageSize + in: query + description: The maximum number of items to return + schema: + type: integer + format: int32 + - name: pageToken + in: query + description: The next_page_token value returned from a previous request, if any + schema: + type: string + - name: filterProvider + in: query + description: Filter by provider name (optional) + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListModelPricingResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + post: + tags: + - PricingService + description: Create new model pricing + operationId: PricingService_CreateModelPricing + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ModelPricing' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ModelPricing' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1alpha1/modelPricing/{modelPricing}: + get: + tags: + - PricingService + description: Get pricing for a specific model + operationId: PricingService_GetModelPricing + parameters: + - name: modelPricing + in: path + description: The modelPricing id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ModelPricing' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + delete: + tags: + - PricingService + description: Delete model pricing + operationId: PricingService_DeleteModelPricing + parameters: + - name: modelPricing + in: path + description: The modelPricing id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + patch: + tags: + - PricingService + description: Update existing model pricing + operationId: PricingService_UpdateModelPricing + parameters: + - name: modelPricing + in: path + description: The modelPricing id. + required: true + schema: + type: string + - name: updateMask + in: query + description: The list of fields to update + schema: + type: string + format: field-mask + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ModelPricing' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ModelPricing' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /v1alpha1/modelPricing:byProvider: + get: + tags: + - PricingService + description: Get pricing by provider + operationId: PricingService_GetPricingByProvider + parameters: + - name: providerName + in: query + description: The provider name to filter by + schema: + type: string + - name: pageSize + in: query + description: The maximum number of items to return + schema: + type: integer + format: int32 + - name: pageToken + in: query + description: The next_page_token value returned from a previous request, if any + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetPricingByProviderResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' /v1alpha1/orgs: get: tags: @@ -2657,6 +2840,22 @@ components: - $ref: '#/components/schemas/Thread' description: The newly created thread description: ForkThreadResponse is the response for forking a thread + GetPricingByProviderResponse: + type: object + properties: + modelPricing: + type: array + items: + $ref: '#/components/schemas/ModelPricing' + description: The list of model pricing for the provider + nextPageToken: + type: string + description: Token to retrieve the next page of results, or empty if there are no more results + totalSize: + type: integer + description: Total number of items available for this provider + format: int32 + description: Response message for GetPricingByProvider GoogleProtobufAny: type: object properties: @@ -2772,6 +2971,22 @@ components: description: The total LLM messages count based on request, this may be an inexact estimate. format: int32 description: ListLlmMessagesResponse is the response for listing LLM messages on a thread + ListModelPricingResponse: + type: object + properties: + modelPricing: + type: array + items: + $ref: '#/components/schemas/ModelPricing' + description: The list of model pricing + nextPageToken: + type: string + description: Token to retrieve the next page of results, or empty if there are no more results + totalSize: + type: integer + description: Total number of items available + format: int32 + description: Response message for ListModelPricing ListOrganizationsResponse: type: object properties: @@ -2990,6 +3205,45 @@ components: description: System set at creation time format: date-time description: LlmMessageContent is a content block of an LLM message. + ModelPricing: + type: object + properties: + path: + readOnly: true + type: string + description: The resource path identifier (model ID) + modelId: + type: string + description: Unique identifier for the model + providerName: + type: string + description: Name of the LLM provider (e.g., "anthropic", "openai", "google") + inputPricePer1mTokens: + type: string + description: Price per 1 million input tokens + outputPricePer1mTokens: + type: string + description: Price per 1 million output tokens + cachedInputPricePer1mTokens: + type: string + description: Price per 1 million cached input tokens (optional) + cachedOutputPricePer1mTokens: + type: string + description: Price per 1 million cached output tokens (optional) + currency: + type: string + description: Currency code (ISO 4217) + createTime: + readOnly: true + type: string + description: Timestamp when the pricing was created + format: date-time + updateTime: + readOnly: true + type: string + description: Timestamp when the pricing was last updated + format: date-time + description: ModelPricing represents the pricing information for an LLM model Organization: type: object properties: @@ -3840,6 +4094,8 @@ tags: description: OrganizationService is the service for managing organizations - name: PersonaService description: PersonaService defines the APIs for managing personas + - name: PricingService + description: PricingService provides operations for managing LLM model pricing - name: ThreadContextService description: ThreadContextService is an internal service for accessing a thread's active context - name: ThreadService diff --git a/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.pb.go b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.pb.go new file mode 100644 index 000000000..781f88ab7 --- /dev/null +++ b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.pb.go @@ -0,0 +1,599 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.9 +// protoc (unknown) +// source: tim/api/pricing/v1alpha1/pricing_service.proto + +package pricingv1alpha1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request message for GetModelPricing +type GetModelPricingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the model pricing to retrieve + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetModelPricingRequest) Reset() { + *x = GetModelPricingRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetModelPricingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetModelPricingRequest) ProtoMessage() {} + +func (x *GetModelPricingRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetModelPricingRequest.ProtoReflect.Descriptor instead. +func (*GetModelPricingRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{0} +} + +func (x *GetModelPricingRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Request message for ListModelPricing +type ListModelPricingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The maximum number of items to return + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // The next_page_token value returned from a previous request, if any + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Filter by provider name (optional) + FilterProvider string `protobuf:"bytes,3,opt,name=filter_provider,json=filterProvider,proto3" json:"filter_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListModelPricingRequest) Reset() { + *x = ListModelPricingRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListModelPricingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListModelPricingRequest) ProtoMessage() {} + +func (x *ListModelPricingRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListModelPricingRequest.ProtoReflect.Descriptor instead. +func (*ListModelPricingRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListModelPricingRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListModelPricingRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListModelPricingRequest) GetFilterProvider() string { + if x != nil { + return x.FilterProvider + } + return "" +} + +// Response message for ListModelPricing +type ListModelPricingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of model pricing + ModelPricing []*ModelPricing `protobuf:"bytes,1,rep,name=model_pricing,json=modelPricing,proto3" json:"model_pricing,omitempty"` + // Token to retrieve the next page of results, or empty if there are no more results + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // Total number of items available + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListModelPricingResponse) Reset() { + *x = ListModelPricingResponse{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListModelPricingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListModelPricingResponse) ProtoMessage() {} + +func (x *ListModelPricingResponse) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListModelPricingResponse.ProtoReflect.Descriptor instead. +func (*ListModelPricingResponse) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListModelPricingResponse) GetModelPricing() []*ModelPricing { + if x != nil { + return x.ModelPricing + } + return nil +} + +func (x *ListModelPricingResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListModelPricingResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +// Request message for CreateModelPricing +type CreateModelPricingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The model pricing to create + ModelPricing *ModelPricing `protobuf:"bytes,1,opt,name=model_pricing,json=modelPricing,proto3" json:"model_pricing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateModelPricingRequest) Reset() { + *x = CreateModelPricingRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateModelPricingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateModelPricingRequest) ProtoMessage() {} + +func (x *CreateModelPricingRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateModelPricingRequest.ProtoReflect.Descriptor instead. +func (*CreateModelPricingRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateModelPricingRequest) GetModelPricing() *ModelPricing { + if x != nil { + return x.ModelPricing + } + return nil +} + +// Request message for UpdateModelPricing +type UpdateModelPricingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The model pricing to update + ModelPricing *ModelPricing `protobuf:"bytes,1,opt,name=model_pricing,json=modelPricing,proto3" json:"model_pricing,omitempty"` + // The list of fields to update + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateModelPricingRequest) Reset() { + *x = UpdateModelPricingRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateModelPricingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateModelPricingRequest) ProtoMessage() {} + +func (x *UpdateModelPricingRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateModelPricingRequest.ProtoReflect.Descriptor instead. +func (*UpdateModelPricingRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateModelPricingRequest) GetModelPricing() *ModelPricing { + if x != nil { + return x.ModelPricing + } + return nil +} + +func (x *UpdateModelPricingRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +// Request message for DeleteModelPricing +type DeleteModelPricingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the model pricing to delete + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteModelPricingRequest) Reset() { + *x = DeleteModelPricingRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteModelPricingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteModelPricingRequest) ProtoMessage() {} + +func (x *DeleteModelPricingRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteModelPricingRequest.ProtoReflect.Descriptor instead. +func (*DeleteModelPricingRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{5} +} + +func (x *DeleteModelPricingRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Request message for GetPricingByProvider +type GetPricingByProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The provider name to filter by + ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // The maximum number of items to return + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // The next_page_token value returned from a previous request, if any + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPricingByProviderRequest) Reset() { + *x = GetPricingByProviderRequest{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPricingByProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPricingByProviderRequest) ProtoMessage() {} + +func (x *GetPricingByProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPricingByProviderRequest.ProtoReflect.Descriptor instead. +func (*GetPricingByProviderRequest) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{6} +} + +func (x *GetPricingByProviderRequest) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *GetPricingByProviderRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *GetPricingByProviderRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// Response message for GetPricingByProvider +type GetPricingByProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of model pricing for the provider + ModelPricing []*ModelPricing `protobuf:"bytes,1,rep,name=model_pricing,json=modelPricing,proto3" json:"model_pricing,omitempty"` + // Token to retrieve the next page of results, or empty if there are no more results + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // Total number of items available for this provider + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPricingByProviderResponse) Reset() { + *x = GetPricingByProviderResponse{} + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPricingByProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPricingByProviderResponse) ProtoMessage() {} + +func (x *GetPricingByProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPricingByProviderResponse.ProtoReflect.Descriptor instead. +func (*GetPricingByProviderResponse) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP(), []int{7} +} + +func (x *GetPricingByProviderResponse) GetModelPricing() []*ModelPricing { + if x != nil { + return x.ModelPricing + } + return nil +} + +func (x *GetPricingByProviderResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *GetPricingByProviderResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +var File_tim_api_pricing_v1alpha1_pricing_service_proto protoreflect.FileDescriptor + +const file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDesc = "" + + "\n" + + ".tim/api/pricing/v1alpha1/pricing_service.proto\x12\x18tim.api.pricing.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a,tim/api/pricing/v1alpha1/pricing_types.proto\"o\n" + + "\x16GetModelPricingRequest\x12U\n" + + "\x04name\x18\x01 \x01(\tBA\xfaA#\n" + + "!tim.settlerlabs.com/model-pricing\xbaH\x18\xc8\x01\x01r\x132\x11^modelPricing/.+$R\x04name\"\x93\x01\n" + + "\x17ListModelPricingRequest\x12'\n" + + "\tpage_size\x18\x01 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xe8\a(\x01R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x120\n" + + "\x0ffilter_provider\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x18dR\x0efilterProvider\"\xae\x01\n" + + "\x18ListModelPricingResponse\x12K\n" + + "\rmodel_pricing\x18\x01 \x03(\v2&.tim.api.pricing.v1alpha1.ModelPricingR\fmodelPricing\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"p\n" + + "\x19CreateModelPricingRequest\x12S\n" + + "\rmodel_pricing\x18\x01 \x01(\v2&.tim.api.pricing.v1alpha1.ModelPricingB\x06\xbaH\x03\xc8\x01\x01R\fmodelPricing\"\xad\x01\n" + + "\x19UpdateModelPricingRequest\x12S\n" + + "\rmodel_pricing\x18\x01 \x01(\v2&.tim.api.pricing.v1alpha1.ModelPricingB\x06\xbaH\x03\xc8\x01\x01R\fmodelPricing\x12;\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" + + "updateMask\"r\n" + + "\x19DeleteModelPricingRequest\x12U\n" + + "\x04name\x18\x01 \x01(\tBA\xfaA#\n" + + "!tim.settlerlabs.com/model-pricing\xbaH\x18\xc8\x01\x01r\x132\x11^modelPricing/.+$R\x04name\"\x98\x01\n" + + "\x1bGetPricingByProviderRequest\x121\n" + + "\rprovider_name\x18\x01 \x01(\tB\f\xbaH\t\xc8\x01\x01r\x04\x10\x01\x18dR\fproviderName\x12'\n" + + "\tpage_size\x18\x02 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xe8\a(\x01R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\xb2\x01\n" + + "\x1cGetPricingByProviderResponse\x12K\n" + + "\rmodel_pricing\x18\x01 \x03(\v2&.tim.api.pricing.v1alpha1.ModelPricingR\fmodelPricing\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize2\xba\b\n" + + "\x0ePricingService\x12\x9b\x01\n" + + "\x0fGetModelPricing\x120.tim.api.pricing.v1alpha1.GetModelPricingRequest\x1a&.tim.api.pricing.v1alpha1.ModelPricing\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/v1alpha1/{name=modelPricing/*}\x12\x99\x01\n" + + "\x10ListModelPricing\x121.tim.api.pricing.v1alpha1.ListModelPricingRequest\x1a2.tim.api.pricing.v1alpha1.ListModelPricingResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/v1alpha1/modelPricing\x12\xb0\x01\n" + + "\x12CreateModelPricing\x123.tim.api.pricing.v1alpha1.CreateModelPricingRequest\x1a&.tim.api.pricing.v1alpha1.ModelPricing\"=\xdaA\rmodel_pricing\x82\xd3\xe4\x93\x02':\rmodel_pricing\"\x16/v1alpha1/modelPricing\x12\xc7\x01\n" + + "\x12UpdateModelPricing\x123.tim.api.pricing.v1alpha1.UpdateModelPricingRequest\x1a&.tim.api.pricing.v1alpha1.ModelPricing\"T\xdaA\rmodel_pricing\x82\xd3\xe4\x93\x02>:\rmodel_pricing2-/v1alpha1/{model_pricing.path=modelPricing/*}\x12\x91\x01\n" + + "\x12DeleteModelPricing\x123.tim.api.pricing.v1alpha1.DeleteModelPricingRequest\x1a\x16.google.protobuf.Empty\".\xdaA\x04name\x82\xd3\xe4\x93\x02!*\x1f/v1alpha1/{name=modelPricing/*}\x12\xc0\x01\n" + + "\x14GetPricingByProvider\x125.tim.api.pricing.v1alpha1.GetPricingByProviderRequest\x1a6.tim.api.pricing.v1alpha1.GetPricingByProviderResponse\"9\xdaA\rprovider_name\x82\xd3\xe4\x93\x02#\x12!/v1alpha1/modelPricing:byProvider\x1a\x1a\xcaA\x17tim-api.settlerlabs.comB\x8a\x02\n" + + "\x1ccom.tim.api.pricing.v1alpha1B\x13PricingServiceProtoP\x01ZRgithub.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1;pricingv1alpha1\xa2\x02\x03TAP\xaa\x02\x18Tim.Api.Pricing.V1alpha1\xca\x02\x18Tim\\Api\\Pricing\\V1alpha1\xe2\x02$Tim\\Api\\Pricing\\V1alpha1\\GPBMetadata\xea\x02\x1bTim::Api::Pricing::V1alpha1b\x06proto3" + +var ( + file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescOnce sync.Once + file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescData []byte +) + +func file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescGZIP() []byte { + file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescOnce.Do(func() { + file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDesc), len(file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDesc))) + }) + return file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDescData +} + +var file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_tim_api_pricing_v1alpha1_pricing_service_proto_goTypes = []any{ + (*GetModelPricingRequest)(nil), // 0: tim.api.pricing.v1alpha1.GetModelPricingRequest + (*ListModelPricingRequest)(nil), // 1: tim.api.pricing.v1alpha1.ListModelPricingRequest + (*ListModelPricingResponse)(nil), // 2: tim.api.pricing.v1alpha1.ListModelPricingResponse + (*CreateModelPricingRequest)(nil), // 3: tim.api.pricing.v1alpha1.CreateModelPricingRequest + (*UpdateModelPricingRequest)(nil), // 4: tim.api.pricing.v1alpha1.UpdateModelPricingRequest + (*DeleteModelPricingRequest)(nil), // 5: tim.api.pricing.v1alpha1.DeleteModelPricingRequest + (*GetPricingByProviderRequest)(nil), // 6: tim.api.pricing.v1alpha1.GetPricingByProviderRequest + (*GetPricingByProviderResponse)(nil), // 7: tim.api.pricing.v1alpha1.GetPricingByProviderResponse + (*ModelPricing)(nil), // 8: tim.api.pricing.v1alpha1.ModelPricing + (*fieldmaskpb.FieldMask)(nil), // 9: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty +} +var file_tim_api_pricing_v1alpha1_pricing_service_proto_depIdxs = []int32{ + 8, // 0: tim.api.pricing.v1alpha1.ListModelPricingResponse.model_pricing:type_name -> tim.api.pricing.v1alpha1.ModelPricing + 8, // 1: tim.api.pricing.v1alpha1.CreateModelPricingRequest.model_pricing:type_name -> tim.api.pricing.v1alpha1.ModelPricing + 8, // 2: tim.api.pricing.v1alpha1.UpdateModelPricingRequest.model_pricing:type_name -> tim.api.pricing.v1alpha1.ModelPricing + 9, // 3: tim.api.pricing.v1alpha1.UpdateModelPricingRequest.update_mask:type_name -> google.protobuf.FieldMask + 8, // 4: tim.api.pricing.v1alpha1.GetPricingByProviderResponse.model_pricing:type_name -> tim.api.pricing.v1alpha1.ModelPricing + 0, // 5: tim.api.pricing.v1alpha1.PricingService.GetModelPricing:input_type -> tim.api.pricing.v1alpha1.GetModelPricingRequest + 1, // 6: tim.api.pricing.v1alpha1.PricingService.ListModelPricing:input_type -> tim.api.pricing.v1alpha1.ListModelPricingRequest + 3, // 7: tim.api.pricing.v1alpha1.PricingService.CreateModelPricing:input_type -> tim.api.pricing.v1alpha1.CreateModelPricingRequest + 4, // 8: tim.api.pricing.v1alpha1.PricingService.UpdateModelPricing:input_type -> tim.api.pricing.v1alpha1.UpdateModelPricingRequest + 5, // 9: tim.api.pricing.v1alpha1.PricingService.DeleteModelPricing:input_type -> tim.api.pricing.v1alpha1.DeleteModelPricingRequest + 6, // 10: tim.api.pricing.v1alpha1.PricingService.GetPricingByProvider:input_type -> tim.api.pricing.v1alpha1.GetPricingByProviderRequest + 8, // 11: tim.api.pricing.v1alpha1.PricingService.GetModelPricing:output_type -> tim.api.pricing.v1alpha1.ModelPricing + 2, // 12: tim.api.pricing.v1alpha1.PricingService.ListModelPricing:output_type -> tim.api.pricing.v1alpha1.ListModelPricingResponse + 8, // 13: tim.api.pricing.v1alpha1.PricingService.CreateModelPricing:output_type -> tim.api.pricing.v1alpha1.ModelPricing + 8, // 14: tim.api.pricing.v1alpha1.PricingService.UpdateModelPricing:output_type -> tim.api.pricing.v1alpha1.ModelPricing + 10, // 15: tim.api.pricing.v1alpha1.PricingService.DeleteModelPricing:output_type -> google.protobuf.Empty + 7, // 16: tim.api.pricing.v1alpha1.PricingService.GetPricingByProvider:output_type -> tim.api.pricing.v1alpha1.GetPricingByProviderResponse + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_tim_api_pricing_v1alpha1_pricing_service_proto_init() } +func file_tim_api_pricing_v1alpha1_pricing_service_proto_init() { + if File_tim_api_pricing_v1alpha1_pricing_service_proto != nil { + return + } + file_tim_api_pricing_v1alpha1_pricing_types_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDesc), len(file_tim_api_pricing_v1alpha1_pricing_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_tim_api_pricing_v1alpha1_pricing_service_proto_goTypes, + DependencyIndexes: file_tim_api_pricing_v1alpha1_pricing_service_proto_depIdxs, + MessageInfos: file_tim_api_pricing_v1alpha1_pricing_service_proto_msgTypes, + }.Build() + File_tim_api_pricing_v1alpha1_pricing_service_proto = out.File + file_tim_api_pricing_v1alpha1_pricing_service_proto_goTypes = nil + file_tim_api_pricing_v1alpha1_pricing_service_proto_depIdxs = nil +} diff --git a/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.swagger.json b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.swagger.json new file mode 100644 index 000000000..9294f332d --- /dev/null +++ b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_service.swagger.json @@ -0,0 +1,425 @@ +{ + "swagger": "2.0", + "info": { + "title": "tim/api/pricing/v1alpha1/pricing_service.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "PricingService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1alpha1/modelPricing": { + "get": { + "summary": "List all model pricing", + "operationId": "PricingService_ListModelPricing", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ListModelPricingResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "pageSize", + "description": "The maximum number of items to return", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageToken", + "description": "The next_page_token value returned from a previous request, if any", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "filterProvider", + "description": "Filter by provider name (optional)", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "PricingService" + ] + }, + "post": { + "summary": "Create new model pricing", + "operationId": "PricingService_CreateModelPricing", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ModelPricing" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "modelPricing", + "description": "The model pricing to create", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1alpha1ModelPricing" + } + } + ], + "tags": [ + "PricingService" + ] + } + }, + "/v1alpha1/modelPricing:byProvider": { + "get": { + "summary": "Get pricing by provider", + "operationId": "PricingService_GetPricingByProvider", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1GetPricingByProviderResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "providerName", + "description": "The provider name to filter by", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "pageSize", + "description": "The maximum number of items to return", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageToken", + "description": "The next_page_token value returned from a previous request, if any", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "PricingService" + ] + } + }, + "/v1alpha1/{modelPricing.path}": { + "patch": { + "summary": "Update existing model pricing", + "operationId": "PricingService_UpdateModelPricing", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ModelPricing" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "modelPricing.path", + "description": "The resource path identifier (model ID)", + "in": "path", + "required": true, + "type": "string", + "pattern": "modelPricing/[^/]+" + }, + { + "name": "modelPricing", + "description": "The model pricing to update", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "modelId": { + "type": "string", + "title": "Unique identifier for the model" + }, + "providerName": { + "type": "string", + "title": "Name of the LLM provider (e.g., \"anthropic\", \"openai\", \"google\")" + }, + "inputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million input tokens" + }, + "outputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million output tokens" + }, + "cachedInputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million cached input tokens (optional)" + }, + "cachedOutputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million cached output tokens (optional)" + }, + "currency": { + "type": "string", + "title": "Currency code (ISO 4217)" + }, + "createTime": { + "type": "string", + "format": "date-time", + "title": "Timestamp when the pricing was created", + "readOnly": true + }, + "updateTime": { + "type": "string", + "format": "date-time", + "title": "Timestamp when the pricing was last updated", + "readOnly": true + } + }, + "title": "The model pricing to update" + } + } + ], + "tags": [ + "PricingService" + ] + } + }, + "/v1alpha1/{name}": { + "get": { + "summary": "Get pricing for a specific model", + "operationId": "PricingService_GetModelPricing", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ModelPricing" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "name", + "description": "The resource name of the model pricing to retrieve", + "in": "path", + "required": true, + "type": "string", + "pattern": "modelPricing/[^/]+" + } + ], + "tags": [ + "PricingService" + ] + }, + "delete": { + "summary": "Delete model pricing", + "operationId": "PricingService_DeleteModelPricing", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": {} + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "name", + "description": "The resource name of the model pricing to delete", + "in": "path", + "required": true, + "type": "string", + "pattern": "modelPricing/[^/]+" + } + ], + "tags": [ + "PricingService" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + } + }, + "additionalProperties": {}, + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1alpha1GetPricingByProviderResponse": { + "type": "object", + "properties": { + "modelPricing": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1ModelPricing" + }, + "title": "The list of model pricing for the provider" + }, + "nextPageToken": { + "type": "string", + "title": "Token to retrieve the next page of results, or empty if there are no more results" + }, + "totalSize": { + "type": "integer", + "format": "int32", + "title": "Total number of items available for this provider" + } + }, + "title": "Response message for GetPricingByProvider" + }, + "v1alpha1ListModelPricingResponse": { + "type": "object", + "properties": { + "modelPricing": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1ModelPricing" + }, + "title": "The list of model pricing" + }, + "nextPageToken": { + "type": "string", + "title": "Token to retrieve the next page of results, or empty if there are no more results" + }, + "totalSize": { + "type": "integer", + "format": "int32", + "title": "Total number of items available" + } + }, + "title": "Response message for ListModelPricing" + }, + "v1alpha1ModelPricing": { + "type": "object", + "properties": { + "path": { + "type": "string", + "title": "The resource path identifier (model ID)", + "readOnly": true + }, + "modelId": { + "type": "string", + "title": "Unique identifier for the model" + }, + "providerName": { + "type": "string", + "title": "Name of the LLM provider (e.g., \"anthropic\", \"openai\", \"google\")" + }, + "inputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million input tokens" + }, + "outputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million output tokens" + }, + "cachedInputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million cached input tokens (optional)" + }, + "cachedOutputPricePer1mTokens": { + "type": "string", + "title": "Price per 1 million cached output tokens (optional)" + }, + "currency": { + "type": "string", + "title": "Currency code (ISO 4217)" + }, + "createTime": { + "type": "string", + "format": "date-time", + "title": "Timestamp when the pricing was created", + "readOnly": true + }, + "updateTime": { + "type": "string", + "format": "date-time", + "title": "Timestamp when the pricing was last updated", + "readOnly": true + } + }, + "title": "ModelPricing represents the pricing information for an LLM model" + } + } +} diff --git a/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.pb.go b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.pb.go new file mode 100644 index 000000000..19464a133 --- /dev/null +++ b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.9 +// protoc (unknown) +// source: tim/api/pricing/v1alpha1/pricing_types.proto + +package pricingv1alpha1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ModelPricing represents the pricing information for an LLM model +type ModelPricing struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource path identifier (model ID) + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Unique identifier for the model + ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` + // Name of the LLM provider (e.g., "anthropic", "openai", "google") + ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // Price per 1 million input tokens + InputPricePer_1MTokens string `protobuf:"bytes,4,opt,name=input_price_per_1m_tokens,json=inputPricePer1mTokens,proto3" json:"input_price_per_1m_tokens,omitempty"` + // Price per 1 million output tokens + OutputPricePer_1MTokens string `protobuf:"bytes,5,opt,name=output_price_per_1m_tokens,json=outputPricePer1mTokens,proto3" json:"output_price_per_1m_tokens,omitempty"` + // Price per 1 million cached input tokens (optional) + CachedInputPricePer_1MTokens *string `protobuf:"bytes,6,opt,name=cached_input_price_per_1m_tokens,json=cachedInputPricePer1mTokens,proto3,oneof" json:"cached_input_price_per_1m_tokens,omitempty"` + // Price per 1 million cached output tokens (optional) + CachedOutputPricePer_1MTokens *string `protobuf:"bytes,7,opt,name=cached_output_price_per_1m_tokens,json=cachedOutputPricePer1mTokens,proto3,oneof" json:"cached_output_price_per_1m_tokens,omitempty"` + // Currency code (ISO 4217) + Currency string `protobuf:"bytes,8,opt,name=currency,proto3" json:"currency,omitempty"` + // Timestamp when the pricing was created + CreateTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Timestamp when the pricing was last updated + UpdateTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelPricing) Reset() { + *x = ModelPricing{} + mi := &file_tim_api_pricing_v1alpha1_pricing_types_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelPricing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelPricing) ProtoMessage() {} + +func (x *ModelPricing) ProtoReflect() protoreflect.Message { + mi := &file_tim_api_pricing_v1alpha1_pricing_types_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelPricing.ProtoReflect.Descriptor instead. +func (*ModelPricing) Descriptor() ([]byte, []int) { + return file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescGZIP(), []int{0} +} + +func (x *ModelPricing) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ModelPricing) GetModelId() string { + if x != nil { + return x.ModelId + } + return "" +} + +func (x *ModelPricing) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *ModelPricing) GetInputPricePer_1MTokens() string { + if x != nil { + return x.InputPricePer_1MTokens + } + return "" +} + +func (x *ModelPricing) GetOutputPricePer_1MTokens() string { + if x != nil { + return x.OutputPricePer_1MTokens + } + return "" +} + +func (x *ModelPricing) GetCachedInputPricePer_1MTokens() string { + if x != nil && x.CachedInputPricePer_1MTokens != nil { + return *x.CachedInputPricePer_1MTokens + } + return "" +} + +func (x *ModelPricing) GetCachedOutputPricePer_1MTokens() string { + if x != nil && x.CachedOutputPricePer_1MTokens != nil { + return *x.CachedOutputPricePer_1MTokens + } + return "" +} + +func (x *ModelPricing) GetCurrency() string { + if x != nil { + return x.Currency + } + return "" +} + +func (x *ModelPricing) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ModelPricing) GetUpdateTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +var File_tim_api_pricing_v1alpha1_pricing_types_proto protoreflect.FileDescriptor + +const file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDesc = "" + + "\n" + + ",tim/api/pricing/v1alpha1/pricing_types.proto\x12\x18tim.api.pricing.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x96\a\n" + + "\fModelPricing\x12!\n" + + "\x04path\x18\x01 \x01(\tB\r\xe0A\x03\xbaH\a\xd8\x01\x01r\x02\x10\x01R\x04path\x12(\n" + + "\bmodel_id\x18\x02 \x01(\tB\r\xbaH\n" + + "\xc8\x01\x01r\x05\x10\x01\x18\xff\x01R\amodelId\x121\n" + + "\rprovider_name\x18\x03 \x01(\tB\f\xbaH\t\xc8\x01\x01r\x04\x10\x01\x18dR\fproviderName\x12[\n" + + "\x19input_price_per_1m_tokens\x18\x04 \x01(\tB!\xbaH\x1e\xc8\x01\x01r\x192\x17^[0-9]+(\\.[0-9]{1,4})?$R\x15inputPricePer1mTokens\x12]\n" + + "\x1aoutput_price_per_1m_tokens\x18\x05 \x01(\tB!\xbaH\x1e\xc8\x01\x01r\x192\x17^[0-9]+(\\.[0-9]{1,4})?$R\x16outputPricePer1mTokens\x12m\n" + + " cached_input_price_per_1m_tokens\x18\x06 \x01(\tB!\xbaH\x1e\xd8\x01\x01r\x192\x17^[0-9]+(\\.[0-9]{1,4})?$H\x00R\x1bcachedInputPricePer1mTokens\x88\x01\x01\x12o\n" + + "!cached_output_price_per_1m_tokens\x18\a \x01(\tB!\xbaH\x1e\xd8\x01\x01r\x192\x17^[0-9]+(\\.[0-9]{1,4})?$H\x01R\x1ccachedOutputPricePer1mTokens\x88\x01\x01\x123\n" + + "\bcurrency\x18\b \x01(\tB\x17\xbaH\x14\xc8\x01\x01r\x0f2\n" + + "^[A-Z]{3}$\x98\x01\x03R\bcurrency\x12F\n" + + "\vcreate_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampB\t\xe0A\x03\xbaH\x03\xc8\x01\x01R\n" + + "createTime\x12F\n" + + "\vupdate_time\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampB\t\xe0A\x03\xbaH\x03\xc8\x01\x01R\n" + + "updateTime:Z\xeaAW\n" + + "!tim.settlerlabs.com/model-pricing\x12\x14modelPricing/{model}*\rmodel-pricing2\rmodel-pricingB#\n" + + "!_cached_input_price_per_1m_tokensB$\n" + + "\"_cached_output_price_per_1m_tokensB\x88\x02\n" + + "\x1ccom.tim.api.pricing.v1alpha1B\x11PricingTypesProtoP\x01ZRgithub.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1;pricingv1alpha1\xa2\x02\x03TAP\xaa\x02\x18Tim.Api.Pricing.V1alpha1\xca\x02\x18Tim\\Api\\Pricing\\V1alpha1\xe2\x02$Tim\\Api\\Pricing\\V1alpha1\\GPBMetadata\xea\x02\x1bTim::Api::Pricing::V1alpha1b\x06proto3" + +var ( + file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescOnce sync.Once + file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescData []byte +) + +func file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescGZIP() []byte { + file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescOnce.Do(func() { + file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDesc), len(file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDesc))) + }) + return file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDescData +} + +var file_tim_api_pricing_v1alpha1_pricing_types_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_tim_api_pricing_v1alpha1_pricing_types_proto_goTypes = []any{ + (*ModelPricing)(nil), // 0: tim.api.pricing.v1alpha1.ModelPricing + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp +} +var file_tim_api_pricing_v1alpha1_pricing_types_proto_depIdxs = []int32{ + 1, // 0: tim.api.pricing.v1alpha1.ModelPricing.create_time:type_name -> google.protobuf.Timestamp + 1, // 1: tim.api.pricing.v1alpha1.ModelPricing.update_time:type_name -> google.protobuf.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_tim_api_pricing_v1alpha1_pricing_types_proto_init() } +func file_tim_api_pricing_v1alpha1_pricing_types_proto_init() { + if File_tim_api_pricing_v1alpha1_pricing_types_proto != nil { + return + } + file_tim_api_pricing_v1alpha1_pricing_types_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDesc), len(file_tim_api_pricing_v1alpha1_pricing_types_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_tim_api_pricing_v1alpha1_pricing_types_proto_goTypes, + DependencyIndexes: file_tim_api_pricing_v1alpha1_pricing_types_proto_depIdxs, + MessageInfos: file_tim_api_pricing_v1alpha1_pricing_types_proto_msgTypes, + }.Build() + File_tim_api_pricing_v1alpha1_pricing_types_proto = out.File + file_tim_api_pricing_v1alpha1_pricing_types_proto_goTypes = nil + file_tim_api_pricing_v1alpha1_pricing_types_proto_depIdxs = nil +} diff --git a/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.swagger.json b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.swagger.json new file mode 100644 index 000000000..deb410332 --- /dev/null +++ b/tim-proto/gen/tim/api/pricing/v1alpha1/pricing_types.swagger.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "tim/api/pricing/v1alpha1/pricing_types.proto", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + } + }, + "additionalProperties": {}, + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/tim-proto/gen/tim/api/pricing/v1alpha1/pricingv1alpha1connect/pricing_service.connect.go b/tim-proto/gen/tim/api/pricing/v1alpha1/pricingv1alpha1connect/pricing_service.connect.go new file mode 100644 index 000000000..3ca8ef95b --- /dev/null +++ b/tim-proto/gen/tim/api/pricing/v1alpha1/pricingv1alpha1connect/pricing_service.connect.go @@ -0,0 +1,268 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: tim/api/pricing/v1alpha1/pricing_service.proto + +package pricingv1alpha1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // PricingServiceName is the fully-qualified name of the PricingService service. + PricingServiceName = "tim.api.pricing.v1alpha1.PricingService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // PricingServiceGetModelPricingProcedure is the fully-qualified name of the PricingService's + // GetModelPricing RPC. + PricingServiceGetModelPricingProcedure = "/tim.api.pricing.v1alpha1.PricingService/GetModelPricing" + // PricingServiceListModelPricingProcedure is the fully-qualified name of the PricingService's + // ListModelPricing RPC. + PricingServiceListModelPricingProcedure = "/tim.api.pricing.v1alpha1.PricingService/ListModelPricing" + // PricingServiceCreateModelPricingProcedure is the fully-qualified name of the PricingService's + // CreateModelPricing RPC. + PricingServiceCreateModelPricingProcedure = "/tim.api.pricing.v1alpha1.PricingService/CreateModelPricing" + // PricingServiceUpdateModelPricingProcedure is the fully-qualified name of the PricingService's + // UpdateModelPricing RPC. + PricingServiceUpdateModelPricingProcedure = "/tim.api.pricing.v1alpha1.PricingService/UpdateModelPricing" + // PricingServiceDeleteModelPricingProcedure is the fully-qualified name of the PricingService's + // DeleteModelPricing RPC. + PricingServiceDeleteModelPricingProcedure = "/tim.api.pricing.v1alpha1.PricingService/DeleteModelPricing" + // PricingServiceGetPricingByProviderProcedure is the fully-qualified name of the PricingService's + // GetPricingByProvider RPC. + PricingServiceGetPricingByProviderProcedure = "/tim.api.pricing.v1alpha1.PricingService/GetPricingByProvider" +) + +// PricingServiceClient is a client for the tim.api.pricing.v1alpha1.PricingService service. +type PricingServiceClient interface { + // Get pricing for a specific model + GetModelPricing(context.Context, *connect.Request[v1alpha1.GetModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // List all model pricing + ListModelPricing(context.Context, *connect.Request[v1alpha1.ListModelPricingRequest]) (*connect.Response[v1alpha1.ListModelPricingResponse], error) + // Create new model pricing + CreateModelPricing(context.Context, *connect.Request[v1alpha1.CreateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // Update existing model pricing + UpdateModelPricing(context.Context, *connect.Request[v1alpha1.UpdateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // Delete model pricing + DeleteModelPricing(context.Context, *connect.Request[v1alpha1.DeleteModelPricingRequest]) (*connect.Response[emptypb.Empty], error) + // Get pricing by provider + GetPricingByProvider(context.Context, *connect.Request[v1alpha1.GetPricingByProviderRequest]) (*connect.Response[v1alpha1.GetPricingByProviderResponse], error) +} + +// NewPricingServiceClient constructs a client for the tim.api.pricing.v1alpha1.PricingService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewPricingServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PricingServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + pricingServiceMethods := v1alpha1.File_tim_api_pricing_v1alpha1_pricing_service_proto.Services().ByName("PricingService").Methods() + return &pricingServiceClient{ + getModelPricing: connect.NewClient[v1alpha1.GetModelPricingRequest, v1alpha1.ModelPricing]( + httpClient, + baseURL+PricingServiceGetModelPricingProcedure, + connect.WithSchema(pricingServiceMethods.ByName("GetModelPricing")), + connect.WithClientOptions(opts...), + ), + listModelPricing: connect.NewClient[v1alpha1.ListModelPricingRequest, v1alpha1.ListModelPricingResponse]( + httpClient, + baseURL+PricingServiceListModelPricingProcedure, + connect.WithSchema(pricingServiceMethods.ByName("ListModelPricing")), + connect.WithClientOptions(opts...), + ), + createModelPricing: connect.NewClient[v1alpha1.CreateModelPricingRequest, v1alpha1.ModelPricing]( + httpClient, + baseURL+PricingServiceCreateModelPricingProcedure, + connect.WithSchema(pricingServiceMethods.ByName("CreateModelPricing")), + connect.WithClientOptions(opts...), + ), + updateModelPricing: connect.NewClient[v1alpha1.UpdateModelPricingRequest, v1alpha1.ModelPricing]( + httpClient, + baseURL+PricingServiceUpdateModelPricingProcedure, + connect.WithSchema(pricingServiceMethods.ByName("UpdateModelPricing")), + connect.WithClientOptions(opts...), + ), + deleteModelPricing: connect.NewClient[v1alpha1.DeleteModelPricingRequest, emptypb.Empty]( + httpClient, + baseURL+PricingServiceDeleteModelPricingProcedure, + connect.WithSchema(pricingServiceMethods.ByName("DeleteModelPricing")), + connect.WithClientOptions(opts...), + ), + getPricingByProvider: connect.NewClient[v1alpha1.GetPricingByProviderRequest, v1alpha1.GetPricingByProviderResponse]( + httpClient, + baseURL+PricingServiceGetPricingByProviderProcedure, + connect.WithSchema(pricingServiceMethods.ByName("GetPricingByProvider")), + connect.WithClientOptions(opts...), + ), + } +} + +// pricingServiceClient implements PricingServiceClient. +type pricingServiceClient struct { + getModelPricing *connect.Client[v1alpha1.GetModelPricingRequest, v1alpha1.ModelPricing] + listModelPricing *connect.Client[v1alpha1.ListModelPricingRequest, v1alpha1.ListModelPricingResponse] + createModelPricing *connect.Client[v1alpha1.CreateModelPricingRequest, v1alpha1.ModelPricing] + updateModelPricing *connect.Client[v1alpha1.UpdateModelPricingRequest, v1alpha1.ModelPricing] + deleteModelPricing *connect.Client[v1alpha1.DeleteModelPricingRequest, emptypb.Empty] + getPricingByProvider *connect.Client[v1alpha1.GetPricingByProviderRequest, v1alpha1.GetPricingByProviderResponse] +} + +// GetModelPricing calls tim.api.pricing.v1alpha1.PricingService.GetModelPricing. +func (c *pricingServiceClient) GetModelPricing(ctx context.Context, req *connect.Request[v1alpha1.GetModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return c.getModelPricing.CallUnary(ctx, req) +} + +// ListModelPricing calls tim.api.pricing.v1alpha1.PricingService.ListModelPricing. +func (c *pricingServiceClient) ListModelPricing(ctx context.Context, req *connect.Request[v1alpha1.ListModelPricingRequest]) (*connect.Response[v1alpha1.ListModelPricingResponse], error) { + return c.listModelPricing.CallUnary(ctx, req) +} + +// CreateModelPricing calls tim.api.pricing.v1alpha1.PricingService.CreateModelPricing. +func (c *pricingServiceClient) CreateModelPricing(ctx context.Context, req *connect.Request[v1alpha1.CreateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return c.createModelPricing.CallUnary(ctx, req) +} + +// UpdateModelPricing calls tim.api.pricing.v1alpha1.PricingService.UpdateModelPricing. +func (c *pricingServiceClient) UpdateModelPricing(ctx context.Context, req *connect.Request[v1alpha1.UpdateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return c.updateModelPricing.CallUnary(ctx, req) +} + +// DeleteModelPricing calls tim.api.pricing.v1alpha1.PricingService.DeleteModelPricing. +func (c *pricingServiceClient) DeleteModelPricing(ctx context.Context, req *connect.Request[v1alpha1.DeleteModelPricingRequest]) (*connect.Response[emptypb.Empty], error) { + return c.deleteModelPricing.CallUnary(ctx, req) +} + +// GetPricingByProvider calls tim.api.pricing.v1alpha1.PricingService.GetPricingByProvider. +func (c *pricingServiceClient) GetPricingByProvider(ctx context.Context, req *connect.Request[v1alpha1.GetPricingByProviderRequest]) (*connect.Response[v1alpha1.GetPricingByProviderResponse], error) { + return c.getPricingByProvider.CallUnary(ctx, req) +} + +// PricingServiceHandler is an implementation of the tim.api.pricing.v1alpha1.PricingService +// service. +type PricingServiceHandler interface { + // Get pricing for a specific model + GetModelPricing(context.Context, *connect.Request[v1alpha1.GetModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // List all model pricing + ListModelPricing(context.Context, *connect.Request[v1alpha1.ListModelPricingRequest]) (*connect.Response[v1alpha1.ListModelPricingResponse], error) + // Create new model pricing + CreateModelPricing(context.Context, *connect.Request[v1alpha1.CreateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // Update existing model pricing + UpdateModelPricing(context.Context, *connect.Request[v1alpha1.UpdateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) + // Delete model pricing + DeleteModelPricing(context.Context, *connect.Request[v1alpha1.DeleteModelPricingRequest]) (*connect.Response[emptypb.Empty], error) + // Get pricing by provider + GetPricingByProvider(context.Context, *connect.Request[v1alpha1.GetPricingByProviderRequest]) (*connect.Response[v1alpha1.GetPricingByProviderResponse], error) +} + +// NewPricingServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewPricingServiceHandler(svc PricingServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + pricingServiceMethods := v1alpha1.File_tim_api_pricing_v1alpha1_pricing_service_proto.Services().ByName("PricingService").Methods() + pricingServiceGetModelPricingHandler := connect.NewUnaryHandler( + PricingServiceGetModelPricingProcedure, + svc.GetModelPricing, + connect.WithSchema(pricingServiceMethods.ByName("GetModelPricing")), + connect.WithHandlerOptions(opts...), + ) + pricingServiceListModelPricingHandler := connect.NewUnaryHandler( + PricingServiceListModelPricingProcedure, + svc.ListModelPricing, + connect.WithSchema(pricingServiceMethods.ByName("ListModelPricing")), + connect.WithHandlerOptions(opts...), + ) + pricingServiceCreateModelPricingHandler := connect.NewUnaryHandler( + PricingServiceCreateModelPricingProcedure, + svc.CreateModelPricing, + connect.WithSchema(pricingServiceMethods.ByName("CreateModelPricing")), + connect.WithHandlerOptions(opts...), + ) + pricingServiceUpdateModelPricingHandler := connect.NewUnaryHandler( + PricingServiceUpdateModelPricingProcedure, + svc.UpdateModelPricing, + connect.WithSchema(pricingServiceMethods.ByName("UpdateModelPricing")), + connect.WithHandlerOptions(opts...), + ) + pricingServiceDeleteModelPricingHandler := connect.NewUnaryHandler( + PricingServiceDeleteModelPricingProcedure, + svc.DeleteModelPricing, + connect.WithSchema(pricingServiceMethods.ByName("DeleteModelPricing")), + connect.WithHandlerOptions(opts...), + ) + pricingServiceGetPricingByProviderHandler := connect.NewUnaryHandler( + PricingServiceGetPricingByProviderProcedure, + svc.GetPricingByProvider, + connect.WithSchema(pricingServiceMethods.ByName("GetPricingByProvider")), + connect.WithHandlerOptions(opts...), + ) + return "/tim.api.pricing.v1alpha1.PricingService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PricingServiceGetModelPricingProcedure: + pricingServiceGetModelPricingHandler.ServeHTTP(w, r) + case PricingServiceListModelPricingProcedure: + pricingServiceListModelPricingHandler.ServeHTTP(w, r) + case PricingServiceCreateModelPricingProcedure: + pricingServiceCreateModelPricingHandler.ServeHTTP(w, r) + case PricingServiceUpdateModelPricingProcedure: + pricingServiceUpdateModelPricingHandler.ServeHTTP(w, r) + case PricingServiceDeleteModelPricingProcedure: + pricingServiceDeleteModelPricingHandler.ServeHTTP(w, r) + case PricingServiceGetPricingByProviderProcedure: + pricingServiceGetPricingByProviderHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPricingServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPricingServiceHandler struct{} + +func (UnimplementedPricingServiceHandler) GetModelPricing(context.Context, *connect.Request[v1alpha1.GetModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.GetModelPricing is not implemented")) +} + +func (UnimplementedPricingServiceHandler) ListModelPricing(context.Context, *connect.Request[v1alpha1.ListModelPricingRequest]) (*connect.Response[v1alpha1.ListModelPricingResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.ListModelPricing is not implemented")) +} + +func (UnimplementedPricingServiceHandler) CreateModelPricing(context.Context, *connect.Request[v1alpha1.CreateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.CreateModelPricing is not implemented")) +} + +func (UnimplementedPricingServiceHandler) UpdateModelPricing(context.Context, *connect.Request[v1alpha1.UpdateModelPricingRequest]) (*connect.Response[v1alpha1.ModelPricing], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.UpdateModelPricing is not implemented")) +} + +func (UnimplementedPricingServiceHandler) DeleteModelPricing(context.Context, *connect.Request[v1alpha1.DeleteModelPricingRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.DeleteModelPricing is not implemented")) +} + +func (UnimplementedPricingServiceHandler) GetPricingByProvider(context.Context, *connect.Request[v1alpha1.GetPricingByProviderRequest]) (*connect.Response[v1alpha1.GetPricingByProviderResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("tim.api.pricing.v1alpha1.PricingService.GetPricingByProvider is not implemented")) +} diff --git a/tim-worker/internal/apiclient/client.go b/tim-worker/internal/apiclient/client.go index de59e2ba8..2312794d8 100644 --- a/tim-worker/internal/apiclient/client.go +++ b/tim-worker/internal/apiclient/client.go @@ -9,8 +9,12 @@ import ( "github.com/Greybox-Labs/tim/shared/logger" "github.com/Greybox-Labs/tim/shared/proto" "github.com/Greybox-Labs/tim/shared/tools" + billingv1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/billing/v1alpha1" + "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/billing/v1alpha1/billingv1alpha1connect" llmresponsev1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/llm_response/v1alpha1" "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/llm_response/v1alpha1/llm_responsev1alpha1connect" + pricingv1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1" + "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1/pricingv1alpha1connect" thread "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/thread/v1alpha1" "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/thread/v1alpha1/threadv1alpha1connect" threadcontextv1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/thread_context/v1alpha1" @@ -31,6 +35,8 @@ type Client struct { llmResponseClient llm_responsev1alpha1connect.LlmResponseServiceClient toolExecutionClient tool_executionv1alpha1connect.ToolExecutionServiceClient todoClient todov1alpha1connect.TodoServiceClient + pricingClient pricingv1alpha1connect.PricingServiceClient + billingClient billingv1alpha1connect.BillingServiceClient } // NewClient creates a new internal API client @@ -60,6 +66,14 @@ func NewClient(baseURL string, logger *logger.Logger) *Client { httpClient, baseURL, ), + pricingClient: pricingv1alpha1connect.NewPricingServiceClient( + httpClient, + baseURL, + ), + billingClient: billingv1alpha1connect.NewBillingServiceClient( + httpClient, + baseURL, + ), } } @@ -415,6 +429,55 @@ func (c *Client) CheckHealth(ctx context.Context) error { return nil } +// GetModelPricing gets pricing information for a specific model +func (c *Client) GetModelPricing(ctx context.Context, modelId string) (*pricingv1.ModelPricing, error) { + req := connect.NewRequest(&pricingv1.GetModelPricingRequest{ + Name: fmt.Sprintf("modelPricing/%s", modelId), + }) + + resp, err := c.pricingClient.GetModelPricing(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get model pricing for %s: %w", modelId, err) + } + + return resp.Msg, nil +} + +// ReportUsage reports token usage to the billing service for credit deduction +func (c *Client) ReportUsage(ctx context.Context, orgPath string, tokensUsed int64, threadId string) error { + req := connect.NewRequest(&billingv1.ReportUsageRequest{ + Org: orgPath, + TokensUsed: tokensUsed, + Thread: threadId, + }) + + _, err := c.billingClient.ReportUsage(ctx, req) + if err != nil { + return fmt.Errorf("failed to report usage to billing service: %w", err) + } + + c.logger.Infow("Usage reported to billing service successfully", + "org_path", orgPath, + "tokens_used", tokensUsed, + "thread_id", threadId) + + return nil +} + +// GetCreditBalance gets the credit balance for an organization +func (c *Client) GetCreditBalance(ctx context.Context, orgPath string) (*billingv1.CreditBalance, error) { + req := connect.NewRequest(&billingv1.GetCreditBalanceRequest{ + Path: fmt.Sprintf("%s/credits", orgPath), + }) + + resp, err := c.billingClient.GetCreditBalance(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get credit balance for %s: %w", orgPath, err) + } + + return resp.Msg, nil +} + // Helper functions for todo enum conversions func buildTodoFilter(progressFilter string, priorityFilter string) string { diff --git a/tim-worker/internal/worker/handle_llm_relay.go b/tim-worker/internal/worker/handle_llm_relay.go index c02a628d8..b1d29921c 100644 --- a/tim-worker/internal/worker/handle_llm_relay.go +++ b/tim-worker/internal/worker/handle_llm_relay.go @@ -4,11 +4,13 @@ import ( "context" "encoding/json" "fmt" + "strconv" "github.com/Greybox-Labs/tim/shared/llm" "github.com/Greybox-Labs/tim/shared/proto" "github.com/Greybox-Labs/tim/shared/tools" llmresponse "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/llm_response/v1alpha1" + pricingv1 "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/pricing/v1alpha1" thread "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/thread/v1alpha1" threadcontext "github.com/Greybox-Labs/tim/tim-proto/gen/tim/api/thread_context/v1alpha1" "github.com/Greybox-Labs/tim/tim-worker/internal/llm_provider" @@ -47,6 +49,22 @@ func (w *Worker) handleLLMRelay(ctx context.Context, args ...interface{}) error w.logger.Debugw("Got thread context", "thread_id", threadId, "model", threadContext.Model) + // Extract org path for credit check + orgPath, err := w.extractOrgPathFromThreadId(threadId) + if err != nil { + return fmt.Errorf("failed to extract org path from thread ID %s: %w", threadId, err) + } + + // Check if organization has sufficient credits before processing + err = w.checkSufficientCredits(ctx, orgPath) + if err != nil { + w.logger.Warnw("Insufficient credits for LLM processing", + "thread_id", threadId, + "org_path", orgPath, + "error", err) + return fmt.Errorf("insufficient credits for LLM processing: %w", err) + } + // Convert thread context to provider params params := w.convertThreadContextToProviderParams(threadContext) @@ -103,6 +121,17 @@ func (w *Worker) handleLLMRelay(ctx context.Context, args ...interface{}) error return fmt.Errorf("failed to push token usage: %w", err) } + if usage.InputTokens > 0 || usage.OutputTokens > 0 { + if err := w.handleTokenBilling(ctx, usage, string(params.Model), orgPath, threadId); err != nil { + w.logger.Errorw("Failed to process billing for token usage", + "error", err, + "thread_id", threadId, + "org_path", orgPath) + } + } else { + w.logger.Debugw("No tokens used, skipping billing", "thread_id", threadId) + } + w.logger.Infow("Successfully completed LLM relay job", "thread_id", threadId) return nil @@ -252,3 +281,141 @@ func (w *Worker) convertToolChoice(tc *threadcontext.ToolChoice) llm.ToolChoice return llm.ToolChoice{Type: llm.ToolChoiceTypeAuto} } } + +// extractOrgPathFromThreadId extracts "orgs/{org}" from a thread path +// Input: "orgs/{org}/users/{user}/threads/{thread}" +// Output: "orgs/{org}" +func (w *Worker) extractOrgPathFromThreadId(threadId string) (string, error) { + usersIndex := findSubstring(threadId, "/users/") + if usersIndex == -1 { + return "", fmt.Errorf("invalid thread path format: missing '/users/' segment in %s", threadId) + } + + orgPath := threadId[:usersIndex] + if orgPath == "" { + return "", fmt.Errorf("invalid thread path format: empty org path in %s", threadId) + } + + return orgPath, nil +} + +// checkSufficientCredits verifies that an organization has sufficient credits above the minimum threshold +func (w *Worker) checkSufficientCredits(ctx context.Context, orgPath string) error { + // Define minimum credit threshold (equivalent to ~1000 tokens at $0.015 per 1K tokens = $15 = 150 milli-cents) + const minCreditThreshold int64 = 0 // milli-cents + + balance, err := w.apiClient.GetCreditBalance(ctx, orgPath) + if err != nil { + return fmt.Errorf("failed to get credit balance: %w", err) + } + + if balance.CreditsRemaining < minCreditThreshold { + w.logger.Warnw("Organization has insufficient credits", + "org_path", orgPath, + "credits_remaining", balance.CreditsRemaining, + "minimum_required", minCreditThreshold) + return fmt.Errorf("insufficient credits: %d remaining, minimum required: %d", + balance.CreditsRemaining, minCreditThreshold) + } + + w.logger.Debugw("Credit check passed", + "org_path", orgPath, + "credits_remaining", balance.CreditsRemaining, + "minimum_required", minCreditThreshold) + + return nil +} + +// handleTokenBilling processes billing for token usage by calculating cost-weighted tokens and reporting usage +func (w *Worker) handleTokenBilling(ctx context.Context, usage *llm_provider.TokenUsage, modelId, orgPath, threadId string) error { + w.logger.Debugw("Processing billing for token usage", + "model_id", modelId, + "input_tokens", usage.InputTokens, + "output_tokens", usage.OutputTokens, + "cache_create_tokens", usage.CacheCreateTokens, + "cache_read_tokens", usage.CacheReadTokens, + "org_path", orgPath, + "thread_id", threadId) + + // Get pricing for the model + pricing, err := w.apiClient.GetModelPricing(ctx, modelId) + if err != nil { + return fmt.Errorf("failed to get pricing for model %s: %w", modelId, err) + } + + // Calculate cost-weighted tokens based on model pricing + costWeightedTokens, err := w.calculateCostWeightedTokens(usage, pricing) + if err != nil { + return fmt.Errorf("failed to calculate cost-weighted tokens for model %s: %w", modelId, err) + } + + // Report usage to billing service + return w.reportUsageToBilling(ctx, orgPath, costWeightedTokens, threadId) +} + +// parsePriceOrDefault parses a nullable price string, returning the parsed value or defaultValue if nil/error +func parsePriceOrDefault(pricePtr *string, defaultValue int64) int64 { + if pricePtr == nil { + return defaultValue + } + price, err := strconv.ParseInt(*pricePtr, 10, 64) + if err != nil { + return defaultValue + } + return price +} + +// calculateCostWeightedTokens calculates the cost-equivalent token count based on model pricing +func (w *Worker) calculateCostWeightedTokens(usage *llm_provider.TokenUsage, pricing *pricingv1.ModelPricing) (int64, error) { + // Parse string prices to int64 (they're in milli-cents per 1M tokens) + inputPriceMilliCents, err := strconv.ParseInt(pricing.InputPricePer_1MTokens, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse input price for model %s: %w", pricing.ModelId, err) + } + + outputPriceMilliCents, err := strconv.ParseInt(pricing.OutputPricePer_1MTokens, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse output price for model %s: %w", pricing.ModelId, err) + } + cachedInputPriceMilliCents := parsePriceOrDefault(pricing.CachedInputPricePer_1MTokens, inputPriceMilliCents) + cachedOutputPriceMilliCents := parsePriceOrDefault(pricing.CachedOutputPricePer_1MTokens, outputPriceMilliCents) + + inputCostMilliCents := usage.InputTokens * inputPriceMilliCents + outputCostMilliCents := usage.OutputTokens * outputPriceMilliCents + cacheReadCostMilliCents := usage.CacheReadTokens * cachedInputPriceMilliCents + cacheCreateCostMilliCents := usage.CacheCreateTokens * cachedOutputPriceMilliCents + + costWeightedTokens := inputCostMilliCents + outputCostMilliCents + cacheReadCostMilliCents + cacheCreateCostMilliCents + + w.logger.Debugw("Calculated cost-weighted tokens", + "model_id", pricing.ModelId, + "input_tokens", usage.InputTokens, + "output_tokens", usage.OutputTokens, + "cache_read_tokens", usage.CacheReadTokens, + "cache_create_tokens", usage.CacheCreateTokens, + "input_price_milli_cents", inputPriceMilliCents, + "output_price_milli_cents", outputPriceMilliCents, + "cost_weighted_tokens", costWeightedTokens) + + return costWeightedTokens, nil +} + +// reportUsageToBilling reports token usage to the billing service via API client +func (w *Worker) reportUsageToBilling(ctx context.Context, orgPath string, creditsToDeduct int64, threadId string) error { + err := w.apiClient.ReportUsage(ctx, orgPath, creditsToDeduct, threadId) + if err != nil { + w.logger.Warnw("Failed to report usage to billing service", + "error", err, + "org_path", orgPath, + "credits_to_deduct", creditsToDeduct, + "thread_id", threadId) + return err + } + + w.logger.Infow("Usage reported to billing service successfully", + "org_path", orgPath, + "credits_deducted", creditsToDeduct, + "thread_id", threadId) + + return nil +}