---
phase: 04-ai-summaries
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/CircuitBreaker.swift
autonomous: true
requirements: [SUM-04]

must_haves:
  truths:
    - "MiniMax API calls succeed with valid credentials and return parsed text"
    - "After 3 consecutive API failures, all summary calls are blocked for 5 minutes"
    - "After 5-minute cooldown expires, summary calls resume automatically"
    - "API key is read from MINIMAX_API_KEY environment variable"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift"
      provides: "MiniMax API configuration constants"
      contains: "miniMaxAPIKey"
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift"
      provides: "URLSession-based MiniMax API client with Anthropic-compatible endpoint"
      exports: ["MiniMaxClient", "MiniMaxResult"]
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/CircuitBreaker.swift"
      provides: "Generic circuit breaker with consecutive failure tracking and timed cooldown"
      exports: ["CircuitBreaker", "SummaryError"]
  key_links:
    - from: "MiniMaxClient.swift"
      to: "Config.swift"
      via: "Config.miniMaxAPIKey, Config.miniMaxBaseURL, Config.miniMaxModel"
      pattern: "Config\\.miniMax"
    - from: "MiniMaxClient.swift"
      to: "CircuitBreaker.swift"
      via: "CircuitBreaker instance checked before API call"
      pattern: "circuitBreaker\\.isOpen"
---

<objective>
Create the MiniMax API client and circuit breaker that all three summary types depend on.

Purpose: Foundation layer for AI summaries -- API communication and failure resilience must exist before prompt templates can be built on top.
Output: MiniMaxClient.swift (URLSession API client), CircuitBreaker.swift (failure tracking), Config.swift additions (API constants)
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TTSEngine.swift

<interfaces>
<!-- Existing Config pattern to extend -->
From Config.swift:
```swift
enum Config {
    static let sherpaOnnxPath: String = { ... }()
    static let appName = "claude-tts-companion"
}
```

<!-- Existing error pattern to follow -->

From TTSEngine.swift:

```swift
enum TTSError: Error, CustomStringConvertible {
    case modelLoadFailed(path: String)
    case synthesisReturnedNil
    case wavWriteFailed(path: String)
    var description: String { ... }
}
```

<!-- Existing Logger/queue pattern -->

From TTSEngine.swift:

```swift
private let logger = Logger(label: "tts-engine")
private let queue = DispatchQueue(label: "com.terryli.tts-engine", qos: .userInitiated)
private let lock = NSLock()
```

<!-- Reference: TypeScript MiniMax API call pattern (Anthropic-compatible) -->

From existing TS implementation:

- Endpoint: `${baseUrl}/v1/messages` (POST)
- Headers: Content-Type: application/json, x-api-key: {key}, anthropic-version: 2024-10-22
- Body: { model, max_tokens, system: systemPrompt, messages: [{ role: "user", content: prompt }] }
- Response: { content: [{ type: "text", text: "..." }], usage: { output_tokens: N } }
- Filter out thinking blocks: content.filter(b => b.type == "text")
  </interfaces>

</context>

<tasks>

<task type="auto">
  <name>Task 1: Add MiniMax config constants and create CircuitBreaker</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/CircuitBreaker.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/TTSEngine.swift
  </read_first>
  <action>
1. Extend Config.swift with MiniMax settings:
   - `static let miniMaxAPIKey: String?` — reads from `ProcessInfo.processInfo.environment["MINIMAX_API_KEY"]`
   - `static let miniMaxBaseURL: String` — reads from env `SUMMARY_BASE_URL` with default `"https://api.minimax.chat"`
   - `static let miniMaxModel: String` — reads from env `SUMMARY_MODEL` or `MINIMAX_MODEL` with default `"MiniMax-M1-80k"`
   - `static let summaryMaxTokens: Int` — reads from env `SUMMARY_MAX_TOKENS` with default `2048`

1. Create CircuitBreaker.swift with:
   - `final class CircuitBreaker: @unchecked Sendable` (thread-safe with NSLock, matching TTSEngine pattern)
   - Properties: `private var consecutiveFailures: Int = 0`, `private var disabledUntil: Date = .distantPast`, `private let lock = NSLock()`, `private let logger = Logger(label: "circuit-breaker")`
   - Constants: `let maxFailures: Int` (default 3), `let cooldownSeconds: TimeInterval` (default 300 = 5 min)
   - `init(maxFailures: Int = 3, cooldownSeconds: TimeInterval = 300)`
   - `var isOpen: Bool` — returns true if `Date() < disabledUntil`, resets state if cooldown expired (log reset)
   - `func recordFailure()` — increments counter, if >= maxFailures sets `disabledUntil = Date() + cooldownSeconds` and logs warning
   - `func recordSuccess()` — resets consecutiveFailures to 0
   - `var failureCount: Int` — read-only accessor for testing/status

