// CatalogService -- platform-managed provider and model registry.
//
// The catalog is the authoritative source of truth for AI providers
// (OpenAI, Anthropic, AWS Bedrock, ...) and the models each provider
// exposes. Every model name that appears in an inference request or a
// billing record resolves against this catalog.
//
// Write surface (UpsertProvider, UpsertModel, SetProviderCredential,
// DeleteProvider, DeleteModel) mirrors the IA reference shape. Project
// <-> model assignment lives on this same service as a project view of
// the catalog -- IA does not define these RPCs and fraser's existing
// admin.v1 surface owns the underlying repositories that we reuse.
syntax = "proto3";

package tars.catalog.v1;

import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "tars/auth/v1/auth_options.proto";
import "tars/cli/v1/cli.proto";
import "tars/common/v1/types.proto";
import "tars/docs/v1/docs.proto";

option go_package = "github.com/tetrateio/fraser/api/tars/catalog/v1;catalogv1";

// Provider is an AI inference backend.
message Provider {
  // Resource name prov_<ulid>. Server-assigned; immutable.
  string id = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Machine-readable slug, e.g. "openai", "anthropic", "awsbedrock".
  // Unique. Canonical form is lowercase alphanumeric [a-z0-9]+ (no hyphens,
  // spaces, or uppercase) -- it is matched hyphen-insensitively at inference
  // time, so UpsertProvider requires this canonical form on write.
  string name = 2 [(google.api.field_behavior) = REQUIRED];

  // Human-readable display name, e.g. "OpenAI".
  string display_name = 3 [(google.api.field_behavior) = REQUIRED];

  // Base URL for all models served by this provider,
  // e.g. "https://api.openai.com".
  string base_url = 4 [(google.api.field_behavior) = REQUIRED];

  // Auth schemes this provider supports, e.g. "bearer",
  // "aws-sigv4-static".
  repeated string supported_auth_schemes = 5;

  // Last 4 chars of the stored platform credential (e.g. "sk-..**4a2f").
  // Empty if no platform credential has been set. BYOK keys are tracked
  // separately via aikeys.v1.
  string credential_suffix = 6 [(google.api.field_behavior) = OUTPUT_ONLY];

  // When this provider record was created.
  google.protobuf.Timestamp created_at = 50 [(google.api.field_behavior) = OUTPUT_ONLY];
  // When this provider record was last modified.
  google.protobuf.Timestamp updated_at = 51 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// Pricing holds per-million-token rates for a model. All values are
// decimal strings to avoid float drift (e.g. "2.50").
message Pricing {
  // Cost in USD per million input tokens.
  string input_per_million = 1;

  // Cost in USD per million output tokens.
  string output_per_million = 2;

  // Cost in USD per million cache-read tokens. Empty means not
  // applicable.
  string cache_read_per_million = 3;

  // Hard cap in USD per single request. Empty means no cap.
  string max_cost_per_request = 4;
}

// Model is a platform-curated entry that ties a logical name to a
// provider and a provider-side model identifier.
message Model {
  // Resource name mdl_<ulid>. Server-assigned; immutable.
  string id = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Provider this model belongs to. prov_<ulid>.
  string provider_id = 2 [(google.api.field_behavior) = REQUIRED];

  // Logical name used in routing and billing, e.g. "gpt-4o". Unique
  // within a provider.
  string name = 3 [(google.api.field_behavior) = REQUIRED];

  // Provider-side model id sent as body.model to upstream,
  // e.g. "gpt-4o-2024-11-20".
  string upstream_model = 4 [(google.api.field_behavior) = REQUIRED];

  // Per-token pricing rates.
  Pricing pricing = 5;

  // Maximum context window in tokens.
  uint32 max_context_tokens = 6;

  // Capabilities this model supports, e.g. "chat", "embeddings",
  // "images".
  repeated string capabilities = 7;

  // When this model record was created.
  google.protobuf.Timestamp created_at = 50 [(google.api.field_behavior) = OUTPUT_ONLY];
  // When this model record was last modified.
  google.protobuf.Timestamp updated_at = 51 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// CatalogService is the read surface for providers and models.
service CatalogService {
  // ListProviders returns all registered AI providers. Supports
  // cursor-based pagination via page_size and page_token.
  rpc ListProviders(ListProvidersRequest) returns (ListProvidersResponse) {
    option (tars.docs.v1.title) = "List providers";
    option (tars.docs.v1.group) = "Providers";
    option (google.api.http) = {get: "/v1/catalog/providers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "providers"
      ]
    };
  }

  // GetProvider returns a single provider by id.
  rpc GetProvider(GetProviderRequest) returns (GetProviderResponse) {
    option (tars.docs.v1.title) = "Get a provider";
    option (tars.docs.v1.group) = "Providers";
    option (google.api.http) = {get: "/v1/catalog/providers/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "provider"
      ]
      primary_flag: "id"
    };
  }

  // ListModels returns catalog models, optionally filtered to a single
  // provider via the provider_id query parameter.
  rpc ListModels(ListModelsRequest) returns (ListModelsResponse) {
    option (tars.docs.v1.title) = "List models";
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {get: "/v1/catalog/models"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "models"
      ]
    };
  }

  // GetModel returns a single model by id (UUID) or by unique model name,
  // including its pricing, context window, and capabilities.
  rpc GetModel(GetModelRequest) returns (GetModelResponse) {
    option (tars.docs.v1.title) = "Get a model";
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {get: "/v1/catalog/models/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "model"
      ]
      primary_flag: "id"
    };
  }

  // UpsertProvider creates or fully replaces a provider record.
  rpc UpsertProvider(UpsertProviderRequest) returns (Provider) {
    option (tars.docs.v1.title) = "Create or update a provider";
    option (tars.docs.v1.group) = "Providers";
    option (google.api.http) = {
      post: "/v1/catalog/providers"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "providers",
        "upsert"
      ]
    };
  }

  // UpsertModel creates or fully replaces a model record.
  rpc UpsertModel(UpsertModelRequest) returns (Model) {
    option (tars.docs.v1.title) = "Create or update a model";
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {
      post: "/v1/catalog/models"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "models",
        "upsert"
      ]
    };
  }

  // SetProviderCredential stores or rotates the credential used to
  // authenticate requests to this provider. The plaintext is never
  // returned; the provider's credential_suffix reflects the last 4
  // characters.
  rpc SetProviderCredential(SetProviderCredentialRequest) returns (Provider) {
    option (tars.docs.v1.title) = "Set a provider credential";
    option (tars.docs.v1.group) = "Providers";
    option (google.api.http) = {
      post: "/v1/catalog/providers/{id}/credential"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "providers",
        "set-credential"
      ]
      primary_flag: "id"
    };
  }

  // DeleteProvider removes a provider. Refuses while any model still
  // references it.
  rpc DeleteProvider(DeleteProviderRequest) returns (google.protobuf.Empty) {
    option (tars.docs.v1.title) = "Delete a provider";
    option (tars.docs.v1.group) = "Providers";
    option (google.api.http) = {delete: "/v1/catalog/providers/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "providers",
        "delete"
      ]
      primary_flag: "id"
    };
  }

  // DeleteModel removes a model entry.
  rpc DeleteModel(DeleteModelRequest) returns (google.protobuf.Empty) {
    option (tars.docs.v1.title) = "Delete a model";
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {delete: "/v1/catalog/models/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "catalog",
        "models",
        "delete"
      ]
      primary_flag: "id"
    };
  }

  // AssignModelToProject grants a project access to a catalog model.
  // Idempotent: re-assigning a model is a no-op.
  rpc AssignModelToProject(AssignModelToProjectRequest) returns (AssignModelToProjectResponse) {
    option (tars.docs.v1.title) = "Assign a model to a project";
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {
      post: "/v1/customers/{customer_id}/projects/{project_id}/models"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["project_owner"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "project",
        "models",
        "add"
      ]
    };
  }

  // ListProjectModels returns every catalog model assigned to a project.
  rpc ListProjectModels(ListProjectModelsRequest) returns (ListProjectModelsResponse) {
    option (tars.docs.v1.title) = "List project models";
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {get: "/v1/customers/{customer_id}/projects/{project_id}/models"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "project",
        "models",
        "list"
      ]
    };
  }

  // UnassignModelFromProject removes a project's access to a catalog model.
  rpc UnassignModelFromProject(UnassignModelFromProjectRequest) returns (UnassignModelFromProjectResponse) {
    option (tars.docs.v1.title) = "Unassign a model from a project";
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {delete: "/v1/customers/{customer_id}/projects/{project_id}/models/{model_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["project_owner"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "project",
        "models",
        "remove"
      ]
    };
  }
}

