Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,31 @@ This plugin exposes OpenCode Go as a single provider (`opencode-go`) backed by a
- **Thinking & Reasoning Support**: Maps reasoning effort across supported client and upstream formats.
- **Dynamic Catalog Discovery**: Fetches remote model catalogs with local fallback and custom route overrides.
- **Multi-Key Auth Scheduling**: Pools multiple API keys with CLIProxyAPI's native scheduler for rotation, retries, and error cooldowns across all protocols.
- **OpenCode Go Quota Page**: Management Center includes a separate `OpenCode Go Quota` page. Page load lists credentials without contacting OpenCode; each card is refreshed manually and independently, and quota values do not affect routing or CPA's native quota page.
- **Native quotas**: Rolling, weekly, and monthly quotas use CLIProxyAPI's generic quota endpoints and the selected credential. The separate plugin page is removed.

## Requirements

- **CLIProxyAPI**: `v7.2.138+`
- **Go Toolchain**: Go 1.24+ (with CGO enabled for C-shared build mode)
- **CLIProxyAPI**: `v8.0.0+`
- **Go Toolchain**: Go 1.26.7+ (with CGO enabled for C-shared build mode)

## Build

Build the dynamic shared library for your platform:
Build the shared library with the `debug` tag for local development.
CI handles release builds.

### Windows (AMD64)
```powershell
go build -buildmode=c-shared -o plugins/windows/amd64/opencode-go-cliproxyapi.dll .
go build -tags debug -buildmode=c-shared -o plugins/windows/amd64/opencode-go-cliproxyapi.dll .
```

### Linux (AMD64)
```bash
go build -buildmode=c-shared -o plugins/linux/amd64/opencode-go-cliproxyapi.so .
go build -tags debug -buildmode=c-shared -o plugins/linux/amd64/opencode-go-cliproxyapi.so .
```

### macOS (ARM64)
```bash
go build -buildmode=c-shared -o plugins/darwin/arm64/opencode-go-cliproxyapi.dylib .
go build -tags debug -buildmode=c-shared -o plugins/darwin/arm64/opencode-go-cliproxyapi.dylib .
```

Place the compiled binary into your CLIProxyAPI plugin directory (e.g. `<cliproxyapi_root>/plugins/<os>/<arch>/`).
Expand All @@ -81,6 +82,7 @@ plugins:
# OpenCode Go API keys (at least one required). Supports ${ENV_VAR} expansion.
api-keys:
- value: "sk-opencode-key-1"
name: "Personal" # optional account label
- value: "sk-opencode-key-2"
- value: "${OPENCODE_GO_API_KEY}"

Expand Down Expand Up @@ -111,7 +113,7 @@ plugins:

| Option | Type | Default | Description |
|---|---|---|---|
| `api-keys` | `[]object` | *(Required)* | List of API keys (`- value: "..."`). Supports `${ENV_VAR}` expansion. Duplicates and empty values are rejected. |
| `api-keys` | `[]object` | *(Required)* | List of API keys (`- value: "..."`, optional `name: "Personal"`). Supports `${ENV_VAR}` expansion. Duplicates and empty values are rejected. |
| `base-url` | `string` | `https://opencode.ai/zen/go/v1` | Upstream base URL. Must be valid HTTPS (or HTTP if `allow-http: true`) without query parameters, fragments, or userinfo. |
| `catalog-url` | `string` | `{base-url}/models` | Full URL for catalog discovery. Defaults to `{base-url}/models`. |
| `model-prefix.enabled` | `bool` | `true` | When `true`, client-facing model names use `<prefix>/<model>`. When `false`, uses bare model IDs. |
Expand All @@ -130,11 +132,32 @@ plugins:

```powershell
# Run all tests
go test ./...
go test -tags debug ./...

# Run tests with coverage
go test ./... -cover
go test -tags debug ./... -cover

# Run linter / vetting
go vet ./...
```

## Account names and quotas

Set an optional `name` on each `api-keys` entry, alongside `value`.
For example, use `name: Personal` or `name: Work`. Without a name, one key
uses **OpenCode Go**; multiple keys use **OpenCode Go 1**, **OpenCode Go 2**, and subsequent numbers.
Default numbers follow configuration order. Explicit names remain stable when keys are reordered.
The usage response contains no email or account identity. The plugin does not infer an email from a key.

New credential files have readable names. Existing auth IDs remain unchanged, and
legacy generated labels are replaced when parsed. Existing custom labels are preserved
unless configuration specifies a name. Existing filenames are retained automatically;
stop CLIProxyAPI before renaming an old file to a readable `.json` filename, and keep
its JSON `id` and all other fields unchanged. Back up the file first.

