Skip to content
Merged
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
48 changes: 48 additions & 0 deletions cmd/outfit/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,54 @@ func TestCmdAdd_PiUnsupportedProvider(t *testing.T) {
}
}

func TestCmdAdd_PiUnsupportedVertex(t *testing.T) {
isolateConfig(t)
for _, p := range []string{"google-vertex", "google-vertex-anthropic"} {
t.Setenv("GOOGLE_VERTEX_PROJECT", "my-proj") // a project is set, so the failure is Pi support, not a missing option
if err := cmdAdd([]string{"-H", "pi", "-p", p, "-m", "some-model"}); err == nil {
t.Errorf("%s: expected error adding a Pi-unsupported provider", p)
}
}
}

func TestCmdAdd_VertexRequiresProject(t *testing.T) {
home := isolateConfig(t)

// Without a project, the whole `add` path fails with a clear error.
err := cmdAdd([]string{"-p", "google-vertex-anthropic", "-m", "claude-3-5-sonnet-v2@20241022"})
if err == nil || !strings.Contains(err.Error(), "GOOGLE_VERTEX_PROJECT") {
t.Fatalf("expected a missing-project error naming GOOGLE_VERTEX_PROJECT, got %v", err)
}

// With the project set, the provider is written to the opencode config with
// project and the default location, and no apiKey.
t.Setenv("GOOGLE_VERTEX_PROJECT", "my-proj")
captureStdout(t, func() {
if err := cmdAdd([]string{"-p", "google-vertex-anthropic", "-m", "claude-3-5-sonnet-v2@20241022"}); err != nil {
t.Fatalf("cmdAdd with project set: %v", err)
}
})

m := readConfigMap(t, filepath.Join(home, ".config", "opencode", "opencode.json"))
if m["model"] != "google-vertex-anthropic/claude-3-5-sonnet-v2@20241022" {
t.Errorf("default model = %v", m["model"])
}
prov := m["provider"].(map[string]any)["google-vertex-anthropic"].(map[string]any)
if _, hasNPM := prov["npm"]; hasNPM {
t.Error("google-vertex-anthropic should carry no npm (opencode built-in)")
}
opts := prov["options"].(map[string]any)
if opts["project"] != "my-proj" {
t.Errorf("project = %v, want my-proj", opts["project"])
}
if opts["location"] != "global" {
t.Errorf("location = %v, want the global default", opts["location"])
}
if _, hasKey := opts["apiKey"]; hasKey {
t.Error("google-vertex-anthropic should carry no apiKey (ambient credentials)")
}
}