message ListProvidersRequest {
  // Page size and continuation token for cursor-based pagination.
  tars.common.v1.PageRequest page = 1;
}

message ListProvidersResponse {
  // Providers on this page, in catalog order.
  repeated Provider providers = 1;
  // Pagination cursor; carries next_page_token for the following call.
  tars.common.v1.PageResponse page = 2;
}

message GetProviderRequest {
  // Provider id to fetch. prov_<ulid>.
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message GetProviderResponse {
  // The requested provider.
  Provider provider = 1;
}

message ListModelsRequest {
  // Filter to models belonging to this provider. Empty returns all
  // models.
  string provider_id = 1;
  // Page size and continuation token for cursor-based pagination.
  tars.common.v1.PageRequest page = 2;
}

message ListModelsResponse {
  // Models on this page, in catalog order.
  repeated Model models = 1;
  // Pagination cursor; carries next_page_token for the following call.
  tars.common.v1.PageResponse page = 2;
}

message GetModelRequest {
  // Model id (UUID) or unique model name. A UUID is looked up by id; any
  // other value is resolved by model name across the visible catalog. A
  // name that matches models from multiple providers is rejected with
  // InvalidArgument naming the candidates.
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message GetModelResponse {
  // The requested model.
  Model model = 1;
}

// ProjectModelEntry pairs a project assignment row with the catalog
// Model it points at. The Model field is populated on List responses
// via a JOIN against ai_models.
message ProjectModelEntry {
  // Resource name of the project_models row (uuid).
  string id = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Customer that owns this assignment.
  string customer_id = 2;
  // Project the model is assigned to.
  string project_id = 3;

  // Catalog model id (uuid). Same value as model.id when model is set.
  string model_id = 4;

  // Full catalog Model resource. Populated on List responses; may be
  // empty on Add responses if the join was skipped.
  Model model = 5;

  google.protobuf.Timestamp created_at = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// UpsertProviderRequest carries the fields of a Provider as scalar
// top-level fields so the generated CLI can surface each one as a
// flag (`protoc-gen-ia-cli` skips message-typed fields). This is the
// one unavoidable divergence from IA's nested-`provider` shape; the
// HTTP body shape on the wire stays semantically equivalent.
message UpsertProviderRequest {
  // Provider slug, e.g. "openai". Natural key; required. Must be canonical
  // lowercase alphanumeric [a-z0-9]+ (no hyphens, spaces, or uppercase).
  string id = 1 [(google.api.field_behavior) = REQUIRED];

  // Human-readable display name, e.g. "OpenAI".
  string display_name = 2 [(google.api.field_behavior) = REQUIRED];
  // Provider API base URL. The CLI flag is renamed to
  // --provider-base-url to avoid colliding with the root command's
  // persistent --base-url (which overrides the active profile's API
  // URL for one invocation).
  string base_url = 3 [
    (google.api.field_behavior) = REQUIRED,
    (tars.cli.v1.field) = {flag: "provider-base-url"}
  ];

  // Auth schemes this provider supports, e.g. ["bearer"].
  repeated string supported_auth_schemes = 4;
}

// UpsertModelRequest carries the fields of a Model as scalar top-level
// fields for the same CLI-flag reason as UpsertProviderRequest.
message UpsertModelRequest {
  // Provider slug this model belongs to.
  string provider_id = 1 [(google.api.field_behavior) = REQUIRED];

  // Logical model name, e.g. "gpt-4o". Unique within provider.
  string name = 2 [(google.api.field_behavior) = REQUIRED];

  // Provider-side model id (defaults to name when empty).
  string upstream_model = 3;

  // Cost in USD per million input tokens (decimal string).
  string input_per_million = 4;
  // Cost in USD per million output tokens (decimal string).
  string output_per_million = 5;
  // Cost in USD per million cache-read tokens. Empty if not applicable.
  string cache_read_per_million = 6;
  // Hard cap in USD per single request. Empty means no cap.
  string max_cost_per_request = 7;

  // Maximum context window in tokens.
  uint32 max_context_tokens = 8;
  // Capabilities this model supports, e.g. ["chat", "embeddings"].
  repeated string capabilities = 9;
}

message SetProviderCredentialRequest {
  // Provider id / slug.
  string id = 1 [(google.api.field_behavior) = REQUIRED];

  // Plaintext credential (API key, bearer token, etc.). Stored
  // server-side under envelope encryption and never returned.
  // Provider.credential_suffix reflects the last 4 chars.
  string credential = 2 [(google.api.field_behavior) = REQUIRED];
}

message DeleteProviderRequest {
  // Id of the provider to delete. prov_<ulid>.
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message DeleteModelRequest {
  // Id of the model to delete. mdl_<ulid>.
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message AssignModelToProjectRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
  // Catalog model id (uuid).
  string model_id = 3 [(google.api.field_behavior) = REQUIRED];
}

message AssignModelToProjectResponse {
  // The created (or already-existing) project-to-model assignment.
  ProjectModelEntry entry = 1;
}

message ListProjectModelsRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
}

message ListProjectModelsResponse {
  // Every model assigned to the project, each joined with its catalog Model.
  repeated ProjectModelEntry entries = 1;
}

message UnassignModelFromProjectRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
  // Catalog model id (uuid) to unassign from the project.
  string model_id = 3 [(google.api.field_behavior) = REQUIRED];
}

message UnassignModelFromProjectResponse {
  // True when the assignment was removed.
  bool success = 1;
}