2. Create SummaryError enum in CircuitBreaker.swift:
   - `enum SummaryError: Error, CustomStringConvertible` with cases:
     - `.circuitBreakerOpen` — "Summary API disabled: circuit breaker open (cooldown active)"
     - `.missingAPIKey` — "MINIMAX_API_KEY environment variable not set"
     - `.apiError(statusCode: Int, body: String)` — "MiniMax API error {statusCode}: {body}"
     - `.emptyResponse` — "MiniMax API returned empty text content"
     - `.decodingError(String)` — "Failed to decode MiniMax response: {detail}"
       </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - Config.swift contains `static let miniMaxAPIKey` reading from environment
    - Config.swift contains `static let miniMaxBaseURL` with default URL string
    - Config.swift contains `static let miniMaxModel` reading from environment
    - CircuitBreaker.swift exists with `class CircuitBreaker`
    - CircuitBreaker.swift contains `var isOpen: Bool`
    - CircuitBreaker.swift contains `func recordFailure()`
    - CircuitBreaker.swift contains `func recordSuccess()`
    - CircuitBreaker.swift contains `enum SummaryError`
    - CircuitBreaker.swift contains `case circuitBreakerOpen`
    - CircuitBreaker.swift contains `case apiError`
    - `swift build` succeeds with zero errors
  </acceptance_criteria>
  <done>Config has MiniMax constants, CircuitBreaker tracks 3 consecutive failures with 5-min cooldown, SummaryError covers all failure modes, project compiles</done>
</task>

<task type="auto">
  <name>Task 2: Create MiniMaxClient with URLSession API calls</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/MiniMaxClient.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/CircuitBreaker.swift
  </read_first>
  <action>
Create MiniMaxClient.swift following the Anthropic-compatible API pattern from the existing TypeScript implementation.

1. Define response types:

   ```swift
   struct MiniMaxResult {
       let text: String
       let durationMs: Int
       let outputTokens: Int?
   }
   ```

2. Create `final class MiniMaxClient: @unchecked Sendable`:
   - `private let logger = Logger(label: "minimax-client")`
   - `private let session = URLSession.shared`
   - `let circuitBreaker: CircuitBreaker` (public for status queries)
   - `init(circuitBreaker: CircuitBreaker = CircuitBreaker())`

3. Implement `func query(prompt: String, systemPrompt: String, maxTokens: Int = Config.summaryMaxTokens) async throws -> MiniMaxResult`:
   - Check `circuitBreaker.isOpen` first — throw `.circuitBreakerOpen` if open
   - Check `Config.miniMaxAPIKey` is non-nil — throw `.missingAPIKey` if nil
   - Build URLRequest to `"\(Config.miniMaxBaseURL)/v1/messages"` (POST)
   - Set headers: `Content-Type: application/json`, `x-api-key: {apiKey}`, `anthropic-version: 2024-10-22`
   - Encode JSON body: `{ "model": Config.miniMaxModel, "max_tokens": maxTokens, "system": systemPrompt, "messages": [{"role": "user", "content": prompt}] }`
   - Use `try await session.data(for: request)` (async/await, no completion handlers)
   - Check HTTP status — if not 200, call `circuitBreaker.recordFailure()` and throw `.apiError(statusCode:body:)`
   - Decode response JSON manually using JSONSerialization (not Codable — the response shape varies):
     - Extract `content` array, filter to entries where `type == "text"`, concatenate `text` values
     - Extract `usage.output_tokens` if present
   - If text is empty after filtering, call `circuitBreaker.recordFailure()` and throw `.emptyResponse`
   - On success, call `circuitBreaker.recordSuccess()`
   - Log: model, duration, output tokens, text length
   - Return `MiniMaxResult(text: trimmedText, durationMs: elapsed, outputTokens: outputTokens)`

4. Use `import Foundation` and `import Logging` only — no new dependencies.
   </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - MiniMaxClient.swift exists with `class MiniMaxClient`
    - MiniMaxClient.swift contains `func query(prompt:` with `async throws`
    - MiniMaxClient.swift contains `circuitBreaker.isOpen`
    - MiniMaxClient.swift contains `circuitBreaker.recordFailure()`
    - MiniMaxClient.swift contains `circuitBreaker.recordSuccess()`
    - MiniMaxClient.swift contains `x-api-key`
    - MiniMaxClient.swift contains `anthropic-version`
    - MiniMaxClient.swift contains `v1/messages`
    - MiniMaxClient.swift contains `struct MiniMaxResult`
    - `swift build` succeeds with zero errors
  </acceptance_criteria>
  <done>MiniMaxClient makes Anthropic-compatible API calls via URLSession, integrates circuit breaker for failure tracking, handles all error cases, compiles cleanly</done>
</task>

</tasks>

<verification>
- `swift build` succeeds in plugins/claude-tts-companion
- Config.swift has miniMaxAPIKey, miniMaxBaseURL, miniMaxModel constants
- CircuitBreaker tracks consecutive failures and auto-resets after cooldown
- MiniMaxClient.query() is async/throws with circuit breaker integration
- SummaryError covers all failure modes
</verification>

<success_criteria>

- Project compiles with `swift build`
- MiniMaxClient can make API calls (validated by compilation; runtime requires API key)
- Circuit breaker opens after 3 failures and resets after 5 minutes
- No new SwiftPM dependencies added
  </success_criteria>

<output>
After completion, create `.planning/phases/04-ai-summaries/04-01-SUMMARY.md`
</output>
