// Inference ingress surface -- the public RPC customers call to issue a
// prompt. The control plane picks an eligible dataplane and forwards
// the request; this proto is the customer-visible shape.
//
// Skeleton: a single unary Prompt RPC. Streaming via SSE / server
// streaming lands when the dataplane filter chain matures (milestone C).
//
// Plan reference: B4 in check-ia-and-plan-optimized-cook.md.
syntax = "proto3";

package tars.inference.v1;

import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "tars/auth/v1/auth_options.proto";
import "tars/cli/v1/cli.proto";
import "tars/docs/v1/docs.proto";

option go_package = "github.com/tetrateio/fraser/api/tars/inference/v1;inferencev1";

// InferenceIngressService is the customer-visible inference surface.
service InferenceIngressService {
  // Prompt sends a prompt to an upstream LLM and returns the model's
  // completion. The gateway selects an eligible data plane, retrieves
  // and applies the project's BYOK key, enforces quota, records a
  // request log entry, and proxies the call to the upstream provider.
  // Streaming, multi-turn conversations, tools, and vision are not yet
  // supported on this path.
  rpc Prompt(PromptRequest) returns (PromptResponse) {
    option (tars.docs.v1.title) = "Send a prompt";
    option (google.api.http) = {
      post: "/v1/customers/{customer_id}/projects/{project_id}/prompts"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: ["prompt"]
      primary_flag: "prompt"
    };
  }

  // GetDataplaneURL returns the dataplane (gateway) URL for a single project.
  // The project is deduced from the caller's credential -- no input is required
  // in the common case: a caller who belongs to one project gets that project's
  // dataplane; one whose identity spans several gets their default (the oldest
  // membership). Pass project_id to disambiguate or target a specific project
  // (it is membership-checked). The customer is derived the same way. When the
  // credential itself already pins a workspace, that workspace is used directly.
  rpc GetDataplaneURL(GetDataplaneURLRequest) returns (GetDataplaneURLResponse) {
    option (tars.docs.v1.title) = "Get dataplane URL";
    option (google.api.http) = {get: "/v1/dataplane-url"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: ["dataplane-url"]
    };
  }

  // ListDataplaneURLs returns every dataplane URL the caller can reach -- one
  // per workspace under the customer deduced from the credential. Key-deduced;
  // no input. Use GetDataplaneURL when a single project's URL is wanted.
  rpc ListDataplaneURLs(ListDataplaneURLsRequest) returns (ListDataplaneURLsResponse) {
    option (tars.docs.v1.title) = "List dataplane URLs";
    option (google.api.http) = {get: "/v1/dataplane-urls"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: ["dataplane-urls"]
    };
  }
}

message PromptRequest {
  // Customer the prompt is billed/scoped against. When empty the server
  // resolves the caller's default customer from their session identity
  // (the first customer the caller is a member of, ordered by oldest
  // membership). Supply explicitly to override the session default.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];

  // Project the prompt is logged against. When empty the server resolves
  // the caller's default project from their session identity (the first
  // project the caller is a member of, ordered by oldest membership).
  // Supply explicitly to override the session default.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];

  // Model selector. A bare model name (e.g. gpt-4o) is accepted when it is
  // unique across the catalog; the server resolves its provider. Use the
  // provider/model form (e.g. openai/gpt-4o) to disambiguate when the same
  // model name exists under more than one provider. Empty selects the
  // project's default model.
  string model = 3;

  // The user prompt as plain text. Multi-message conversations land
  // when the proto grows a `messages` repeated field; the skeleton path
  // is a single user turn.
  string prompt = 4 [(google.api.field_behavior) = REQUIRED];

  // Optional client_keys / api_keys row id (UUID) to bind this request
  // to a specific customer-owned key for per-key ratelimit + fallback
  // accounting on the dataplane. Empty means "use the latest active
  // client key for the customer". The id flows through the minted MP
  // JWT's jti claim, which the dataplane reads in its ext-authz filter.
  string key_id = 5;

  // Idempotency key for safe retry of mutating prompts.
  string idempotency_key = 99 [(tars.cli.v1.field) = {hidden: true}];
}

message PromptResponse {
  // The model's reply text.
  string completion = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // The model that produced the reply.
  string model = 2 [(google.api.field_behavior) = OUTPUT_ONLY];

  // The dataplane that served the request, for observability. Empty in
  // milestone B (no dataplane yet); populated once milestone C lands.
  string dataplane_id = 3 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Token accounting (best effort; some providers don't report).
  int32 prompt_tokens = 4 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Tokens generated in the completion (output tokens), as reported by the
  // provider's usage block.
  int32 completion_tokens = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
}

message GetDataplaneURLRequest {
  // Optional project override. Empty resolves to the workspace the credential
  // pins (when it carries one) or the caller's default project (the oldest
  // membership). Supply explicitly to pick a specific project when the identity
  // spans several; the chosen project is membership-checked.
  string project_id = 1 [(tars.cli.v1.field) = {flag: "project"}];

  // Optional customer override. Empty resolves to the customer deduced from
  // the credential. Honored only for principals not already bound to a
  // customer; a cross-customer value is rejected unless the caller is a member.
  string customer_id = 2 [(tars.cli.v1.field) = {flag: "customer"}];
}

// DataplaneURL is a resolved gateway endpoint for one workspace.
message DataplaneURL {
  // Resolved gateway URL (scheme + host + port).
  string url = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Customer that owns the workspace.
  string customer_id = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Project the workspace belongs to (empty if the workspace has no project).
  string project_id = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Workspace the URL routes to.
  string workspace_id = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Optional display name.
  string name = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
}

message GetDataplaneURLResponse {
  DataplaneURL dataplane = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// Customer is deduced from the credential; no fields.
message ListDataplaneURLsRequest {}

message ListDataplaneURLsResponse {
  repeated DataplaneURL dataplanes = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
}