Discover support with `GET /v0/management/quota/providers`, then call
`POST /v0/management/quota/fetch` with `{"auth_index":"<selected index>"}`.
These endpoints require the CLIProxyAPI management key. The normalized response contains
`subscription.plan`, `groups[].buckets[].window`, `remainingFraction`, and `resetTime`.
Missing windows are omitted. Invalid readings return an error, not an invented zero.
Quota reset is unsupported. The old plugin quota route and resource page are removed.
32 changes: 31 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
## CLIProxyAPI v8

### Features

- Support CLIProxyAPI v8 and its generic plugin quota API.
- Return rolling, weekly, and monthly quotas for the credential selected by CLIProxyAPI.
- Advertise the quota provider as **OpenCode Go**.
- Remove the separate quota page and custom management routes.
- Accept an optional `name` for each API key.
- Use readable account labels and filenames instead of key digests.
- Preserve existing auth IDs, custom labels, filenames, and host metadata.
- Prevent duplicate auth records at cold start when readable filenames exist.
- Reject invalid quota values and redact provider request errors.

### Upgrade

1. Upgrade CLIProxyAPI to v8.0.0 or later.
2. Replace the plugin binary with the new build.
3. Restart CLIProxyAPI.

The generic endpoints are `GET /v0/management/quota/providers` and `POST /v0/management/quota/fetch`.
Both require the management key. Quota reset is unsupported.

The management dashboard must support the generic quota API to show plugin refresh controls.
Older dashboard builds do not show these controls.

Existing credential files keep their names and stable IDs.
Set `name:` on an API key to choose its display label.
New files use readable names.

## What's Changed

Full OpenAI Codex CLI support across all models: multi-agent namespaces, custom programmatic tools (`exec`), tool call results, and reasoning efforts are fully functional.
Expand All @@ -23,4 +53,4 @@ Full OpenAI Codex CLI support across all models: multi-agent namespaces, custom
- Restart CLIProxyAPI after replacing the plugin.
- Hard-refresh Management Center if the plugin page looks stale.

**Full Changelog**: https://github.com/massiveits/opencode-go-cliproxyapi/compare/v0.1.9...v0.1.10
**Full Changelog**: https://github.com/massiveits/opencode-go-cliproxyapi/compare/v0.1.9...v0.1.10
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ module opencode-go-cliproxyapi
go 1.26.7

require (
github.com/router-for-me/CLIProxyAPI/v7 v7.2.138
github.com/router-for-me/CLIProxyAPI/v8 v8.0.0
gopkg.in/yaml.v3 v3.0.1
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/router-for-me/CLIProxyAPI/v7 v7.2.138 h1:RbMi2wh0cOzvTq3O1vooSHz9FFvCvP5yHd3Aganjk5g=
github.com/router-for-me/CLIProxyAPI/v7 v7.2.138/go.mod h1:lTHwMAGajc1wKGQiRtDvYbwV0FWsM7sy+N0ZU5/gxJQ=
github.com/router-for-me/CLIProxyAPI/v8 v8.0.0 h1:UfV1dhI+319cCquIKqY3c5VpJCP81Y052PPKCIhx0l0=
github.com/router-for-me/CLIProxyAPI/v8 v8.0.0/go.mod h1:j1Mtcj0TrC66O1JwbSBB03lY3uYPx4J7yHNvU6nDBCo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
2 changes: 1 addition & 1 deletion internal/adapter/chatcompletions/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"net/http"
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/catalog"
Expand Down
4 changes: 1 addition & 3 deletions internal/adapter/chatcompletions/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/errclass"
)
Expand Down Expand Up @@ -586,7 +586,6 @@ func TestFromResponsesRequest_FunctionCallOutputArray(t *testing.T) {
}
}


func TestBuildRequestResponsesVariants(t *testing.T) {
t.Run("empty string input yields no messages", func(t *testing.T) {
m := mustBuild(t, "openai-response", `{"input":""}`, nil)
Expand Down Expand Up @@ -1010,4 +1009,3 @@ func TestResponsesReasoningEffortPassthroughWithoutValidation(t *testing.T) {
t.Fatalf("reasoning_effort = %v, want xhigh", m["reasoning_effort"])
}
}

2 changes: 1 addition & 1 deletion internal/adapter/messages/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/errclass"
Expand Down
3 changes: 1 addition & 2 deletions internal/adapter/messages/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/errclass"
Expand Down Expand Up @@ -493,7 +493,6 @@ func TestFromResponsesRequest_FunctionCallOutputArray(t *testing.T) {
}
}


func TestResponsesReasoningOmittedWhenEmptySummary(t *testing.T) {
m, eErr := respReq(t, `{"input":[{"type":"reasoning","summary":[]}]}`)
if eErr != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/adapter/responses/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"slices"
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/catalog"
Expand Down
2 changes: 1 addition & 1 deletion internal/adapter/responses/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/errclass"
Expand Down
2 changes: 1 addition & 1 deletion internal/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"strings"
"sync"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/config"
)
Expand Down
2 changes: 1 addition & 1 deletion internal/catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"sync"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/config"
)
Expand Down
4 changes: 3 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type ModelPrefix struct {
}