func TestCmdExportRemove_PiRoundTrip(t *testing.T) {
home := isolateConfig(t)
t.Setenv("OUTFIT_HARNESS", "pi")
Expand Down
15 changes: 14 additions & 1 deletion cmd/outfit/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,24 @@ func TestCmdList(t *testing.T) {
t.Fatalf("cmdList: %v", err)
}
})
for _, want := range []string{"openrouter", "amazon-bedrock", "api key", "harnesses"} {
for _, want := range []string{"openrouter", "amazon-bedrock", "google-vertex", "google-vertex-anthropic", "api key", "harnesses"} {
if !strings.Contains(out, want) {
t.Errorf("list output missing %q:\n%s", want, out)
}
}
// Vertex is opencode-only (no pi block) and injects no key, so its entry
// carries neither a "pi" harness nor an "api key" line.
vertexLine := strings.Index(out, "google-vertex —")
if vertexLine < 0 {
t.Fatalf("google-vertex entry not found:\n%s", out)
}
rest := out[vertexLine:]
if end := strings.Index(rest[len("google-vertex —"):], "\n\n"); end >= 0 {
entry := rest[:end+len("google-vertex —")]
if strings.Contains(entry, "pi") {
t.Errorf("google-vertex should be opencode-only:\n%s", entry)
}
}
// The catalogue no longer enumerates models, so no family lines appear.
if strings.Contains(out, "family ") {
t.Errorf("list should not print family lines:\n%s", out)
Expand Down
15 changes: 15 additions & 0 deletions docs/commands/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ outfit add -p ollama -m llama3.2
# Claude on AWS Bedrock (uses your AWS credentials)
outfit add -p amazon-bedrock -m anthropic.claude-3-5-sonnet

# Claude on GCP Vertex AI (uses your Google credentials; set the project)
GOOGLE_VERTEX_PROJECT=my-gcp-project \
outfit add -p google-vertex-anthropic -m claude-3-5-sonnet-v2@20241022

# Gemini on GCP Vertex AI
GOOGLE_VERTEX_PROJECT=my-gcp-project \
outfit add -p google-vertex -m gemini-2.0-flash

# Any OpenAI-compatible endpoint
OPENAI_API_KEY=sk-... \
outfit add -p openai-compatible -m my-model --base-url https://my-endpoint/v1
Expand Down Expand Up @@ -53,6 +61,13 @@ outfit add -p llamacpp -m my-model -c 128k -o 32k
environment, and
never written anywhere they'll leak. A provider that requires a key tells you
which variable to set.
- Some cloud providers authenticate with ambient credentials instead of an API
key: `amazon-bedrock` via your AWS credentials, and `google-vertex` /
`google-vertex-anthropic` via Google Application Default Credentials (run
`gcloud auth application-default login`, or set `GOOGLE_APPLICATION_CREDENTIALS`
to a service-account key file). The Vertex providers need a project — set
`GOOGLE_VERTEX_PROJECT` (and optionally `GOOGLE_VERTEX_LOCATION`, which
defaults to `global`). These providers are opencode-only.
- On opencode, `add` sets the chosen model as the default. Pi has no
default-model setting, so `add` tells you which model to pick with `/model`.
- `--output` needs `--context`, and cannot exceed it.
Expand Down
7 changes: 7 additions & 0 deletions examples/amazon-bedrock/claude-sonnet/Outfit
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Claude 3.5 Sonnet on AWS Bedrock.
# No API key: the AWS SDK reads your ambient credentials (see the README).
# Override the region with AWS_REGION, or edit the provider's options.
PROVIDER amazon-bedrock
MODEL anthropic.claude-3-5-sonnet-20241022-v2:0 # the Bedrock model id opencode calls
CONTEXT 200k
OUTPUT 8k
63 changes: 63 additions & 0 deletions examples/amazon-bedrock/claude-sonnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Claude 3.5 Sonnet on AWS Bedrock

Point opencode at Anthropic's Claude running on [AWS Bedrock](https://aws.amazon.com/bedrock/),
using the [`Outfit`](Outfit) in this directory. Bedrock authenticates with your
**AWS credentials**, so there is no API key to set — `outfit` injects none.

## Prerequisites

- An AWS account with Bedrock enabled in your region, and **model access**
granted for the Claude model you want (request it once in the Bedrock console
under *Model access*).
- AWS credentials the SDK can find, via any of the usual sources:
- `aws configure` (a profile in `~/.aws`), then optionally `AWS_PROFILE`,
- environment credentials (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`,
plus `AWS_SESSION_TOKEN` for temporary creds),
- or an SSO session (`aws sso login`).
- IAM permission to invoke the model (`bedrock:InvokeModel` and, for streaming,
`bedrock:InvokeModelWithResponseStream`).

## 1. Choose a region

Bedrock is regional, and model availability differs by region. The provider
defaults to `us-east-1`; override it for this shell with `AWS_REGION`:

```sh
export AWS_REGION=eu-west-2
```

## 2. Apply the Outfit

```sh
outfit apply examples/amazon-bedrock/claude-sonnet/Outfit
# or, from this directory:
outfit apply
```

The Outfit is:

```dockerfile
PROVIDER amazon-bedrock
MODEL anthropic.claude-3-5-sonnet-20241022-v2:0
CONTEXT 200k
OUTPUT 8k
```

- `MODEL` is the Bedrock model id opencode calls — so use the exact id, not a
nickname. Swap in another the account has access to, e.g.
`anthropic.claude-3-5-haiku-20241022-v1:0`. Some models are only reachable
through a cross-region *inference profile*, whose id is prefixed with the
region group (e.g. `us.anthropic.claude-3-5-sonnet-20241022-v2:0`).
- `CONTEXT`/`OUTPUT` set opencode's context window and output cap.

No key is written anywhere: the AWS SDK resolves your credentials at run time.

## 3. Run it

```sh
opencode
```

Select `amazon-bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0`. If a call fails with an access or
credentials error, check that your AWS credentials are active
(`aws sts get-caller-identity`), the region is right, and model access is granted.
8 changes: 8 additions & 0 deletions examples/google-vertex-anthropic/claude-sonnet/Outfit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Claude 3.5 Sonnet on GCP Vertex AI.
# No API key: the Vertex SDK reads your Google Application Default Credentials.
# Set GOOGLE_VERTEX_PROJECT (required) and optionally GOOGLE_VERTEX_LOCATION
# (defaults to `global`). See the README.
PROVIDER google-vertex-anthropic
MODEL claude-3-5-sonnet-v2@20241022 # the Vertex (publisher: anthropic) model id
CONTEXT 200k
OUTPUT 8k
67 changes: 67 additions & 0 deletions examples/google-vertex-anthropic/claude-sonnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Claude 3.5 Sonnet on GCP Vertex AI

Point opencode at Anthropic's Claude running on
[GCP Vertex AI](https://cloud.google.com/vertex-ai), using the [`Outfit`](Outfit)
in this directory. Vertex authenticates with your **Google credentials**
(Application Default Credentials), so there is no API key to set — `outfit`
injects none. It does need a **project**.

## Prerequisites

- A Google Cloud project with the **Vertex AI API** enabled, and the Claude
model you want enabled in *Vertex AI → Model Garden* (Anthropic models are
granted per-project).
- Application Default Credentials the SDK can find, via either:
- `gcloud auth application-default login` (interactive), or
- a service-account key file pointed to by `GOOGLE_APPLICATION_CREDENTIALS`.
- IAM permission to call the model (e.g. the *Vertex AI User* role, which
includes `aiplatform.endpoints.predict`).

## 1. Set the project (and, optionally, the location)

The project is required — there is no default. The location defaults to
`global`; override it if you want a specific region:

```sh
export GOOGLE_VERTEX_PROJECT=my-gcp-project
export GOOGLE_VERTEX_LOCATION=us-east5 # optional; defaults to `global`
```

If `GOOGLE_VERTEX_PROJECT` is unset, `outfit apply` fails with a clear error
naming the option and the variable.

## 2. Apply the Outfit

```sh
outfit apply examples/google-vertex-anthropic/claude-sonnet/Outfit
# or, from this directory:
outfit apply
```

The Outfit is:

```dockerfile
PROVIDER google-vertex-anthropic
MODEL claude-3-5-sonnet-v2@20241022
CONTEXT 200k
OUTPUT 8k
```

- `MODEL` is the Vertex model id opencode calls — note the `@date` form
(publisher `anthropic`), unlike Bedrock's `anthropic.claude-…-v2:0`. Swap in
another the project has enabled, e.g. `claude-3-5-haiku@20241022`.
- `CONTEXT`/`OUTPUT` set opencode's context window and output cap.

No key is written anywhere: the Vertex SDK resolves your credentials at run time.
The `project` and `location` are stored as provider options.

## 3. Run it

```sh
opencode
```

Select `google-vertex-anthropic/claude-3-5-sonnet-v2@20241022`. This provider is opencode-only;
the Pi harness does not support Vertex (it needs OAuth/ADC rather than an API
key). For Gemini on Vertex instead, see
[`../../google-vertex/gemini-flash`](../../google-vertex/gemini-flash).
8 changes: 8 additions & 0 deletions examples/google-vertex/gemini-flash/Outfit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Gemini 2.0 Flash on GCP Vertex AI.
# No API key: the Vertex SDK reads your Google Application Default Credentials.
# Set GOOGLE_VERTEX_PROJECT (required) and optionally GOOGLE_VERTEX_LOCATION
# (defaults to `global`). See the README.
PROVIDER google-vertex
MODEL gemini-2.0-flash # the Vertex Gemini model id opencode calls
CONTEXT 1m
OUTPUT 8k
57 changes: 57 additions & 0 deletions examples/google-vertex/gemini-flash/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Gemini 2.0 Flash on GCP Vertex AI

Point opencode at Google's Gemini running on
[GCP Vertex AI](https://cloud.google.com/vertex-ai), using the [`Outfit`](Outfit)
in this directory. Like the Claude-on-Vertex example, this authenticates with
your **Google credentials** (Application Default Credentials) — no API key — and
needs a **project**. The difference is the provider (`google-vertex` serves
Gemini) and the model id.

## Prerequisites

- A Google Cloud project with the **Vertex AI API** enabled.
- Application Default Credentials the SDK can find, via either:
- `gcloud auth application-default login` (interactive), or
- a service-account key file pointed to by `GOOGLE_APPLICATION_CREDENTIALS`.
- IAM permission to call the model (e.g. the *Vertex AI User* role).

## 1. Set the project (and, optionally, the location)

```sh
export GOOGLE_VERTEX_PROJECT=my-gcp-project
export GOOGLE_VERTEX_LOCATION=us-central1 # optional; defaults to `global`
```

The project is required; `outfit apply` fails with a clear error if it is unset.

## 2. Apply the Outfit

```sh
outfit apply examples/google-vertex/gemini-flash/Outfit
# or, from this directory:
outfit apply
```

The Outfit is:

```dockerfile
PROVIDER google-vertex
MODEL gemini-2.0-flash
CONTEXT 1m
OUTPUT 8k
```

- `MODEL` is the Vertex Gemini model id opencode calls. Swap in another the
project can serve, e.g. `gemini-1.5-pro`.
- `CONTEXT`/`OUTPUT` set opencode's context window and output cap. Gemini 2.0
Flash takes a very large context (~1M tokens); adjust to taste.

## 3. Run it

```sh
opencode
```

Select `google-vertex/gemini-2.0-flash`. This provider is opencode-only; the Pi
harness does not support Vertex. For Claude on Vertex instead, see
[`../../google-vertex-anthropic/claude-sonnet`](../../google-vertex-anthropic/claude-sonnet).
17 changes: 17 additions & 0 deletions internal/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ type Provider struct {
APIKeyPrefix string `yaml:"apiKeyPrefix"`
Options map[string]any `yaml:"options"`
OptionsFromEnv map[string]string `yaml:"optionsFromEnv"`
// OptionsRequired lists option keys that must resolve to a non-empty value
// (from static options or optionsFromEnv) when the provider is applied.
// It guards a caller-supplied option that has no usable default — such as a
// Vertex AI project — on a provider that injects no API key. See
// BuildProviderBlock.
OptionsRequired []string `yaml:"optionsRequired"`
// Pi marks the provider as usable by the Pi harness and carries its
// Pi-specific settings. Nil when the provider has no `pi:` block, in which
// case BuildPiProvider reports it as unsupported.
Expand Down Expand Up @@ -146,6 +152,17 @@ func BuildProviderBlock(id string, p *Provider, modelOverride, baseURLOverride s
if baseURLOverride != "" {
options["baseURL"] = baseURLOverride
}
// A provider may require caller-supplied options that have no usable default
// (e.g. a Vertex AI project). Unlike the API key, these are plain options,
// so apiKeyRequired does not cover them.
for _, optKey := range p.OptionsRequired {
if v, ok := options[optKey]; !ok || v == nil || v == "" {
if env := p.OptionsFromEnv[optKey]; env != "" {
return nil, "", fmt.Errorf("the %q option is required for provider %q; set %s in your .env or environment", optKey, id, env)
}
return nil, "", fmt.Errorf("the %q option is required for provider %q; set it in the catalogue options", optKey, id)
}
}
if p.APIKeyEnv != "" {
key := resolve(p.APIKeyEnv)
baseURL, _ := options["baseURL"].(string)
Expand Down
Loading