openapi: 3.1.0
info:
  title: CostVane API
  version: 1.0.0
  description: Privacy-first AI cost estimation, attribution, and budget enforcement. Prompts, outputs, and provider credentials are never required.
  contact:
    email: support@costvane.com
servers:
  - url: https://api.costvane.com/v1
    description: Production
security:
  - ApiKey: []
paths:
  /estimate:
    post:
      summary: Estimate an AI request cost
      operationId: estimateCost
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CostInput' }
      responses:
        '200': { description: Cost estimate, content: { application/json: { schema: { $ref: '#/components/schemas/Estimate' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /preflight:
    post:
      summary: Estimate cost and evaluate budgets before execution
      operationId: preflight
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AttributedCostInput' }
      responses:
        '200':
          description: Allow, warn, or block decision
          content:
            application/json:
              schema:
                type: object
                required: [decision, estimate, budgets]
                properties:
                  decision: { type: string, enum: [allow, warn, block] }
                  estimate: { $ref: '#/components/schemas/Estimate' }
                  budgets: { type: array, items: { $ref: '#/components/schemas/BudgetEvaluation' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /usage:
    post:
      summary: Record actual usage and cost
      operationId: recordUsage
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 200 }
          description: Prevents duplicate ingestion for the authenticated API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - { $ref: '#/components/schemas/AttributedCostInput' }
                - type: object
                  properties:
                    actualCost: { type: number, minimum: 0, description: Provider-reported USD cost; catalog estimate is used when omitted. }
                    timestamp: { type: string, format: date-time }
                    requestId: { type: string, maxLength: 200 }
                    idempotencyKey: { type: string, maxLength: 200 }
      responses:
        '200':
          description: Recorded or previously recorded idempotent duplicate
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [recorded, duplicate] }
                  requestId: { type: string }
                  actualCost: { type: number }
                  estimatedCost: { type: [number, 'null'] }
                  costSource: { type: string, enum: [catalog, provider_reported] }
                  budgetDecision: { type: string, enum: [allow, warn, block] }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /usage/summary:
    get:
      summary: Summarize attributed usage and cost
      operationId: getUsageSummary
      parameters:
        - in: query
          name: period
          schema: { type: string, enum: [day, week, month], default: month }
        - in: query
          name: groupBy
          schema: { type: string, enum: [provider, model, customer, feature, workflow, team, project, environment], default: provider }
      responses:
        '200':
          description: Totals, estimate variance, and grouped breakdown
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UsageSummary' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /budgets:
    get:
      summary: List budgets
      operationId: listBudgets
      responses:
        '200':
          description: Budget list
          content: { application/json: { schema: { type: object, properties: { budgets: { type: array, items: { $ref: '#/components/schemas/Budget' } } } } } }
    post:
      summary: Create a dollar or token budget
      operationId: createBudget
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/BudgetInput' } } }
      responses:
        '201': { description: Budget created, content: { application/json: { schema: { $ref: '#/components/schemas/Budget' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /budgets/{id}:
    delete:
      summary: Delete a budget
      operationId: deleteBudget
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }
  /catalog/status:
    get:
      summary: Get pricing catalog provenance and freshness
      operationId: catalogStatus
      responses:
        '200': { description: Catalog status }
  /catalog/models:
    get:
      summary: List priced models
      operationId: listModels
      parameters:
        - { in: query, name: provider, schema: { type: string, enum: [openai, anthropic, google, openrouter] } }
      responses:
        '200': { description: Model prices and source URLs }
  /webhooks:
    get:
      summary: List budget webhook endpoints
      operationId: listWebhooks
      responses:
        '200': { description: Webhook list; signing secrets are never returned. }
    post:
      summary: Register an encrypted, signed budget webhook
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, secret]
              properties:
                url: { type: string, format: uri, pattern: '^https://' }
                secret: { type: string, minLength: 24, maxLength: 200, writeOnly: true }
      responses:
        '201': { description: Webhook registered }
        '400': { $ref: '#/components/responses/BadRequest' }
        '503': { description: Operator encryption key is not configured }
  /webhooks/{id}:
    delete:
      summary: Delete a webhook
      operationId: deleteWebhook
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }
components:
  securitySchemes:
    ApiKey: { type: apiKey, in: header, name: X-API-Key }
  schemas:
    CostInput:
      type: object
      required: [provider, model, inputTokens, outputTokens]
      properties:
        provider: { type: string, enum: [openai, anthropic, google, openrouter] }
        model: { type: string }
        inputTokens: { type: integer, minimum: 0, maximum: 10000000 }
        cachedInputTokens: { type: integer, minimum: 0, maximum: 10000000, default: 0 }
        outputTokens: { type: integer, minimum: 0, maximum: 10000000 }
    AttributedCostInput:
      allOf:
        - { $ref: '#/components/schemas/CostInput' }
        - type: object
          properties:
            customerId: { type: string, maxLength: 200 }
            feature: { type: string, maxLength: 200 }
            workflow: { type: string, maxLength: 200 }
            team: { type: string, maxLength: 200 }
            project: { type: string, maxLength: 200 }
            environment: { type: string, maxLength: 200 }
            tags: { type: object, maxProperties: 20, additionalProperties: { type: [string, number, boolean] } }
    Estimate:
      type: object
      properties:
        provider: { type: string }
        model: { type: string }
        inputCost: { type: number }
        cachedInputCost: { type: number }
        outputCost: { type: number }
        totalCost: { type: number }
        currency: { type: string, const: USD }
        pricingCatalogVersion: { type: string }
    BudgetInput:
      type: object
      required: [name, limitType, limitAmount]
      properties:
        name: { type: string, maxLength: 100 }
        scopeType: { type: string, enum: [all, customer, feature, workflow, team, project, environment], default: all }
        scopeValue: { type: string, description: Required unless scopeType is all. }
        limitType: { type: string, enum: [usd, tokens] }
        limitAmount: { type: number, exclusiveMinimum: 0 }
        period: { type: string, enum: [day, week, month], default: month }
        enforcement: { type: string, enum: [warn, block], default: warn }
        warningPercent: { type: integer, minimum: 1, maximum: 100, default: 80 }
    Budget:
      allOf:
        - { $ref: '#/components/schemas/BudgetInput' }
        - type: object
          properties:
            id: { type: integer }
            enabled: { type: boolean }
            createdAt: { type: string, format: date-time }
    BudgetEvaluation:
      type: object
      properties:
        budgetId: { type: integer }
        name: { type: string }
        status: { type: string, enum: [allow, warn, block] }
        current: { type: number }
        projected: { type: number }
        percentUsed: { type: number }
    UsageSummary:
      type: object
      properties:
        period: { type: string }
        groupBy: { type: string }
        totalRequests: { type: integer }
        totalInputTokens: { type: integer }
        totalOutputTokens: { type: integer }
        totalCost: { type: number }
        totalEstimatedCost: { type: number }
        variance: { type: number }
        breakdown: { type: array, items: { type: object } }
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
  responses:
    BadRequest: { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Unauthorized: { description: Missing or invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    TooManyRequests: { description: Rate limit or quota exceeded, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    NotFound: { description: Resource not found, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