type APIKey struct {
Name string
Value string
}

Expand Down Expand Up @@ -82,6 +83,7 @@ type rawPrefix struct {
}

type rawKey struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
}

Expand Down Expand Up @@ -114,7 +116,7 @@ func Load(yamlBytes []byte) (Config, error) {
}
keys := make([]APIKey, len(raw.APIKeys))
for i, k := range raw.APIKeys {
keys[i] = APIKey{Value: os.ExpandEnv(k.Value)}
keys[i] = APIKey{Value: os.ExpandEnv(k.Value), Name: strings.TrimSpace(k.Name)}
}
refreshInterval, err := parseDuration("catalog.refresh-interval", raw.Catalog.RefreshInterval, DefaultRefreshInterval)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/errclass/errclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"regexp"
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
)

// Class is a stable error-category identifier (FR-009) used as the
Expand Down
2 changes: 1 addition & 1 deletion internal/errclass/errclass_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
)

func TestClassConstants(t *testing.T) {
Expand Down
31 changes: 27 additions & 4 deletions internal/plugin/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
"opencode-go-cliproxyapi/internal/config"
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)

type authProvider struct{}
type authProvider struct{ cfg config.Config }

var _ pluginapi.AuthProvider = authProvider{}

func (authProvider) Identifier() string { return ProviderID }

func (authProvider) ParseAuth(_ context.Context, req pluginapi.AuthParseRequest) (pluginapi.AuthParseResponse, error) {
func (p authProvider) ParseAuth(_ context.Context, req pluginapi.AuthParseRequest) (pluginapi.AuthParseResponse, error) {
debugTrace("auth parse request provider=%s file=%s raw_bytes=%d", req.Provider, req.FileName, len(req.RawJSON))
var raw struct {
Type string `json:"type"`
Expand All @@ -41,7 +42,7 @@ func (authProvider) ParseAuth(_ context.Context, req pluginapi.AuthParseRequest)
}
debugTrace("auth parse handled provider=%s file=%s id=%s api_key_present=%t api_key_length=%d", req.Provider, req.FileName, raw.ID, strings.TrimSpace(raw.APIKey) != "", len(raw.APIKey))
return pluginapi.AuthParseResponse{Handled: true, Auth: pluginapi.AuthData{
Provider: ProviderID, ID: raw.ID, FileName: req.FileName, Label: raw.Label, StorageJSON: req.RawJSON,
Provider: ProviderID, ID: raw.ID, FileName: req.FileName, Label: accountLabel(p.cfg, raw.APIKey, raw.Label, 0), StorageJSON: req.RawJSON,
Attributes: map[string]string{"api_key": raw.APIKey},
}}, nil
}
Expand All @@ -66,3 +67,25 @@ func mapKeys[T any](values map[string]T) []string {
}
return keys
}

// Labels are presentation only. Stable auth IDs and stored host metadata are preserved.
func accountLabel(cfg config.Config, key, existing string, fallbackIndex int) string {
index := fallbackIndex
for i, entry := range cfg.APIKeys {
if entry.Value == key {
if name := strings.TrimSpace(entry.Name); name != "" {
return name
}
index = i
break
}
}
label := strings.TrimSpace(existing)
if label != "" && !strings.HasPrefix(label, "OpenCode Go credential ") && !strings.HasPrefix(label, "opencode-go-key-") {
return label
}
if len(cfg.APIKeys) > 1 {
return fmt.Sprintf("OpenCode Go %d", index+1)
}
return "OpenCode Go"
}
4 changes: 2 additions & 2 deletions internal/plugin/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)

func TestAuthProviderParseAndRefresh(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions internal/plugin/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import (
"sync/atomic"
"time"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/chatcompletions"
"opencode-go-cliproxyapi/internal/adapter/messages"
Expand Down
4 changes: 2 additions & 2 deletions internal/plugin/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"testing"
"time"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"

"opencode-go-cliproxyapi/internal/adapter/shared"
"opencode-go-cliproxyapi/internal/catalog"
Expand Down
4 changes: 2 additions & 2 deletions internal/plugin/hostbridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"sync"
"time"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)

// RawCaller performs one host callback round-trip: method name plus JSON
Expand Down
4 changes: 2 additions & 2 deletions internal/plugin/hostbridge_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"testing"
"time"

"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)

func decodePayload(t *testing.T, c capturedCall) map[string]any {
Expand Down
Loading