diff --git a/.env.example b/.env.example index 52a5dbe..35b0435 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ -OC_GO_CC_API_KEY=sk-opencode-your-key-here -OC_GO_CC_HOST=0.0.0.0 -OC_GO_CC_PORT=3456 +# Primary environment variables (recommended) +ROUTATIC_PROXY_API_KEY=sk-opencode-your-key-here +ROUTATIC_PROXY_HOST=0.0.0.0 +ROUTATIC_PROXY_PORT=3456 + +# Legacy fallback names (still supported for migration) +# OC_GO_CC_API_KEY=sk-opencode-your-key-here +# OC_GO_CC_HOST=0.0.0.0 +# OC_GO_CC_PORT=3456 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 785d5ff..218cae2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: run: go test ./... -v -race - name: Build - run: go build ./cmd/oc-go-cc + run: go build ./cmd/routatic-proxy lint: name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f32ecc1..42a219a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: run: go test ./... -v -race - name: Build (sanity check) - run: go build -o /dev/null ./cmd/oc-go-cc + run: go build -o /dev/null ./cmd/routatic-proxy # ── Stage 2: Create Release ────────────────────────────────────── release: @@ -100,8 +100,8 @@ jobs: echo "Building ${NAME}..." CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \ go build -ldflags "$LDFLAGS -s -w" \ - -o "dist/oc-go-cc_${NAME}${EXT}" \ - ./cmd/oc-go-cc + -o "dist/routatic-proxy_${NAME}${EXT}" \ + ./cmd/routatic-proxy done echo "" @@ -111,7 +111,7 @@ jobs: - name: Generate checksums run: | cd dist - sha256sum oc-go-cc_* > checksums.txt + sha256sum routatic-proxy_* > checksums.txt cat checksums.txt - name: Generate AI Changelog @@ -140,7 +140,7 @@ jobs: COMMITS=$(git log "${PREVIOUS_TAG}..HEAD" --pretty=format:"%H%n%s%n%b%n---COMMIT_SEP---") FILE_CHANGES=$(git diff --stat "${PREVIOUS_TAG}..HEAD") - PROMPT="You are a technical writer generating release notes for a Go proxy server project (oc-go-cc). + PROMPT="You are a technical writer generating release notes for a Go proxy server project (routatic-proxy). Analyze the provided git commits and file changes, then generate a well-structured changelog in Markdown format. Follow these guidelines: @@ -213,7 +213,7 @@ jobs: draft: false prerelease: false files: | - dist/oc-go-cc_* + dist/routatic-proxy_* dist/checksums.txt # ── Stage 3: Publish Docker Image ─────────────────────────────── @@ -223,7 +223,7 @@ jobs: permissions: contents: read packages: write - if: github.repository == 'samueltuyizere/oc-go-cc' + if: github.repository == 'routatic/proxy' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -263,7 +263,7 @@ jobs: homebrew: name: Update Homebrew Tap needs: release - if: github.repository == 'samueltuyizere/oc-go-cc' + if: github.repository == 'routatic/proxy' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -313,58 +313,59 @@ jobs: git clone "https://x-access-token:${PAT}@github.com/${TAP_REPO}.git" tap-repo cd tap-repo - mkdir -p Formula + mkdir -p Formula Aliases - cat > Formula/oc-go-cc.rb << RUBY - class OcGoCc < Formula + cat > Formula/routatic-proxy.rb << RUBY + class RoutaticProxy < Formula desc "Proxy Claude Code requests to OpenCode Go API" homepage "https://github.com/${REPO}" version "${VERSION}" on_macos do if Hardware::CPU.arm? - url "${BASE_URL}/oc-go-cc_darwin-arm64" + url "${BASE_URL}/routatic-proxy_darwin-arm64" sha256 "${DARWIN_ARM64}" else - url "${BASE_URL}/oc-go-cc_darwin-amd64" + url "${BASE_URL}/routatic-proxy_darwin-amd64" sha256 "${DARWIN_AMD64}" end end on_linux do if Hardware::CPU.intel? - url "${BASE_URL}/oc-go-cc_linux-amd64" + url "${BASE_URL}/routatic-proxy_linux-amd64" sha256 "${LINUX_AMD64}" else - url "${BASE_URL}/oc-go-cc_linux-arm64" + url "${BASE_URL}/routatic-proxy_linux-arm64" sha256 "${LINUX_ARM64}" end end def install if OS.mac? && Hardware::CPU.arm? - bin.install "oc-go-cc_darwin-arm64" => "oc-go-cc" + bin.install "routatic-proxy_darwin-arm64" => "routatic-proxy" elsif OS.mac? - bin.install "oc-go-cc_darwin-amd64" => "oc-go-cc" + bin.install "routatic-proxy_darwin-amd64" => "routatic-proxy" elsif OS.linux? && Hardware::CPU.intel? - bin.install "oc-go-cc_linux-amd64" => "oc-go-cc" + bin.install "routatic-proxy_linux-amd64" => "routatic-proxy" else - bin.install "oc-go-cc_linux-arm64" => "oc-go-cc" + bin.install "routatic-proxy_linux-arm64" => "routatic-proxy" end + bin.install_symlink bin/"routatic-proxy" => "oc-go-cc" end def caveats <<~EOS - To get started with oc-go-cc: + To get started with routatic-proxy: 1. Initialize configuration: - oc-go-cc init + routatic-proxy init 2. Set your OpenCode Go API key: - export OC_GO_CC_API_KEY=sk-opencode-your-key + export ROUTATIC_PROXY_API_KEY=sk-opencode-your-key 3. Start the proxy: - oc-go-cc serve + routatic-proxy serve 4. Configure Claude Code: export ANTHROPIC_BASE_URL=http://127.0.0.1:3456 @@ -376,22 +377,25 @@ jobs: end test do + system "#{bin}/routatic-proxy", "--version" system "#{bin}/oc-go-cc", "--version" end end RUBY - git add Formula/oc-go-cc.rb + ln -sf ../Formula/routatic-proxy.rb Aliases/oc-go-cc + + git add Formula/routatic-proxy.rb Aliases/oc-go-cc git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -m "Update oc-go-cc to ${VERSION}" || echo "No changes to commit" + git commit -m "Update routatic-proxy to ${VERSION}" || echo "No changes to commit" git push # ── Stage 5: Update Scoop Bucket ────────────────────────────────── scoop: name: Update Scoop Bucket needs: release - if: github.repository == 'samueltuyizere/oc-go-cc' + if: github.repository == 'routatic/proxy' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -437,7 +441,7 @@ jobs: mkdir -p bucket - cat > bucket/oc-go-cc.json << JSON + cat > bucket/routatic-proxy.json << JSON { "version": "${VERSION}", "description": "Proxy Claude Code requests to OpenCode Go API", @@ -445,34 +449,39 @@ jobs: "license": "AGPL-3.0-only", "architecture": { "64bit": { - "url": "${BASE_URL}/oc-go-cc_windows-amd64.exe", + "url": "${BASE_URL}/routatic-proxy_windows-amd64.exe", "hash": "${WINDOWS_AMD64}" }, "arm64": { - "url": "${BASE_URL}/oc-go-cc_windows-arm64.exe", + "url": "${BASE_URL}/routatic-proxy_windows-arm64.exe", "hash": "${WINDOWS_ARM64}" } }, - "bin": "oc-go-cc.exe", - "post_install": "Get-ChildItem \$dir\\\\oc-go-cc_windows-*.exe | Rename-Item -NewName oc-go-cc.exe", + "bin": [ + ["routatic-proxy.exe", "routatic-proxy"], + ["routatic-proxy.exe", "oc-go-cc"] + ], + "post_install": "Get-ChildItem \$dir\\\\routatic-proxy_windows-*.exe | Rename-Item -NewName routatic-proxy.exe", "checkver": { "github": "https://github.com/${REPO}" }, "autoupdate": { "architecture": { "64bit": { - "url": "https://github.com/${REPO}/releases/download/v\$version/oc-go-cc_windows-amd64.exe" + "url": "https://github.com/${REPO}/releases/download/v\$version/routatic-proxy_windows-amd64.exe" }, "arm64": { - "url": "https://github.com/${REPO}/releases/download/v\$version/oc-go-cc_windows-arm64.exe" + "url": "https://github.com/${REPO}/releases/download/v\$version/routatic-proxy_windows-arm64.exe" } } } } JSON - git add bucket/oc-go-cc.json + cp bucket/routatic-proxy.json bucket/oc-go-cc.json + + git add bucket/routatic-proxy.json bucket/oc-go-cc.json git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -m "Update oc-go-cc to ${VERSION}" || echo "No changes to commit" + git commit -m "Update routatic-proxy to ${VERSION}" || echo "No changes to commit" git push diff --git a/CLAUDE.md b/CLAUDE.md index 59a1130..f35858a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands ```bash -make build # Build binary to bin/oc-go-cc +make build # Build binary to bin/routatic-proxy make run # Run without building make test # Run tests with race detector make lint # go vet + test @@ -18,9 +18,9 @@ Run a single test: `go test ./internal/router/ -v` ## Architecture -**Purpose:** oc-go-cc is a proxy server that sits between Claude Code and OpenCode Go. It intercepts Anthropic API requests, transforms them to OpenAI Chat Completions format, forwards them to OpenCode Go, and transforms responses back to Anthropic SSE. +**Purpose:** routatic-proxy is a proxy server that sits between Claude Code and OpenCode Go. It intercepts Anthropic API requests, transforms them to OpenAI Chat Completions format, forwards them to OpenCode Go, and transforms responses back to Anthropic SSE. -**Model routing is config-driven, not code-driven.** All models are defined in `~/.config/oc-go-cc/config.json` — adding a new model requires no code changes. Go provider models are transformed to OpenAI Chat Completions format automatically. Zen models use endpoint classification via `ClassifyEndpoint()`. The router in `internal/router/` selects models by matching request content against scenario patterns defined in `scenarios.go`. +**Model routing is config-driven, not code-driven.** All models are defined in `~/.config/routatic-proxy/config.json` — adding a new model requires no code changes. Go provider models are transformed to OpenAI Chat Completions format automatically. Zen models use endpoint classification via `ClassifyEndpoint()`. The router in `internal/router/` selects models by matching request content against scenario patterns defined in `scenarios.go`. If a model's upstream doesn't support Anthropic tool format (`type: "custom"` server-tool shorthands), set `"anthropic_tools_disabled": true` in the model config to force it through the Chat Completions transform path instead of the raw Anthropic endpoint. @@ -47,7 +47,7 @@ For streaming, the router downgrades to fast models (Qwen3.6 Plus) for better TT ## Key Files -- `cmd/oc-go-cc/main.go` — CLI entry point (cobra). Default config template is generated here. +- `cmd/routatic-proxy/main.go` — CLI entry point (cobra). Default config template is generated here. - `internal/config/` — Config types and JSON loader with `${VAR}` env interpolation. - `internal/transformer/` — Request/response format conversion (Anthropic ↔ OpenAI). - `internal/router/fallback.go` — Circuit breaker per model (3 failures = 30s skip). diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 11c6b36..3930e0e 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -2,15 +2,17 @@ ## Config File -Location: `~/.config/oc-go-cc/config.json` +Location: `~/.config/routatic-proxy/config.json` -Override with `OC_GO_CC_CONFIG` environment variable. +Override with `ROUTATIC_PROXY_CONFIG` environment variable. + +For migration, `~/.config/oc-go-cc/config.json` is loaded when the new config file does not exist, and every `OC_GO_CC_*` environment variable is still accepted as a fallback for its `ROUTATIC_PROXY_*` replacement. ## Full Config Reference ```json { - "api_key": "${OC_GO_CC_API_KEY}", + "api_key": "${ROUTATIC_PROXY_API_KEY}", "host": "127.0.0.1", "port": 3456, "hot_reload": false, @@ -115,7 +117,7 @@ Override with `OC_GO_CC_CONFIG` environment variable. ## Providers -oc-go-cc supports two providers for upstream API calls: +routatic-proxy supports two providers for upstream API calls: ### OpenCode Go (`opencode-go`) @@ -139,13 +141,15 @@ Environment variables override config file values. Config values also support `$ | Variable | Description | Default | | ----------------------- | ------------------------------------------- | ------------------------------------------------ | -| `OC_GO_CC_API_KEY` | OpenCode Go API key (**required**) | — | -| `OC_GO_CC_CONFIG` | Custom config file path | `~/.config/oc-go-cc/config.json` | -| `OC_GO_CC_HOST` | Proxy listen host | `127.0.0.1` | -| `OC_GO_CC_PORT` | Proxy listen port | `3456` | -| `OC_GO_CC_OPENCODE_URL` | OpenCode Go API endpoint | `https://opencode.ai/zen/go/v1/chat/completions` | -| `OC_GO_CC_OPENCODE_ZEN_URL` | OpenCode Zen API endpoint | `https://opencode.ai/zen/v1/chat/completions` | -| `OC_GO_CC_LOG_LEVEL` | Log level: `debug`, `info`, `warn`, `error` | `info` | +| `ROUTATIC_PROXY_API_KEY` | OpenCode Go API key (**required**) | — | +| `ROUTATIC_PROXY_CONFIG` | Custom config file path | `~/.config/routatic-proxy/config.json` | +| `ROUTATIC_PROXY_HOST` | Proxy listen host | `127.0.0.1` | +| `ROUTATIC_PROXY_PORT` | Proxy listen port | `3456` | +| `ROUTATIC_PROXY_OPENCODE_URL` | OpenCode Go API endpoint | `https://opencode.ai/zen/go/v1/chat/completions` | +| `ROUTATIC_PROXY_OPENCODE_ZEN_URL` | OpenCode Zen API endpoint | `https://opencode.ai/zen/v1/chat/completions` | +| `ROUTATIC_PROXY_LOG_LEVEL` | Log level: `debug`, `info`, `warn`, `error` | `info` | + +Legacy equivalents such as `OC_GO_CC_API_KEY`, `OC_GO_CC_CONFIG`, and `OC_GO_CC_PORT` continue to work. When both names are set, the `ROUTATIC_PROXY_*` value wins. ## Hot Reload @@ -232,7 +236,7 @@ When a request arrives, the proxy checks `model_overrides[]` **first**. I Each entry accepts the same fields as a `ModelConfig` (`provider`, `model_id`, `temperature`, `max_tokens`, `reasoning_effort`, `thinking`, etc.). `model_id` is required; `provider` must be `"opencode-go"` or `"opencode-zen"` (or omitted to inherit the default). -See `oc-go-cc models` for the complete list of available Zen models across all endpoint types (Claude, GPT, Gemini, and free-tier). +See `routatic-proxy models` for the complete list of available Zen models across all endpoint types (Claude, GPT, Gemini, and free-tier). ### Routing precedence diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0838ba8..8ff2b45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,14 +31,14 @@ Run a single test: `go test ./internal/router/ -v` ``` ┌─────────────┐ Anthropic API ┌─────────────┐ OpenAI/Gemini/Responses ┌─────────────┐ -│ Claude Code ├──────────────────────►│ oc-go-cc ├─────────────────────────────►│ OpenCode │ +│ Claude Code ├──────────────────────►│ routatic-proxy ├─────────────────────────────►│ OpenCode │ │ (CLI) │ POST /v1/messages │ (Proxy) │ Multiple endpoint formats │ (Upstream) │ │ │◄──────────────────────┤ │◄─────────────────────────────┤ │ └─────────────┘ Anthropic SSE └─────────────┘ Format-appropriate SSE └─────────────┘ ``` 1. Claude Code sends a request in [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) format -2. oc-go-cc parses the request, counts tokens, and selects a model via routing rules +2. routatic-proxy parses the request, counts tokens, and selects a model via routing rules 3. Based on the model's provider and endpoint type, the request is transformed to the appropriate format: - **OpenAI Chat Completions** — for most OpenCode Go and Zen models - **Anthropic Messages** — for MiniMax models (sent directly without transformation) @@ -79,7 +79,7 @@ For Claude Code and other agentic coding workflows, configure DeepSeek V4 models } ``` -`oc-go-cc` forwards these fields to OpenCode Go as OpenAI Chat Completions parameters: +`routatic-proxy` forwards these fields to OpenCode Go as OpenAI Chat Completions parameters: - `reasoning_effort`: controls DeepSeek V4 thinking effort (`high` or `max`) - `thinking`: enables or disables DeepSeek V4 thinking mode @@ -89,7 +89,7 @@ DeepSeek V4 thinking responses are returned as OpenAI `reasoning_content` and tr ## Architecture ``` -cmd/oc-go-cc/main.go CLI entry point (cobra commands) +cmd/routatic-proxy/main.go CLI entry point (cobra commands) internal/ ├── config/ │ ├── config.go Config types (OpenCodeGoConfig, OpenCodeZenConfig) @@ -130,7 +130,7 @@ configs/ - **Polymorphic field handling**: Anthropic's `system` and `content` fields accept both strings and arrays. We use `json.RawMessage` with accessor methods (`SystemText()`, `ContentBlocks()`) to handle both formats correctly. - **Real-time stream proxying**: SSE events are transformed in-flight, not buffered. This means Claude Code sees responses as they arrive from upstream. - **Circuit breaker per model**: Each model gets its own circuit breaker. After 3 consecutive failures, the model is skipped for 30 seconds, then tested again. -- **Environment variable interpolation**: Config values like `"${OC_GO_CC_API_KEY}"` are resolved at load time, so you never need to put secrets in the config file. +- **Environment variable interpolation**: Config values like `"${ROUTATIC_PROXY_API_KEY}"` are resolved at load time, so you never need to put secrets in the config file. - **Provider-aware routing**: The `provider` field in model config determines which upstream service to use (Go or Zen). Zen models are further classified by endpoint type (Chat Completions, Anthropic, Responses, Gemini). ## API Endpoints diff --git a/Dockerfile b/Dockerfile index 94f2fff..4d7af48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,26 +4,27 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . ARG VERSION=docker -RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" -o /app/oc-go-cc ./cmd/oc-go-cc +RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" -o /app/routatic-proxy ./cmd/routatic-proxy FROM alpine:3.21 RUN apk add --no-cache ca-certificates tzdata wget && \ addgroup -S appgroup && adduser -S appuser -G appgroup && \ - mkdir -p /etc/oc-go-cc && \ - chown -R appuser:appgroup /etc/oc-go-cc + mkdir -p /etc/routatic-proxy && \ + chown -R appuser:appgroup /etc/routatic-proxy -COPY --from=builder /app/oc-go-cc /usr/local/bin/oc-go-cc -COPY --from=builder /app/configs/config.example.json /etc/oc-go-cc/config.json -RUN chown -R appuser:appgroup /etc/oc-go-cc +COPY --from=builder /app/routatic-proxy /usr/local/bin/routatic-proxy +RUN ln -s /usr/local/bin/routatic-proxy /usr/local/bin/oc-go-cc +COPY --from=builder /app/configs/config.example.json /etc/routatic-proxy/config.json +RUN chown -R appuser:appgroup /etc/routatic-proxy USER appuser -ENV OC_GO_CC_CONFIG=/etc/oc-go-cc/config.json -ENV OC_GO_CC_HOST=0.0.0.0 +ENV ROUTATIC_PROXY_CONFIG=/etc/routatic-proxy/config.json +ENV ROUTATIC_PROXY_HOST=0.0.0.0 EXPOSE 3456 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3456/health || exit 1 -ENTRYPOINT ["oc-go-cc", "serve"] +ENTRYPOINT ["routatic-proxy", "serve"] diff --git a/INSTALLATION.md b/INSTALLATION.md index bad1e5e..ea3c056 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -3,60 +3,63 @@ ## Homebrew (macOS & Linux) ```bash -brew tap samueltuyizere/tap -brew install oc-go-cc +brew tap routatic/tap +brew install routatic-proxy ``` ## Scoop (Windows) ```powershell -scoop bucket add oc-go-cc https://github.com/samueltuyizere/scoop-bucket -scoop install oc-go-cc +scoop bucket add routatic https://github.com/routatic/scoop-bucket +scoop install routatic-proxy ``` ## Build from Source ```bash -git clone https://github.com/samueltuyizere/oc-go-cc.git -cd oc-go-cc +git clone https://github.com/routatic/proxy.git +cd proxy make build -# Binary is at bin/oc-go-cc +# Binary is at bin/routatic-proxy +# bin/oc-go-cc is created as a compatibility alias # Optionally install to $GOPATH/bin make install ``` ## Download a Release Binary -Download the latest release for your platform from the [Releases page](https://github.com/samueltuyizere/oc-go-cc/releases): +Download the latest release for your platform from the [Releases page](https://github.com/routatic/proxy/releases): | Platform | File | | --------------------- | ---------------------------- | -| macOS (Apple Silicon) | `oc-go-cc_darwin-arm64` | -| macOS (Intel) | `oc-go-cc_darwin-amd64` | -| Linux (x86_64) | `oc-go-cc_linux-amd64` | -| Linux (ARM64) | `oc-go-cc_linux-arm64` | -| Windows (x86_64) | `oc-go-cc_windows-amd64.exe` | -| Windows (ARM64) | `oc-go-cc_windows-arm64.exe` | +| macOS (Apple Silicon) | `routatic-proxy_darwin-arm64` | +| macOS (Intel) | `routatic-proxy_darwin-amd64` | +| Linux (x86_64) | `routatic-proxy_linux-amd64` | +| Linux (ARM64) | `routatic-proxy_linux-arm64` | +| Windows (x86_64) | `routatic-proxy_windows-amd64.exe` | +| Windows (ARM64) | `routatic-proxy_windows-arm64.exe` | ```bash # macOS Apple Silicon -curl -L -o oc-go-cc https://github.com/samueltuyizere/oc-go-cc/releases/latest/download/oc-go-cc_darwin-arm64 -chmod +x oc-go-cc -sudo mv oc-go-cc /usr/local/bin/ +curl -L -o routatic-proxy https://github.com/routatic/proxy/releases/latest/download/routatic-proxy_darwin-arm64 +chmod +x routatic-proxy +sudo mv routatic-proxy /usr/local/bin/ # Windows (PowerShell) -Invoke-WebRequest -Uri "https://github.com/samueltuyizere/oc-go-cc/releases/latest/download/oc-go-cc_windows-amd64.exe" -OutFile "oc-go-cc.exe" -Move-Item -Path "oc-go-cc.exe" -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\oc-go-cc.exe" +Invoke-WebRequest -Uri "https://github.com/routatic/proxy/releases/latest/download/routatic-proxy_windows-amd64.exe" -OutFile "routatic-proxy.exe" +Move-Item -Path "routatic-proxy.exe" -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\routatic-proxy.exe" ``` +Homebrew and Scoop installs also provide `oc-go-cc` as an alias for `routatic-proxy`. + ## Docker ### Quick start with Makefile ```bash cp .env.example .env -# Edit .env and put your OpenCode Go API key +# Edit .env and put your API key make docker-up ``` @@ -69,8 +72,8 @@ make docker-stop ### Build and run manually ```bash -docker build -t oc-go-cc . -docker run -d --restart unless-stopped --name oc-go-cc --env-file .env -p 3456:3456 oc-go-cc +docker build -t routatic-proxy . +docker run -d --restart unless-stopped --name routatic-proxy --env-file .env -p 3456:3456 routatic-proxy ``` ### Use a custom config @@ -78,9 +81,9 @@ docker run -d --restart unless-stopped --name oc-go-cc --env-file .env -p 3456:3 The Docker image uses `configs/config.json` by default (or `configs/config.example.json` as fallback). Override with a volume: ```bash -docker run -d --restart unless-stopped --name oc-go-cc --env-file .env -p 3456:3456 \ - -v /path/to/your/config.json:/etc/oc-go-cc/config.json:ro \ - oc-go-cc +docker run -d --restart unless-stopped --name routatic-proxy --env-file .env -p 3456:3456 \ + -v /path/to/your/config.json:/etc/routatic-proxy/config.json:ro \ + routatic-proxy ``` ## Requirements diff --git a/MODELS.md b/MODELS.md index 16fcd76..09711b1 100644 --- a/MODELS.md +++ b/MODELS.md @@ -37,7 +37,7 @@ Comprehensive guide to OpenCode Go and Zen models with capabilities, costs, and ## Important: API Endpoints -⚠️ **Critical:** Not all models use the same API endpoint! oc-go-cc handles this automatically, but you should know: +⚠️ **Critical:** Not all models use the same API endpoint! routatic-proxy handles this automatically, but you should know: ### OpenCode Go Endpoints @@ -55,7 +55,7 @@ Comprehensive guide to OpenCode Go and Zen models with capabilities, costs, and | **GPT models** (gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro, gpt-5.4-mini, gpt-5.4-nano, gpt-5.3-codex, gpt-5.3-codex-spark, gpt-5.2, gpt-5.2-codex, gpt-5.1, gpt-5.1-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5, gpt-5-codex, gpt-5-nano) | `https://opencode.ai/zen/v1/responses` | **OpenAI Responses** | | **Gemini models** (gemini-3.5-flash, gemini-3.1-pro, gemini-3-flash) | `https://opencode.ai/zen/v1/models/{id}` | **Google Gemini** | -**Why this matters:** On the Go provider, MiniMax and Qwen models use Anthropic format natively. On Zen, only Claude and Qwen use the Anthropic endpoint — MiniMax uses chat completions. oc-go-cc handles all routing automatically. +**Why this matters:** On the Go provider, MiniMax and Qwen models use Anthropic format natively. On Zen, only Claude and Qwen use the Anthropic endpoint — MiniMax uses chat completions. routatic-proxy handles all routing automatically. ## Using OpenCode Zen @@ -83,7 +83,7 @@ All OpenCode Go models are also available on Zen. Zen additionally offers: - **Gemini Models (Gemini endpoint):** gemini-3.5-flash, gemini-3.1-pro, gemini-3-flash - **Free Tier (chat completions):** deepseek-v4-pro, deepseek-v4-flash-free, grok-build-0.1, big-pickle, mimo-v2.5-free, north-mini-code-free, nemotron-3-ultra-free -DeepSeek V4 Pro and Flash are OpenAI-compatible on both Go and Zen providers. On Zen, DeepSeek V4 Pro is available as a free-tier model. oc-go-cc transforms Claude Code's Anthropic request into OpenAI Chat Completions format, including tools, tool results, thinking history, `reasoning_effort`, and `thinking`. +DeepSeek V4 Pro and Flash are OpenAI-compatible on both Go and Zen providers. On Zen, DeepSeek V4 Pro is available as a free-tier model. routatic-proxy transforms Claude Code's Anthropic request into OpenAI Chat Completions format, including tools, tool results, thinking history, `reasoning_effort`, and `thinking`. For Claude Code and OpenCode-style agent workflows, DeepSeek V4 supports max thinking mode with: @@ -217,7 +217,7 @@ Default → Use Kimi K2.6 (1,850 req/$12, ★★★★★) or Qwen3.6 Plus (3,30 - Long conversations - Multi-file context - **When to Use:** When you need 1M context but want to minimize cost -- **Note:** Uses Anthropic endpoint on Go but chat completions on Zen - oc-go-cc handles this automatically +- **Note:** Uses Anthropic endpoint on Go but chat completions on Zen - routatic-proxy handles this automatically #### MiniMax M3 — Latest MiniMax, 1M Context @@ -484,5 +484,5 @@ Critical review → GLM-5.1 (rarely) ## See Also - [OpenCode Go Documentation](https://opencode.ai/docs/go/) -- [oc-go-cc Configuration](../configs/config.example.json) +- [routatic-proxy Configuration](../configs/config.example.json) - [README.md](../README.md) for setup instructions diff --git a/Makefile b/Makefile index 613b5c0..d2b712e 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,15 @@ # Build variables VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") LDFLAGS = -X main.version=$(VERSION) -BINARY = oc-go-cc -CMD = ./cmd/oc-go-cc +BINARY = routatic-proxy +LEGACY_BINARY = oc-go-cc +CMD = ./cmd/routatic-proxy # ── Development ──────────────────────────────────────────────────── build: go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) $(CMD) + @ln -sf $(BINARY) bin/$(LEGACY_BINARY) run: go run -ldflags "$(LDFLAGS)" $(CMD) @@ -34,12 +36,16 @@ install: build cp bin/$(BINARY) $(GOPATH)/bin/$(BINARY) 2>/dev/null || \ cp bin/$(BINARY) $(HOME)/go/bin/$(BINARY) 2>/dev/null || \ go install -ldflags "$(LDFLAGS)" $(CMD) + @INSTALL_DIR="$$(go env GOPATH 2>/dev/null)/bin"; \ + if [ -x "$$INSTALL_DIR/$(BINARY)" ]; then \ + ln -sf "$(BINARY)" "$$INSTALL_DIR/$(LEGACY_BINARY)"; \ + fi # ── Docker ───────────────────────────────────────────────────────── docker-up: @echo "Building Docker image..." - docker build -t oc-go-cc . + docker build -t routatic-proxy . @echo "" @echo "Starting container..." @if [ ! -f .env ]; then \ @@ -47,22 +53,22 @@ docker-up: echo "Create it with: cp .env.example .env"; \ exit 1; \ fi - @docker stop oc-go-cc 2>/dev/null || true - @docker rm oc-go-cc 2>/dev/null || true + @docker stop routatic-proxy 2>/dev/null || true + @docker rm routatic-proxy 2>/dev/null || true docker run -d \ - --name oc-go-cc \ + --name routatic-proxy \ --restart unless-stopped \ --env-file .env \ -p 3456:3456 \ - oc-go-cc + routatic-proxy @echo "" @echo "Container started! Proxy listening on http://localhost:3456" @echo "Stop with: make docker-stop" docker-stop: @echo "Stopping container..." - docker stop oc-go-cc 2>/dev/null || true - docker rm oc-go-cc 2>/dev/null || true + docker stop routatic-proxy 2>/dev/null || true + docker rm routatic-proxy 2>/dev/null || true @echo "Container stopped and removed." # ── Release / Cross-Compilation ──────────────────────────────────── diff --git a/README.md b/README.md index ac97298..c63200e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# oc-go-cc +# routatic-proxy (prev OC-GO-CC) A Go CLI proxy that lets you use your [OpenCode Go](https://opencode.ai/docs/go/) or [OpenCode Zen](https://opencode.ai/docs/zen/) subscription with [Claude Code](https://docs.anthropic.com/en/docs/claude-code). -`oc-go-cc` sits between Claude Code and OpenCode, intercepting Anthropic API requests, transforming them to the appropriate format (OpenAI, Responses, or Gemini), and forwarding them to OpenCode's endpoints. Claude Code thinks it's talking to Anthropic — but your requests go to affordable open models instead. +`routatic-proxy` sits between Claude Code and OpenCode, intercepting Anthropic API requests, transforming them to the appropriate format (OpenAI, Responses, or Gemini), and forwarding them to OpenCode's endpoints. Claude Code thinks it's talking to Anthropic — but your requests go to affordable open models instead. + +`oc-go-cc` remains available as a compatibility alias, and existing `OC_GO_CC_*` environment variables and `~/.config/oc-go-cc/config.json` files are still recognized. ## Why? @@ -29,10 +31,10 @@ OpenCode Go gives you access to powerful open coding models for **$5/month** (th ```bash # macOS / Linux -brew tap samueltuyizere/tap && brew install oc-go-cc +brew tap routatic/tap && brew install routatic-proxy # Windows -scoop bucket add oc-go-cc https://github.com/samueltuyizere/scoop-bucket && scoop install oc-go-cc +scoop bucket add routatic https://github.com/routatic/scoop-bucket && scoop install routatic-proxy # Docker (with Makefile) cp .env.example .env # then put your API key in .env @@ -40,12 +42,12 @@ make docker-up # Docker (manual) cp .env.example .env -docker build -t oc-go-cc . -docker run -d --restart unless-stopped --name oc-go-cc --env-file .env -p 3456:3456 oc-go-cc +docker build -t routatic-proxy . +docker run -d --restart unless-stopped --name routatic-proxy --env-file .env -p 3456:3456 routatic-proxy # Docker from GitHub Container Registry -docker pull ghcr.io/samueltuyizere/oc-go-cc:latest -docker run -d --restart unless-stopped --name oc-go-cc --env-file .env -p 3456:3456 ghcr.io/samueltuyizere/oc-go-cc:latest +docker pull ghcr.io/routatic/proxy:latest +docker run -d --restart unless-stopped --name routatic-proxy --env-file .env -p 3456:3456 ghcr.io/routatic/proxy:latest ``` Or see [INSTALLATION.md](INSTALLATION.md) for more options. @@ -53,19 +55,19 @@ Or see [INSTALLATION.md](INSTALLATION.md) for more options. ### 2. Initialize Configuration ```bash -oc-go-cc init +routatic-proxy init ``` -Creates a default config at `~/.config/oc-go-cc/config.json`. Edit it to add your API key, or set the environment variable: +Creates a default config at `~/.config/routatic-proxy/config.json`. Edit it to add your API key, or set the environment variable: ```bash -export OC_GO_CC_API_KEY=sk-opencode-your-key-here +export ROUTATIC_PROXY_API_KEY=sk-opencode-your-key-here ``` ### 3. Start the Proxy ```bash -oc-go-cc serve +routatic-proxy serve ``` Stop the Docker container (if using Docker): @@ -90,29 +92,29 @@ claude ## CLI Commands ``` -oc-go-cc serve Start the proxy server -oc-go-cc serve -b Start in background (detached from terminal) -oc-go-cc serve --port 8080 Start on a custom port -oc-go-cc stop Stop the running proxy server -oc-go-cc status Check if the proxy is running -oc-go-cc init Create default configuration file -oc-go-cc validate Validate configuration file -oc-go-cc models List all available models (Go + Zen) -oc-go-cc autostart enable Enable auto-start on login -oc-go-cc autostart disable Disable auto-start on login -oc-go-cc autostart status Check autostart status -oc-go-cc --version Show version +routatic-proxy serve Start the proxy server +routatic-proxy serve -b Start in background (detached from terminal) +routatic-proxy serve --port 8080 Start on a custom port +routatic-proxy stop Stop the running proxy server +routatic-proxy status Check if the proxy is running +routatic-proxy init Create default configuration file +routatic-proxy validate Validate configuration file +routatic-proxy models List all available models (Go + Zen) +routatic-proxy autostart enable Enable auto-start on login +routatic-proxy autostart disable Disable auto-start on login +routatic-proxy autostart status Check autostart status +routatic-proxy --version Show version ``` ## Documentation -| Document | Description | -| -------- | ----------- | -| [INSTALLATION.md](INSTALLATION.md) | Homebrew, Scoop, build from source, release binaries | -| [CONFIGURATION.md](CONFIGURATION.md) | Config file reference, env vars, model routing, fallback chains | -| [MODELS.md](MODELS.md) | Model capabilities, costs, and routing recommendations | -| [CONTRIBUTING.md](CONTRIBUTING.md) | Development setup, architecture, how it works | -| [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | Common issues and debug mode | +| Document | Description | +| ---------------------------------------- | --------------------------------------------------------------- | +| [INSTALLATION.md](INSTALLATION.md) | Homebrew, Scoop, build from source, release binaries | +| [CONFIGURATION.md](CONFIGURATION.md) | Config file reference, env vars, model routing, fallback chains | +| [MODELS.md](MODELS.md) | Model capabilities, costs, and routing recommendations | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Development setup, architecture, how it works | +| [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | Common issues and debug mode | ## License diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 8e1959c..7f34a45 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -2,7 +2,7 @@ ## Windows Scoop Background Mode -On Windows, `oc-go-cc serve -b` uses the native Windows process APIs and keeps +On Windows, `routatic-proxy serve -b` uses the native Windows process APIs and keeps the Scoop shim path intact. This means background mode does not require `nohup` or a Unix-like shell, and Scoop-provided environment variables continue to work. @@ -17,23 +17,23 @@ This means the proxy couldn't parse the request from Claude Code. Enable debug l Or set the environment variable: ```bash -export OC_GO_CC_LOG_LEVEL=debug +export ROUTATIC_PROXY_LOG_LEVEL=debug ``` ## "all models failed" Error All models in the fallback chain returned errors. Check: -1. Your API key is valid: `oc-go-cc validate` +1. Your API key is valid: `routatic-proxy validate` 2. You haven't exceeded your [usage limits](https://opencode.ai/auth) -3. The OpenCode Go service is reachable: `curl -H "Authorization: Bearer $OC_GO_CC_API_KEY" https://opencode.ai/zen/go/v1/models` +3. The OpenCode Go service is reachable: `curl -H "Authorization: Bearer $ROUTATIC_PROXY_API_KEY" https://opencode.ai/zen/go/v1/models` ## Connection Refused Make sure the proxy is running: ```bash -oc-go-cc status +routatic-proxy status ``` And Claude Code is pointing to the right address: @@ -55,12 +55,12 @@ The proxy transforms OpenAI SSE to Anthropic SSE in real-time. If streaming appe For maximum logging, run with debug level: ```bash -OC_GO_CC_LOG_LEVEL=debug oc-go-cc serve +ROUTATIC_PROXY_LOG_LEVEL=debug routatic-proxy serve ``` This logs: - Raw Anthropic request body from Claude Code -- Transformed OpenAI request sent to OpenCode Go -- Raw OpenAI response received +- Transformed request sent to upstream (OpenCode Go/Zen) +- Upstream response received - SSE stream events during streaming diff --git a/cmd/oc-go-cc/main.go b/cmd/routatic-proxy/main.go similarity index 95% rename from cmd/oc-go-cc/main.go rename to cmd/routatic-proxy/main.go index bde8070..e40dc20 100644 --- a/cmd/oc-go-cc/main.go +++ b/cmd/routatic-proxy/main.go @@ -1,4 +1,4 @@ -// Package main is the CLI entry point for the oc-go-cc proxy server. +// Package main is the CLI entry point for the Routatic proxy server. package main import ( @@ -10,15 +10,15 @@ import ( "path/filepath" "strings" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/internal/daemon" + "github.com/routatic/proxy/internal/server" "github.com/spf13/cobra" - "oc-go-cc/internal/config" - "oc-go-cc/internal/daemon" - "oc-go-cc/internal/server" ) const ( - appName = "oc-go-cc" - pidFileName = "oc-go-cc.pid" + appName = "routatic-proxy" + pidFileName = "routatic-proxy.pid" ) // Version is set at build time via -ldflags "-X main.version=...". @@ -26,13 +26,15 @@ var version = "dev" func main() { rootCmd := &cobra.Command{ - Use: appName, - Short: "Proxy Claude Code requests to OpenCode Go API", - Long: `oc-go-cc is a CLI proxy tool that allows you to use your OpenCode Go + Use: appName, + Aliases: []string{"oc-go-cc"}, + Short: "Proxy Claude Code requests to OpenCode Go API", + Long: `routatic-proxy is a CLI proxy tool that allows you to use your OpenCode Go subscription with Claude Code. It intercepts Claude Code's Anthropic API requests, transforms them to OpenAI format, and forwards them to OpenCode Go. -Configuration is stored at ~/.config/oc-go-cc/config.json`, +Configuration is stored at ~/.config/routatic-proxy/config.json. +Legacy ~/.config/oc-go-cc/config.json and OC_GO_CC_* environment variables are still supported.`, Version: version, } @@ -73,7 +75,7 @@ func serveCmd() *cobra.Command { // Override config path if provided. if configPath != "" { - _ = os.Setenv("OC_GO_CC_CONFIG", configPath) + _ = os.Setenv("ROUTATIC_PROXY_CONFIG", configPath) } cfg, err := config.Load() @@ -257,7 +259,7 @@ func validateCmd() *cobra.Command { Short: "Validate configuration file", RunE: func(cmd *cobra.Command, args []string) error { if configPath != "" { - _ = os.Setenv("OC_GO_CC_CONFIG", configPath) + _ = os.Setenv("ROUTATIC_PROXY_CONFIG", configPath) } cfg, err := config.Load() @@ -294,7 +296,7 @@ func checkCmd() *cobra.Command { SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if configPath != "" { - _ = os.Setenv("OC_GO_CC_CONFIG", configPath) + _ = os.Setenv("ROUTATIC_PROXY_CONFIG", configPath) } cfg, err := config.Load() @@ -463,7 +465,7 @@ func modelsCmd() *cobra.Command { // getConfigDir returns the default configuration directory path. func getConfigDir() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".config", "oc-go-cc") + return filepath.Join(home, ".config", "routatic-proxy") } // autostartCmd returns the command to manage autostart on login. @@ -533,7 +535,7 @@ func maskString(s string, visible int) string { // Optimized for cost-efficiency: uses cheaper models by default, expensive ones only when needed. func getDefaultConfig() string { return `{ - "api_key": "${OC_GO_CC_API_KEY}", + "api_key": "${ROUTATIC_PROXY_API_KEY}", "host": "127.0.0.1", "port": 3456, "hot_reload": false, diff --git a/configs/config.example.json b/configs/config.example.json index ea3e02f..7d0b486 100644 --- a/configs/config.example.json +++ b/configs/config.example.json @@ -1,5 +1,5 @@ { - "api_key": "${OC_GO_CC_API_KEY}", + "api_key": "${ROUTATIC_PROXY_API_KEY}", "host": "127.0.0.1", "port": 3456, "hot_reload": false, @@ -200,4 +200,4 @@ "level": "info", "requests": true } -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 0d53593..cf4d175 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module oc-go-cc +module github.com/routatic/proxy go 1.25 diff --git a/internal/client/opencode.go b/internal/client/opencode.go index 45d3571..6edfdae 100644 --- a/internal/client/opencode.go +++ b/internal/client/opencode.go @@ -12,8 +12,8 @@ import ( "sync/atomic" "time" - "oc-go-cc/internal/config" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/pkg/types" ) const ( diff --git a/internal/client/opencode_test.go b/internal/client/opencode_test.go index 32d9b1a..a52a921 100644 --- a/internal/client/opencode_test.go +++ b/internal/client/opencode_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) func TestIsAnthropicModelOnlyRoutesNativeAnthropicModels(t *testing.T) { diff --git a/internal/config/env_test.go b/internal/config/env_test.go new file mode 100644 index 0000000..a8998dc --- /dev/null +++ b/internal/config/env_test.go @@ -0,0 +1,35 @@ +package config + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + names := []string{ + "ROUTATIC_PROXY_CONFIG", + "ROUTATIC_PROXY_API_KEY", + "ROUTATIC_PROXY_HOST", + "ROUTATIC_PROXY_PORT", + "ROUTATIC_PROXY_OPENCODE_URL", + "ROUTATIC_PROXY_OPENCODE_ZEN_URL", + "ROUTATIC_PROXY_LOG_LEVEL", + } + original := make(map[string]string, len(names)) + present := make(map[string]bool, len(names)) + for _, name := range names { + original[name], present[name] = os.LookupEnv(name) + _ = os.Unsetenv(name) + } + + code := m.Run() + + for _, name := range names { + if present[name] { + _ = os.Setenv(name, original[name]) + } else { + _ = os.Unsetenv(name) + } + } + os.Exit(code) +} diff --git a/internal/config/loader.go b/internal/config/loader.go index 15eb144..0a7b5c0 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -11,7 +11,8 @@ import ( ) const ( - defaultConfigPath = "~/.config/oc-go-cc/config.json" + defaultConfigPath = "~/.config/routatic-proxy/config.json" + legacyConfigPath = "~/.config/oc-go-cc/config.json" defaultHost = "127.0.0.1" defaultPort = 3456 defaultBaseURL = "https://opencode.ai/zen/go/v1/chat/completions" @@ -28,10 +29,22 @@ const ( // envVarPattern matches ${ENV_VAR} placeholders in config values. var envVarPattern = regexp.MustCompile(`\$\{([A-Za-z0-9_]+)\}`) +var legacyEnvNames = map[string]string{ + "ROUTATIC_PROXY_CONFIG": "OC_GO_CC_CONFIG", + "ROUTATIC_PROXY_API_KEY": "OC_GO_CC_API_KEY", + "ROUTATIC_PROXY_HOST": "OC_GO_CC_HOST", + "ROUTATIC_PROXY_PORT": "OC_GO_CC_PORT", + "ROUTATIC_PROXY_OPENCODE_URL": "OC_GO_CC_OPENCODE_URL", + "ROUTATIC_PROXY_OPENCODE_ZEN_URL": "OC_GO_CC_OPENCODE_ZEN_URL", + "ROUTATIC_PROXY_LOG_LEVEL": "OC_GO_CC_LOG_LEVEL", +} + // Load reads configuration from a JSON file and applies environment variable overrides. // Config path resolution: -// 1. OC_GO_CC_CONFIG env var (explicit override) -// 2. ~/.config/oc-go-cc/config.json (default) +// 1. ROUTATIC_PROXY_CONFIG env var (explicit override) +// 2. OC_GO_CC_CONFIG env var (legacy explicit override) +// 3. ~/.config/routatic-proxy/config.json (default) +// 4. ~/.config/oc-go-cc/config.json (legacy fallback when the new path is absent) func Load() (*Config, error) { return LoadFromPath(ResolveConfigPath()) } @@ -55,10 +68,18 @@ func LoadFromPath(path string) (*Config, error) { // ResolveConfigPath determines which config file to load. func ResolveConfigPath() string { - if path := os.Getenv("OC_GO_CC_CONFIG"); path != "" { + if path := envValue("ROUTATIC_PROXY_CONFIG"); path != "" { + return path + } + path := expandHome(defaultConfigPath) + if _, err := os.Stat(path); err == nil { return path } - return expandHome(defaultConfigPath) + legacyPath := expandHome(legacyConfigPath) + if _, err := os.Stat(legacyPath); err == nil { + return legacyPath + } + return path } // expandHome replaces a leading ~ with the user's home directory. @@ -96,7 +117,7 @@ func interpolateEnvVars(s string) string { return envVarPattern.ReplaceAllStringFunc(s, func(match string) string { // Extract variable name from ${VAR} varName := match[2 : len(match)-1] - if val := os.Getenv(varName); val != "" { + if val := envValue(varName); val != "" { return val } // Leave unchanged if env var is not set @@ -106,29 +127,44 @@ func interpolateEnvVars(s string) string { // applyEnvOverrides applies environment variable overrides to the config. func applyEnvOverrides(cfg *Config) { - if v := os.Getenv("OC_GO_CC_API_KEY"); v != "" { + if v := envValue("ROUTATIC_PROXY_API_KEY"); v != "" { cfg.APIKey = v cfg.APIKeys = nil // env var overrides both api_key and api_keys } - if v := os.Getenv("OC_GO_CC_HOST"); v != "" { + if v := envValue("ROUTATIC_PROXY_HOST"); v != "" { cfg.Host = v } - if v := os.Getenv("OC_GO_CC_PORT"); v != "" { + if v := envValue("ROUTATIC_PROXY_PORT"); v != "" { if port, err := strconv.Atoi(v); err == nil { cfg.Port = port } } - if v := os.Getenv("OC_GO_CC_OPENCODE_URL"); v != "" { + if v := envValue("ROUTATIC_PROXY_OPENCODE_URL"); v != "" { cfg.OpenCodeGo.BaseURL = v } - if v := os.Getenv("OC_GO_CC_OPENCODE_ZEN_URL"); v != "" { + if v := envValue("ROUTATIC_PROXY_OPENCODE_ZEN_URL"); v != "" { cfg.OpenCodeZen.BaseURL = v } - if v := os.Getenv("OC_GO_CC_LOG_LEVEL"); v != "" { + if v := envValue("ROUTATIC_PROXY_LOG_LEVEL"); v != "" { cfg.Logging.Level = v } } +func envValue(name string) string { + if val := os.Getenv(name); val != "" { + return val + } + if legacyName, ok := legacyEnvNames[name]; ok { + return os.Getenv(legacyName) + } + for canonicalName, legacyName := range legacyEnvNames { + if name == legacyName { + return os.Getenv(canonicalName) + } + } + return "" +} + // applyDefaults fills in missing configuration values with sensible defaults. func applyDefaults(cfg *Config) { if cfg.Host == "" { @@ -181,7 +217,7 @@ func applyDefaults(cfg *Config) { // validate checks that all required configuration fields are present. func validate(cfg *Config) error { if cfg.APIKey == "" && len(cfg.APIKeys) == 0 { - return fmt.Errorf("api_key or api_keys is required (set via config file or OC_GO_CC_API_KEY env var)") + return fmt.Errorf("api_key or api_keys is required (set via config file or ROUTATIC_PROXY_API_KEY env var; OC_GO_CC_API_KEY is still supported)") } if err := validateAPIKeys(cfg.APIKeys); err != nil { diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index c75eb2d..04d926c 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -214,6 +214,69 @@ func TestEnvOverrides(t *testing.T) { } } +func TestEnvOverrides_RoutaticProxyTakesPrecedenceOverLegacy(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json") + + if err := os.WriteFile(cfgPath, []byte(`{"api_key": "file-key"}`), 0644); err != nil { + t.Fatalf("failed to write test config: %v", err) + } + + _ = os.Setenv("ROUTATIC_PROXY_CONFIG", cfgPath) + _ = os.Setenv("OC_GO_CC_CONFIG", filepath.Join(dir, "legacy.json")) + _ = os.Setenv("ROUTATIC_PROXY_API_KEY", "new-key") + _ = os.Setenv("OC_GO_CC_API_KEY", "legacy-key") + _ = os.Setenv("ROUTATIC_PROXY_HOST", "new-host") + _ = os.Setenv("OC_GO_CC_HOST", "legacy-host") + defer func() { + _ = os.Unsetenv("ROUTATIC_PROXY_CONFIG") + _ = os.Unsetenv("OC_GO_CC_CONFIG") + _ = os.Unsetenv("ROUTATIC_PROXY_API_KEY") + _ = os.Unsetenv("OC_GO_CC_API_KEY") + _ = os.Unsetenv("ROUTATIC_PROXY_HOST") + _ = os.Unsetenv("OC_GO_CC_HOST") + }() + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.APIKey != "new-key" { + t.Errorf("APIKey = %q, want %q", cfg.APIKey, "new-key") + } + if cfg.Host != "new-host" { + t.Errorf("Host = %q, want %q", cfg.Host, "new-host") + } +} + +func TestInterpolateEnvVars_NewPlaceholderAcceptsLegacyEnv(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json") + + if err := os.WriteFile(cfgPath, []byte(`{"api_key": "${ROUTATIC_PROXY_API_KEY}"}`), 0644); err != nil { + t.Fatalf("failed to write test config: %v", err) + } + + _ = os.Setenv("ROUTATIC_PROXY_CONFIG", cfgPath) + _ = os.Unsetenv("ROUTATIC_PROXY_API_KEY") + _ = os.Setenv("OC_GO_CC_API_KEY", "legacy-key") + defer func() { + _ = os.Unsetenv("ROUTATIC_PROXY_CONFIG") + _ = os.Unsetenv("ROUTATIC_PROXY_API_KEY") + _ = os.Unsetenv("OC_GO_CC_API_KEY") + }() + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.APIKey != "legacy-key" { + t.Errorf("APIKey = %q, want %q", cfg.APIKey, "legacy-key") + } +} + func TestEnvOverrides_OC_GO_CC_API_KEY_OverridesAPIKeys(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config.json") diff --git a/internal/daemon/autostart_windows.go b/internal/daemon/autostart_windows.go index e066ea7..61d8c0b 100644 --- a/internal/daemon/autostart_windows.go +++ b/internal/daemon/autostart_windows.go @@ -25,7 +25,7 @@ func buildAutostartArgs(configPath string, port int) string { return args } -// EnableAutostart adds a registry Run key so oc-go-cc starts on login. +// EnableAutostart adds a registry Run key so routatic-proxy starts on login. func EnableAutostart(configPath string, port int) error { paths, err := DefaultPaths() if err != nil { diff --git a/internal/daemon/background.go b/internal/daemon/background.go index af6a3c4..d429438 100644 --- a/internal/daemon/background.go +++ b/internal/daemon/background.go @@ -30,7 +30,7 @@ func ForkIntoBackground(opts BackgroundOpts) error { _ = os.Remove(paths.PIDFile) } - // Build args for the child process: oc-go-cc serve --_daemonize [--config X] [--port N] + // Build args for the child process: routatic-proxy serve --_daemonize [--config X] [--port N] args := []string{"serve", "--_daemonize"} if opts.ConfigPath != "" { configPath, err := filepath.Abs(opts.ConfigPath) diff --git a/internal/daemon/paths.go b/internal/daemon/paths.go index 6626408..6079cb8 100644 --- a/internal/daemon/paths.go +++ b/internal/daemon/paths.go @@ -10,17 +10,18 @@ import ( ) const ( - AppName = "oc-go-cc" - ConfigDir = ".config/oc-go-cc" - LaunchAgent = "com.opencode.oc-go-cc" + AppName = "routatic-proxy" + LegacyAppName = "oc-go-cc" + ConfigDir = ".config/routatic-proxy" + LaunchAgent = "com.routatic.proxy" ) // Paths holds well-known directories and files for the app. type Paths struct { - ConfigDir string // ~/.config/oc-go-cc - PIDFile string // ~/.config/oc-go-cc/oc-go-cc.pid - LogFile string // ~/.config/oc-go-cc/oc-go-cc.log - PlistPath string // ~/Library/LaunchAgents/com.opencode.oc-go-cc.plist + ConfigDir string // ~/.config/routatic-proxy + PIDFile string // ~/.config/routatic-proxy/routatic-proxy.pid + LogFile string // ~/.config/routatic-proxy/routatic-proxy.log + PlistPath string // ~/Library/LaunchAgents/com.routatic.proxy.plist BinaryPath string // absolute path to the running executable } @@ -50,7 +51,7 @@ func DefaultPaths() (*Paths, error) { return paths, nil } -// EnsureConfigDir creates ~/.config/oc-go-cc/ if it does not exist. +// EnsureConfigDir creates ~/.config/routatic-proxy/ if it does not exist. func (p *Paths) EnsureConfigDir() error { return os.MkdirAll(p.ConfigDir, 0755) } @@ -74,7 +75,7 @@ func WritePID(pidPath string, pid int) error { return os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", pid)), 0644) } -// FindBinary returns the absolute path to the oc-go-cc binary. +// FindBinary returns the absolute path to the routatic-proxy binary. func FindBinary() (string, error) { // First try to use the current executable execPath, err := os.Executable() @@ -82,10 +83,13 @@ func FindBinary() (string, error) { return resolveExecutablePath(execPath), nil } - // Fallback: search PATH for oc-go-cc + // Fallback: search PATH for routatic-proxy, then the legacy oc-go-cc alias. execPath, err = exec.LookPath(AppName) if err != nil { - return "", fmt.Errorf("cannot find oc-go-cc binary: %w", err) + execPath, err = exec.LookPath(LegacyAppName) + if err != nil { + return "", fmt.Errorf("cannot find %s binary: %w", AppName, err) + } } return resolveExecutablePath(execPath), nil } diff --git a/internal/handlers/health.go b/internal/handlers/health.go index b25cc65..7205fac 100644 --- a/internal/handlers/health.go +++ b/internal/handlers/health.go @@ -4,10 +4,10 @@ import ( "encoding/json" "net/http" - "oc-go-cc/internal/metrics" - "oc-go-cc/internal/router" - "oc-go-cc/internal/token" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/metrics" + "github.com/routatic/proxy/internal/router" + "github.com/routatic/proxy/internal/token" + "github.com/routatic/proxy/pkg/types" ) // HealthHandler handles health checks and token counting endpoints. @@ -39,7 +39,7 @@ func (h *HealthHandler) HandleHealth(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "status": "ok", - "service": "oc-go-cc", + "service": "routatic-proxy", "metrics": map[string]interface{}{ "requests_received": snapshot.RequestsReceived, "requests_success": snapshot.RequestsSuccess, diff --git a/internal/handlers/health_test.go b/internal/handlers/health_test.go index b30dc51..efad4e4 100644 --- a/internal/handlers/health_test.go +++ b/internal/handlers/health_test.go @@ -7,8 +7,8 @@ import ( "net/http/httptest" "testing" - "oc-go-cc/internal/metrics" - "oc-go-cc/internal/token" + "github.com/routatic/proxy/internal/metrics" + "github.com/routatic/proxy/internal/token" ) func TestHandleCountTokensSupportsAnthropicContentBlocks(t *testing.T) { diff --git a/internal/handlers/messages.go b/internal/handlers/messages.go index 127f5c6..92848cb 100644 --- a/internal/handlers/messages.go +++ b/internal/handlers/messages.go @@ -13,14 +13,14 @@ import ( "sync/atomic" "time" - "oc-go-cc/internal/client" - "oc-go-cc/internal/config" - "oc-go-cc/internal/metrics" - "oc-go-cc/internal/middleware" - "oc-go-cc/internal/router" - "oc-go-cc/internal/token" - "oc-go-cc/internal/transformer" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/client" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/internal/metrics" + "github.com/routatic/proxy/internal/middleware" + "github.com/routatic/proxy/internal/router" + "github.com/routatic/proxy/internal/token" + "github.com/routatic/proxy/internal/transformer" + "github.com/routatic/proxy/pkg/types" ) // MessagesHandler handles /v1/messages requests. diff --git a/internal/handlers/messages_test.go b/internal/handlers/messages_test.go index aa53c62..ecd65a2 100644 --- a/internal/handlers/messages_test.go +++ b/internal/handlers/messages_test.go @@ -5,8 +5,8 @@ import ( "log/slog" "testing" - "oc-go-cc/internal/config" - "oc-go-cc/internal/router" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/internal/router" ) func boolPtr(b bool) *bool { return &b } diff --git a/internal/handlers/token_count.go b/internal/handlers/token_count.go index e70c5a8..da6141e 100644 --- a/internal/handlers/token_count.go +++ b/internal/handlers/token_count.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "oc-go-cc/internal/token" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/token" + "github.com/routatic/proxy/pkg/types" ) func tokenMessagesFromAnthropic(messages []types.Message) []token.MessageContent { diff --git a/internal/router/fallback.go b/internal/router/fallback.go index 862d25a..c0f3435 100644 --- a/internal/router/fallback.go +++ b/internal/router/fallback.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) // CircuitState represents the state of a circuit breaker. diff --git a/internal/router/fallback_test.go b/internal/router/fallback_test.go index b7a79d9..54e2021 100644 --- a/internal/router/fallback_test.go +++ b/internal/router/fallback_test.go @@ -6,7 +6,7 @@ import ( "fmt" "testing" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) func TestIsRetryableError_ClientsErrorsNotRetryable(t *testing.T) { diff --git a/internal/router/model_router.go b/internal/router/model_router.go index 9e16008..de81f9c 100644 --- a/internal/router/model_router.go +++ b/internal/router/model_router.go @@ -5,7 +5,7 @@ package router import ( "fmt" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) // ModelRouter handles model selection based on scenarios. diff --git a/internal/router/model_router_test.go b/internal/router/model_router_test.go index 729d4e8..18bb170 100644 --- a/internal/router/model_router_test.go +++ b/internal/router/model_router_test.go @@ -3,7 +3,7 @@ package router import ( "testing" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) func boolPtr(b bool) *bool { return &b } diff --git a/internal/router/scenarios.go b/internal/router/scenarios.go index ac7336c..987f7f7 100644 --- a/internal/router/scenarios.go +++ b/internal/router/scenarios.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) // Scenario represents the routing scenario for model selection. diff --git a/internal/router/scenarios_test.go b/internal/router/scenarios_test.go index 998ef3a..2edb6b2 100644 --- a/internal/router/scenarios_test.go +++ b/internal/router/scenarios_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "oc-go-cc/internal/config" + "github.com/routatic/proxy/internal/config" ) func TestHasComplexPattern_UserMessage(t *testing.T) { diff --git a/internal/server/server.go b/internal/server/server.go index af7e87d..2aba183 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,12 +11,12 @@ import ( "syscall" "time" - "oc-go-cc/internal/client" - "oc-go-cc/internal/config" - "oc-go-cc/internal/handlers" - "oc-go-cc/internal/metrics" - "oc-go-cc/internal/router" - "oc-go-cc/internal/token" + "github.com/routatic/proxy/internal/client" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/internal/handlers" + "github.com/routatic/proxy/internal/metrics" + "github.com/routatic/proxy/internal/router" + "github.com/routatic/proxy/internal/token" ) // Server represents the proxy server. @@ -104,7 +104,7 @@ func NewServer(atomic *config.AtomicConfig) (*Server, error) { // Start starts the server with graceful shutdown. func (s *Server) Start() error { cfg := s.atomic.Get() - s.logger.Info("starting oc-go-cc proxy", + s.logger.Info("starting routatic-proxy", "host", cfg.Host, "port", cfg.Port, "base_url", cfg.OpenCodeGo.BaseURL, diff --git a/internal/token/counter.go b/internal/token/counter.go index 323e0bb..db90472 100644 --- a/internal/token/counter.go +++ b/internal/token/counter.go @@ -16,7 +16,7 @@ type Counter struct { // defaultCacheDir returns a user-writable cache directory for tiktoken files. // Uses TIKTOKEN_CACHE_DIR or DATA_GYM_CACHE_DIR if already set; otherwise -// defaults to ~/.cache/oc-go-cc/tiktoken to avoid /tmp permission issues. +// defaults to ~/.cache/routatic-proxy/tiktoken to avoid /tmp permission issues. func defaultCacheDir() string { if d := os.Getenv("TIKTOKEN_CACHE_DIR"); d != "" { return d @@ -28,7 +28,7 @@ func defaultCacheDir() string { if err != nil { return filepath.Join(os.TempDir(), "data-gym-cache") } - return filepath.Join(home, ".cache", "oc-go-cc", "tiktoken") + return filepath.Join(home, ".cache", "routatic-proxy", "tiktoken") } // NewCounter creates a new token counter with cl100k_base encoding. diff --git a/internal/token/counter_test.go b/internal/token/counter_test.go index 151c0d0..cce9337 100644 --- a/internal/token/counter_test.go +++ b/internal/token/counter_test.go @@ -26,7 +26,7 @@ func TestDefaultCacheDir(t *testing.T) { }, { name: "falls back to home cache", - want: filepath.Join(mustHome(), ".cache", "oc-go-cc", "tiktoken"), + want: filepath.Join(mustHome(), ".cache", "routatic-proxy", "tiktoken"), }, } diff --git a/internal/transformer/gemini.go b/internal/transformer/gemini.go index 1178ef9..dedf4b0 100644 --- a/internal/transformer/gemini.go +++ b/internal/transformer/gemini.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "oc-go-cc/internal/config" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/pkg/types" ) // TransformToGemini converts an Anthropic MessageRequest to GeminiRequest. diff --git a/internal/transformer/request.go b/internal/transformer/request.go index 11a89f0..7e5b1ab 100644 --- a/internal/transformer/request.go +++ b/internal/transformer/request.go @@ -7,8 +7,8 @@ import ( "fmt" "strings" - "oc-go-cc/internal/config" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/pkg/types" ) // contentText is a convenience wrapper around types.TextContent for brevity diff --git a/internal/transformer/request_test.go b/internal/transformer/request_test.go index f9de7c9..580b32d 100644 --- a/internal/transformer/request_test.go +++ b/internal/transformer/request_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "oc-go-cc/internal/config" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/pkg/types" ) // TestTransformRequestRoundTripReasoning verifies that a DeepSeek response with diff --git a/internal/transformer/response.go b/internal/transformer/response.go index 9a3574f..cb46bc9 100644 --- a/internal/transformer/response.go +++ b/internal/transformer/response.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/pkg/types" ) // ResponseTransformer converts OpenAI responses to Anthropic format. diff --git a/internal/transformer/response_test.go b/internal/transformer/response_test.go index c5d4420..ae629fb 100644 --- a/internal/transformer/response_test.go +++ b/internal/transformer/response_test.go @@ -3,7 +3,7 @@ package transformer import ( "testing" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/pkg/types" ) func TestTransformResponsePreservesReasoningContent(t *testing.T) { diff --git a/internal/transformer/responses.go b/internal/transformer/responses.go index 8480763..bb6f57b 100644 --- a/internal/transformer/responses.go +++ b/internal/transformer/responses.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "oc-go-cc/internal/config" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/internal/config" + "github.com/routatic/proxy/pkg/types" ) // TransformToResponses converts an Anthropic MessageRequest to OpenAI ResponsesRequest. diff --git a/internal/transformer/stream.go b/internal/transformer/stream.go index ced8c6e..a2c084e 100644 --- a/internal/transformer/stream.go +++ b/internal/transformer/stream.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/pkg/types" ) // ErrClientDisconnected is returned when the client disconnects during streaming. diff --git a/internal/transformer/stream_test.go b/internal/transformer/stream_test.go index f592b7c..3b33462 100644 --- a/internal/transformer/stream_test.go +++ b/internal/transformer/stream_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "oc-go-cc/pkg/types" + "github.com/routatic/proxy/pkg/types" ) // mockResponseWriter implements http.ResponseWriter and http.Flusher for testing. diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index 7058f4a..6deab2f 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -14,8 +14,8 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" # --- Config --- -PORT="${OC_GO_CC_PORT:-3457}" -HOST="${OC_GO_CC_HOST:-127.0.0.1}" +PORT="${ROUTATIC_PROXY_PORT:-${OC_GO_CC_PORT:-3457}}" +HOST="${ROUTATIC_PROXY_HOST:-${OC_GO_CC_HOST:-127.0.0.1}}" BASE_URL="http://${HOST}:${PORT}" TIMEOUT_SEC=60 pass=0 @@ -33,9 +33,9 @@ cleanup() { kill "${PROXY_PID}" 2>/dev/null || true wait "${PROXY_PID}" 2>/dev/null || true fi - ./bin/oc-go-cc stop 2>/dev/null || true + ./bin/routatic-proxy stop 2>/dev/null || true sleep 1 - rm -f ~/.config/oc-go-cc/oc-go-cc.pid + rm -f ~/.config/routatic-proxy/routatic-proxy.pid } trap cleanup EXIT @@ -43,7 +43,7 @@ trap cleanup EXIT if [ "${1:-}" = "--skip-build" ]; then echo -e "${YELLOW}Skipping build...${NC}" else - echo "=== Building oc-go-cc ===" + echo "=== Building routatic-proxy ===" make build echo "" fi @@ -56,8 +56,9 @@ if [ -f .env ]; then set +a fi -if [ -z "${OC_GO_CC_API_KEY:-}" ]; then - echo -e "${RED}Error: OC_GO_CC_API_KEY not set. Create a .env file or export it.${NC}" +API_KEY="${ROUTATIC_PROXY_API_KEY:-${OC_GO_CC_API_KEY:-}}" +if [ -z "$API_KEY" ]; then + echo -e "${RED}Error: ROUTATIC_PROXY_API_KEY not set. OC_GO_CC_API_KEY is also accepted for compatibility.${NC}" exit 1 fi @@ -67,7 +68,7 @@ cleanup # Run server in foreground but background it with & so we can capture the PID. # Do NOT use -b (daemonize) because that forks and exits the parent, making $! # capture the wrong PID. -./bin/oc-go-cc serve --port "$PORT" > /tmp/oc-go-cc-e2e.log 2>&1 & +./bin/routatic-proxy serve --port "$PORT" > /tmp/routatic-proxy-e2e.log 2>&1 & PROXY_PID=$! echo "Server PID: ${PROXY_PID}" @@ -82,7 +83,7 @@ for i in $(seq 1 10); do done if [ "${HEALTH_OK}" != "true" ]; then echo -e "${RED}Proxy failed to start${NC}" - cat /tmp/oc-go-cc-e2e.log 2>/dev/null || true + cat /tmp/routatic-proxy-e2e.log 2>/dev/null || true exit 1 fi echo -e "${GREEN}Proxy is running${NC}" @@ -126,10 +127,10 @@ JSON ) REQUEST_BODY="${REQUEST_BODY//MODEL_PLACEHOLDER/$model}" - HTTP_CODE=$(curl -s -o /tmp/oc-go-cc-e2e-response.json -w '%{http_code}' \ + HTTP_CODE=$(curl -s -o /tmp/routatic-proxy-e2e-response.json -w '%{http_code}' \ -X POST "${BASE_URL}/v1/messages" \ -H "Content-Type: application/json" \ - -H "x-api-key: ${OC_GO_CC_API_KEY}" \ + -H "x-api-key: $API_KEY" \ -d "$REQUEST_BODY" \ --max-time "$TIMEOUT_SEC") @@ -137,7 +138,7 @@ JSON # Extract the text response for verification TEXT=$(python3 -c " import json -with open('/tmp/oc-go-cc-e2e-response.json') as f: +with open('/tmp/routatic-proxy-e2e-response.json') as f: d = json.load(f) blocks = d.get('content', []) for b in blocks: @@ -147,7 +148,7 @@ for b in blocks: echo -e "${GREEN}PASS${NC} (200, response: \"${TEXT}\")" pass=$((pass + 1)) else - ERROR_MSG=$(head -c 300 /tmp/oc-go-cc-e2e-response.json 2>/dev/null || echo "") + ERROR_MSG=$(head -c 300 /tmp/routatic-proxy-e2e-response.json 2>/dev/null || echo "") echo -e "${RED}FAIL${NC} (HTTP ${HTTP_CODE})" echo " Response: ${ERROR_MSG}" fail=$((fail + 1)) @@ -192,26 +193,26 @@ JSON ) REQUEST_BODY="${REQUEST_BODY//MODEL_PLACEHOLDER/$model}" - HTTP_CODE=$(curl -s -o /tmp/oc-go-cc-e2e-stream-response.txt -w '%{http_code}' \ + HTTP_CODE=$(curl -s -o /tmp/routatic-proxy-e2e-stream-response.txt -w '%{http_code}' \ -X POST "${BASE_URL}/v1/messages" \ -H "Content-Type: application/json" \ - -H "x-api-key: ${OC_GO_CC_API_KEY}" \ + -H "x-api-key: $API_KEY" \ -d "$REQUEST_BODY" \ --max-time "$TIMEOUT_SEC") if [ "$HTTP_CODE" = 200 ]; then # Verify it's a valid SSE stream: must have message_start and message_stop - if grep -q "event: message_start" /tmp/oc-go-cc-e2e-stream-response.txt && \ - grep -q "event: message_stop" /tmp/oc-go-cc-e2e-stream-response.txt; then + if grep -q "event: message_start" /tmp/routatic-proxy-e2e-stream-response.txt && \ + grep -q "event: message_stop" /tmp/routatic-proxy-e2e-stream-response.txt; then echo -e "${GREEN}PASS${NC} (200, valid SSE stream)" pass=$((pass + 1)) else echo -e "${RED}FAIL${NC} (200 but missing message_start/message_stop — corrupted SSE)" - head -c 400 /tmp/oc-go-cc-e2e-stream-response.txt + head -c 400 /tmp/routatic-proxy-e2e-stream-response.txt fail=$((fail + 1)) fi else - ERROR_MSG=$(head -c 300 /tmp/oc-go-cc-e2e-stream-response.txt 2>/dev/null || echo "") + ERROR_MSG=$(head -c 300 /tmp/routatic-proxy-e2e-stream-response.txt 2>/dev/null || echo "") echo -e "${RED}FAIL${NC} (HTTP ${HTTP_CODE})" echo " Response: ${ERROR_MSG}" fail=$((fail + 1)) @@ -243,26 +244,26 @@ JSON ) REQUEST_BODY="${REQUEST_BODY//MODEL_PLACEHOLDER/$model}" - HTTP_CODE=$(curl -s -o /tmp/oc-go-cc-e2e-stream-long.txt -w '%{http_code}' \ + HTTP_CODE=$(curl -s -o /tmp/routatic-proxy-e2e-stream-long.txt -w '%{http_code}' \ -X POST "${BASE_URL}/v1/messages" \ -H "Content-Type: application/json" \ - -H "x-api-key: ${OC_GO_CC_API_KEY}" \ + -H "x-api-key: $API_KEY" \ -d "$REQUEST_BODY" \ --max-time 120) if [ "$HTTP_CODE" = 200 ]; then - if grep -q "event: message_start" /tmp/oc-go-cc-e2e-stream-long.txt && \ - grep -q "event: message_stop" /tmp/oc-go-cc-e2e-stream-long.txt; then - DELTA_COUNT=$(grep -c "event: content_block_delta" /tmp/oc-go-cc-e2e-stream-long.txt 2>/dev/null || echo 0) + if grep -q "event: message_start" /tmp/routatic-proxy-e2e-stream-long.txt && \ + grep -q "event: message_stop" /tmp/routatic-proxy-e2e-stream-long.txt; then + DELTA_COUNT=$(grep -c "event: content_block_delta" /tmp/routatic-proxy-e2e-stream-long.txt 2>/dev/null || echo 0) echo -e "${GREEN}PASS${NC} (200, ${DELTA_COUNT} content deltas, valid SSE)" pass=$((pass + 1)) else echo -e "${RED}FAIL${NC} (200 but invalid SSE — missing start/stop)" - head -c 400 /tmp/oc-go-cc-e2e-stream-long.txt + head -c 400 /tmp/routatic-proxy-e2e-stream-long.txt fail=$((fail + 1)) fi else - ERROR_MSG=$(head -c 300 /tmp/oc-go-cc-e2e-stream-long.txt 2>/dev/null || echo "") + ERROR_MSG=$(head -c 300 /tmp/routatic-proxy-e2e-stream-long.txt 2>/dev/null || echo "") echo -e "${RED}FAIL${NC} (HTTP ${HTTP_CODE})" echo " Response: ${ERROR_MSG}" fail=$((fail + 1))