From 83cfabe6d9a0ff819ecf05c76341fefd8fc8085c Mon Sep 17 00:00:00 2001 From: Pete Cornish Date: Thu, 30 Jul 2026 02:56:18 +0100 Subject: [PATCH 1/5] docs: propose Vertex AI providers and optionsRequired capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture an OpenSpec change adding google-vertex (Gemini) and google-vertex-anthropic (Claude) as GCP analogues of amazon-bedrock — ambient Google credentials, no injected API key — plus an optionsRequired schema addition so a caller-supplied option (the GCP project) with no usable default is validated at apply time. --- .../add-vertex-providers/.openspec.yaml | 2 + .../changes/add-vertex-providers/design.md | 70 ++++++++++++++ .../changes/add-vertex-providers/proposal.md | 49 ++++++++++ .../specs/provider-catalog/spec.md | 95 +++++++++++++++++++ .../changes/add-vertex-providers/tasks.md | 23 +++++ 5 files changed, 239 insertions(+) create mode 100644 openspec/changes/add-vertex-providers/.openspec.yaml create mode 100644 openspec/changes/add-vertex-providers/design.md create mode 100644 openspec/changes/add-vertex-providers/proposal.md create mode 100644 openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md create mode 100644 openspec/changes/add-vertex-providers/tasks.md diff --git a/openspec/changes/add-vertex-providers/.openspec.yaml b/openspec/changes/add-vertex-providers/.openspec.yaml new file mode 100644 index 00000000..ab396754 --- /dev/null +++ b/openspec/changes/add-vertex-providers/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-30 diff --git a/openspec/changes/add-vertex-providers/design.md b/openspec/changes/add-vertex-providers/design.md new file mode 100644 index 00000000..0c2763f5 --- /dev/null +++ b/openspec/changes/add-vertex-providers/design.md @@ -0,0 +1,70 @@ +# Design + +## Context + +Bedrock is a catalogue-only entry today — no Go code special-cases it. Adding +Vertex follows the same path: mostly `providers.yaml`, plus one small schema +addition (`optionsRequired`) because Vertex, unlike Bedrock, has no usable +default for a caller-supplied value. + +## Decisions + +### Two providers, keyed by opencode's built-in ids + +opencode resolves a provider with no `npm` against its built-in registry, keyed +by the provider id (`opencode.go` writes the block under `/provider/`). Its +built-in ids are `google-vertex` (Gemini) and `google-vertex-anthropic` +(Claude). We use those keys verbatim, so neither provider needs an npm package — +identical to how `amazon-bedrock` works. (Naming the Gemini one +`google-vertex-gemini` was considered and rejected: it would not match the +built-in id and would then require `npm: "@ai-sdk/google-vertex"` to load as a +custom provider, for no benefit.) + +### No API key; ambient credentials + +Like Bedrock, neither provider sets `apiKeyEnv`. The AI SDK's Vertex provider +reads Application Default Credentials (`gcloud auth application-default login`, +or a service-account key file via `GOOGLE_APPLICATION_CREDENTIALS`). outfit +injects nothing secret. + +### Options: `project` (required) and `location` (default `global`) + +```yaml +options: + location: global +optionsRequired: + - project +optionsFromEnv: + project: GOOGLE_VERTEX_PROJECT + location: GOOGLE_VERTEX_LOCATION +``` + +`project` has no sensible default (it is caller-specific), so it is required. +`location` defaults to `global`, overridable via env. + +### `optionsRequired` schema addition + +Add `OptionsRequired []string` to `catalog.Provider`. In `BuildProviderBlock`, +after merging static options and `optionsFromEnv`, check each listed key is +present and non-empty in the resolved options map; otherwise return an error +naming the option and, when known, the environment variable from +`optionsFromEnv` that supplies it. This runs only in the opencode build path; +Vertex has no `pi` block, so `BuildPiProvider` already rejects it with the +existing "not supported by the pi harness" error. + +`apiKeyRequired` stays as-is — it guards the injected key, a separate concern +from a required option. + +## Model ids (informational, not in the catalogue) + +The catalogue no longer enumerates models; the user names them. For reference in +docs/examples: +- Claude on Vertex: `claude-3-5-sonnet-v2@20241022` (publisher `anthropic`, `@date` form). +- Gemini on Vertex: `gemini-2.0-flash`, `gemini-1.5-pro`. + +## Risks / trade-offs + +- The `location: global` default may not serve every model in every region; + users override via `GOOGLE_VERTEX_LOCATION`. Documented, not enforced. +- Exact opencode option key (`location` vs `region`) and model-id forms should + be spot-checked against a current opencode build during implementation. diff --git a/openspec/changes/add-vertex-providers/proposal.md b/openspec/changes/add-vertex-providers/proposal.md new file mode 100644 index 00000000..e3dadc2a --- /dev/null +++ b/openspec/changes/add-vertex-providers/proposal.md @@ -0,0 +1,49 @@ +## Why + +`outfit` can point a harness at Claude on AWS Bedrock, but there is no GCP +equivalent. Google Cloud's Vertex AI is the direct analogue — it hosts both +Gemini and Anthropic Claude — and teams standardised on GCP have no first-class +provider today. Adding it mirrors the Bedrock pattern (cloud creds, no injected +API key) and needs only catalogue plumbing plus one small schema addition. + +## What Changes + +- Add two built-in providers to the catalogue: + - `google-vertex` — Gemini on Vertex AI (opencode's built-in provider id). + - `google-vertex-anthropic` — Claude on Vertex AI (opencode's built-in id). + Neither injects an API key: like `amazon-bedrock`, authentication rides on + ambient Google credentials (Application Default Credentials — `gcloud auth + application-default login`, or a service-account key file). Both take + `project` and `location` options, resolved from `GOOGLE_VERTEX_PROJECT` and + `GOOGLE_VERTEX_LOCATION`, with `location` defaulting to `global`. Neither has + a `pi` block: Vertex authenticates with OAuth/ADC, which the Pi harness does + not support (its `google-generative-ai` protocol is the API-key Gemini + Developer API, a different service). +- Add an `optionsRequired` capability to the provider catalogue: a provider MAY + declare a list of option keys that must resolve to a non-empty value, or the + command fails with a clear error. Vertex needs this because it has no usable + default `project` — unlike Bedrock, which ships a working default region. The + existing `apiKeyRequired` only guards the injected API key, not options. + +## Capabilities + +### New Capabilities + +None. Both changes extend the existing catalogue capability. + +### Modified Capabilities + +- `provider-catalog`: extend the embedded-provider schema with `optionsRequired` + (a validated, non-empty-option check at apply time), and ship the two Vertex + providers as part of the built-in catalogue. + +## Impact + +- `internal/catalog/providers.yaml` — two new provider entries; header-comment + documentation for `optionsRequired`. +- `internal/catalog/catalog.go` — `OptionsRequired []string` on `Provider`; + a validation step in `BuildProviderBlock` (opencode). No Go code enumerates + models, so nothing else changes there. +- `outfit list` — the two providers appear in the alphabetical listing. +- Docs: `docs/commands/add.md` gains a Vertex example alongside the Bedrock one. +- No change to the Pi harness (both providers are opencode-only, as Bedrock is). diff --git a/openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md b/openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md new file mode 100644 index 00000000..b9960b3a --- /dev/null +++ b/openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md @@ -0,0 +1,95 @@ +## MODIFIED Requirements + +### Requirement: Embedded provider definitions + +The system SHALL ship with a built-in catalogue of providers, defined in a YAML +file (`providers.yaml`) embedded into the binary at build time. Each provider +entry declares a description and MAY declare a display name, an npm package, an +API key environment variable (with optional required flag, optional flag, and +expected key prefix), static options (such as `baseURL`), options resolved from +environment variables, a list of option keys that are required, and a `pi` block +marking the provider as usable by the Pi harness. The catalogue SHALL NOT +enumerate models: the model a provider serves is named by the user's selection, +not stored in the catalogue. + +A provider whose API key is declared optional is one that also works +unauthenticated — the same engine run as a local server and as an +authenticated remote endpoint — so an unset key variable SHALL mean "no key", +not "a key that is missing". + +A provider MAY declare `optionsRequired`: a list of option keys that MUST resolve +to a non-empty value (whether from static options or from an environment-variable +mapping) when the provider is applied. When any listed key is missing or empty, +applying the provider SHALL fail with an error naming the option and the +environment variable that supplies it. This guards cloud providers that +authenticate via ambient credentials and so inject no API key, but still require +a caller-supplied value (such as a GCP project) that has no usable default. + +#### Scenario: Catalogue loads without external files + +- **WHEN** any command that needs the catalogue runs with no `--providers` flag + and no `OUTFIT_PROVIDERS` environment variable +- **THEN** the embedded catalogue is used, with no file read from disk + +#### Scenario: An optional key is injected only when set + +- **WHEN** a provider whose API key is optional is applied, with its key + variable set +- **THEN** the key is injected into the harness config + +#### Scenario: An optional key that is unset is not an error + +- **WHEN** the same provider is applied with the key variable unset +- **THEN** the configuration is written with no key, and the command succeeds + +#### Scenario: A required option that is unset is an error + +- **WHEN** a provider that declares a required option is applied and neither the + option's static value nor its environment-variable source resolves to a + non-empty value +- **THEN** the command fails with an error naming the option and its + environment variable, and no config is written + +#### Scenario: A required option resolved from the environment succeeds + +- **WHEN** a provider that declares a required option is applied with that + option's environment variable set to a non-empty value +- **THEN** the option is injected into the harness config and the command + succeeds + +## ADDED Requirements + +### Requirement: Vertex AI providers + +The built-in catalogue SHALL include two Google Cloud Vertex AI providers, +keyed by the provider ids opencode resolves natively (so neither declares an npm +package): + +- `google-vertex` — Gemini models on Vertex AI. +- `google-vertex-anthropic` — Anthropic Claude models on Vertex AI. + +Both SHALL authenticate via ambient Google credentials (Application Default +Credentials) and SHALL NOT inject an API key, mirroring `amazon-bedrock`. Both +SHALL declare a `project` option (required, resolved from `GOOGLE_VERTEX_PROJECT`) +and a `location` option (resolved from `GOOGLE_VERTEX_LOCATION`, defaulting to +`global`). Neither SHALL declare a `pi` block: Vertex is not supported by the Pi +harness. + +#### Scenario: Applying a Vertex provider with a project set + +- **WHEN** the user applies `google-vertex-anthropic` (or `google-vertex`) with + `GOOGLE_VERTEX_PROJECT` set to a non-empty value +- **THEN** an opencode provider block is written with the `project` and + `location` options and no `apiKey`, and the command succeeds + +#### Scenario: Applying a Vertex provider without a project fails + +- **WHEN** the user applies a Vertex provider with `GOOGLE_VERTEX_PROJECT` unset + and no `project` override +- **THEN** the command fails with an error naming the `project` option and + `GOOGLE_VERTEX_PROJECT` + +#### Scenario: Vertex is not offered to the Pi harness + +- **WHEN** the user targets the Pi harness with a Vertex provider +- **THEN** the command reports the provider as unsupported by the Pi harness diff --git a/openspec/changes/add-vertex-providers/tasks.md b/openspec/changes/add-vertex-providers/tasks.md new file mode 100644 index 00000000..6c3a46ef --- /dev/null +++ b/openspec/changes/add-vertex-providers/tasks.md @@ -0,0 +1,23 @@ +## 1. optionsRequired schema + +- [ ] 1.1 Add `OptionsRequired []string` (yaml `optionsRequired`) to `Provider` in `internal/catalog/catalog.go` +- [ ] 1.2 In `BuildProviderBlock`, after merging options + optionsFromEnv, fail when any required option is missing/empty, naming the option and (when mapped) its env var +- [ ] 1.3 Document `optionsRequired` in the `providers.yaml` header comment + +## 2. Vertex providers + +- [ ] 2.1 Add `google-vertex` provider (Gemini): no npm, no apiKey, `location: global`, `optionsRequired: [project]`, optionsFromEnv `project`/`location`, no pi block +- [ ] 2.2 Add `google-vertex-anthropic` provider (Claude): same shape as 2.1 +- [ ] 2.3 Spot-check option key (`location` vs `region`) and model-id forms against a current opencode build + +## 3. Tests + +- [ ] 3.1 `catalog` test: required option unset → error names option + env var; set → injected into block +- [ ] 3.2 `catalog` test: both Vertex providers build with no apiKey and expected options; `BuildPiProvider` rejects them +- [ ] 3.3 `outfit list` test/golden: both providers appear, opencode-only +- [ ] 3.4 `go test ./... -cover` stays >= 80%; run gofmt + +## 4. Docs + +- [ ] 4.1 Add a Vertex example to `docs/commands/add.md` beside the Bedrock one +- [ ] 4.2 Note ADC/`gcloud auth application-default login` and `GOOGLE_VERTEX_PROJECT` in the relevant docs From c7e0bde09d368217711ee27af6a2639971283b3b Mon Sep 17 00:00:00 2001 From: Pete Cornish Date: Sat, 1 Aug 2026 01:16:37 +0100 Subject: [PATCH 2/5] feat: add Vertex AI providers and optionsRequired capability Implement the proposal: google-vertex (Gemini) and google-vertex-anthropic (Claude), keyed by opencode's built-in ids so neither needs an npm package. Both authenticate via ambient Google credentials (ADC) and inject no API key, mirroring amazon-bedrock; they take a required project option and a location that defaults to global, from GOOGLE_VERTEX_PROJECT / GOOGLE_VERTEX_LOCATION. Add optionsRequired to the catalogue schema: BuildProviderBlock fails when a listed option resolves to nothing, naming the option and its env var. This guards a caller-supplied option with no usable default (the GCP project) on a keyless provider, which apiKeyRequired does not cover. --- cmd/outfit/main_test.go | 15 +++- docs/commands/add.md | 15 ++++ internal/catalog/catalog.go | 17 ++++ internal/catalog/catalog_test.go | 78 +++++++++++++++++++ internal/catalog/providers.yaml | 31 ++++++++ .../changes/add-vertex-providers/tasks.md | 24 +++--- 6 files changed, 167 insertions(+), 13 deletions(-) diff --git a/cmd/outfit/main_test.go b/cmd/outfit/main_test.go index 85a46915..0d71a682 100644 --- a/cmd/outfit/main_test.go +++ b/cmd/outfit/main_test.go @@ -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) diff --git a/docs/commands/add.md b/docs/commands/add.md index 2b51e30d..dd81043f 100644 --- a/docs/commands/add.md +++ b/docs/commands/add.md @@ -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 @@ -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. diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index a20eb5d4..708f689f 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -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. @@ -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) diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go index fcdea887..b9d86d9e 100644 --- a/internal/catalog/catalog_test.go +++ b/internal/catalog/catalog_test.go @@ -42,6 +42,14 @@ func TestCatalogIntegrity(t *testing.T) { if _, hasBaseURL := p.Options["baseURL"]; hasBaseURL && p.NPM == "" { t.Errorf("provider %q: baseURL set without npm package", name) } + // A required option must have a source: a static option or an env mapping. + for _, opt := range p.OptionsRequired { + _, inStatic := p.Options[opt] + _, inEnv := p.OptionsFromEnv[opt] + if !inStatic && !inEnv { + t.Errorf("provider %q: required option %q has no source (options or optionsFromEnv)", name, opt) + } + } } } @@ -280,6 +288,76 @@ func TestBuildProviderBlock_BedrockNoKeyRegionFromEnv(t *testing.T) { } } +func TestBuildProviderBlock_VertexRequiredOptionMissing(t *testing.T) { + cat, _ := Load() + for _, id := range []string{"google-vertex", "google-vertex-anthropic"} { + p := cat.Providers[id] + _, _, err := BuildProviderBlock(id, p, "some-model", "", noEnv) + if err == nil { + t.Fatalf("%s: expected error when required option project is missing", id) + } + if !strings.Contains(err.Error(), "project") || !strings.Contains(err.Error(), "GOOGLE_VERTEX_PROJECT") { + t.Errorf("%s: error %q should name the option and its env var", id, err) + } + } +} + +func TestBuildProviderBlock_VertexProjectAndLocation(t *testing.T) { + cat, _ := Load() + for _, id := range []string{"google-vertex", "google-vertex-anthropic"} { + p := cat.Providers[id] + block, model, err := BuildProviderBlock(id, p, "some-model", "", envMap(map[string]string{ + "GOOGLE_VERTEX_PROJECT": "my-proj", + })) + if err != nil { + t.Fatalf("%s: BuildProviderBlock: %v", id, err) + } + // Keyed by opencode's built-in id, so no npm is emitted. + if _, ok := block["npm"]; ok { + t.Errorf("%s: block should have no npm (opencode built-in)", id) + } + opts := block["options"].(map[string]any) + if _, ok := opts["apiKey"]; ok { + t.Errorf("%s: block should not carry an apiKey (ambient credentials)", id) + } + if opts["project"] != "my-proj" { + t.Errorf("%s: project = %v, want my-proj (from env)", id, opts["project"]) + } + if opts["location"] != "global" { + t.Errorf("%s: location = %v, want the global default", id, opts["location"]) + } + if !strings.HasPrefix(model, id+"/") { + t.Errorf("%s: model = %q, want %s/...", id, model, id) + } + } +} + +func TestBuildProviderBlock_VertexLocationOverride(t *testing.T) { + cat, _ := Load() + p := cat.Providers["google-vertex"] + block, _, err := BuildProviderBlock("google-vertex", p, "gemini-2.0-flash", "", envMap(map[string]string{ + "GOOGLE_VERTEX_PROJECT": "my-proj", + "GOOGLE_VERTEX_LOCATION": "europe-west4", + })) + if err != nil { + t.Fatalf("BuildProviderBlock: %v", err) + } + opts := block["options"].(map[string]any) + if opts["location"] != "europe-west4" { + t.Errorf("location = %v, want europe-west4 (env override)", opts["location"]) + } +} + +func TestBuildPiProvider_VertexUnsupported(t *testing.T) { + cat, _ := Load() + for _, id := range []string{"google-vertex", "google-vertex-anthropic"} { + p := cat.Providers[id] // no pi block + if _, _, err := BuildPiProvider(id, p, "some-model", "", noEnv); err == nil { + t.Fatalf("%s: expected pi to reject a provider with no pi block", id) + } + } +} + func TestBuildProviderBlock_CustomProviderDefaults(t *testing.T) { cat, _ := Load() p := cat.Providers["ollama"] diff --git a/internal/catalog/providers.yaml b/internal/catalog/providers.yaml index 8108b1e2..2bbcbe55 100644 --- a/internal/catalog/providers.yaml +++ b/internal/catalog/providers.yaml @@ -21,6 +21,10 @@ # apiKeyPrefix sanity-check that the key starts with this # options static options merged into the provider block # optionsFromEnv option -> env var; injected into options when the var is set +# optionsRequired list of option keys that must resolve to a non-empty value +# (from options or optionsFromEnv); applying the provider fails +# otherwise. For a caller-supplied option with no usable +# default (e.g. a Vertex AI project) on a keyless provider. # pi optional block marking the provider as usable by the Pi # harness (~/.pi/agent/models.json). Holds: # api Pi protocol: openai-completions | openai-responses @@ -48,6 +52,33 @@ providers: optionsFromEnv: region: AWS_REGION + google-vertex: + description: Gemini on GCP Vertex AI (authenticates via your Google credentials) + # No apiKey is injected: the Vertex SDK reads Application Default Credentials + # (run `gcloud auth application-default login`, or point + # GOOGLE_APPLICATION_CREDENTIALS at a service-account key file). The project + # is required (there is no sensible default); the location defaults to + # `global` and is overridable with GOOGLE_VERTEX_LOCATION. + options: + location: global + optionsRequired: + - project + optionsFromEnv: + project: GOOGLE_VERTEX_PROJECT + location: GOOGLE_VERTEX_LOCATION + + google-vertex-anthropic: + description: Claude on GCP Vertex AI (authenticates via your Google credentials) + # Same authentication and options as google-vertex; this provider serves + # Anthropic Claude models rather than Gemini. + options: + location: global + optionsRequired: + - project + optionsFromEnv: + project: GOOGLE_VERTEX_PROJECT + location: GOOGLE_VERTEX_LOCATION + ollama: description: Local Ollama server (OpenAI-compatible API) name: Ollama (local) diff --git a/openspec/changes/add-vertex-providers/tasks.md b/openspec/changes/add-vertex-providers/tasks.md index 6c3a46ef..f772dc52 100644 --- a/openspec/changes/add-vertex-providers/tasks.md +++ b/openspec/changes/add-vertex-providers/tasks.md @@ -1,23 +1,23 @@ ## 1. optionsRequired schema -- [ ] 1.1 Add `OptionsRequired []string` (yaml `optionsRequired`) to `Provider` in `internal/catalog/catalog.go` -- [ ] 1.2 In `BuildProviderBlock`, after merging options + optionsFromEnv, fail when any required option is missing/empty, naming the option and (when mapped) its env var -- [ ] 1.3 Document `optionsRequired` in the `providers.yaml` header comment +- [x] 1.1 Add `OptionsRequired []string` (yaml `optionsRequired`) to `Provider` in `internal/catalog/catalog.go` +- [x] 1.2 In `BuildProviderBlock`, after merging options + optionsFromEnv, fail when any required option is missing/empty, naming the option and (when mapped) its env var +- [x] 1.3 Document `optionsRequired` in the `providers.yaml` header comment ## 2. Vertex providers -- [ ] 2.1 Add `google-vertex` provider (Gemini): no npm, no apiKey, `location: global`, `optionsRequired: [project]`, optionsFromEnv `project`/`location`, no pi block -- [ ] 2.2 Add `google-vertex-anthropic` provider (Claude): same shape as 2.1 -- [ ] 2.3 Spot-check option key (`location` vs `region`) and model-id forms against a current opencode build +- [x] 2.1 Add `google-vertex` provider (Gemini): no npm, no apiKey, `location: global`, `optionsRequired: [project]`, optionsFromEnv `project`/`location`, no pi block +- [x] 2.2 Add `google-vertex-anthropic` provider (Claude): same shape as 2.1 +- [x] 2.3 Spot-check option key (`location` vs `region`) and model-id forms against a current opencode build ## 3. Tests -- [ ] 3.1 `catalog` test: required option unset → error names option + env var; set → injected into block -- [ ] 3.2 `catalog` test: both Vertex providers build with no apiKey and expected options; `BuildPiProvider` rejects them -- [ ] 3.3 `outfit list` test/golden: both providers appear, opencode-only -- [ ] 3.4 `go test ./... -cover` stays >= 80%; run gofmt +- [x] 3.1 `catalog` test: required option unset → error names option + env var; set → injected into block +- [x] 3.2 `catalog` test: both Vertex providers build with no apiKey and expected options; `BuildPiProvider` rejects them +- [x] 3.3 `outfit list` test/golden: both providers appear, opencode-only +- [x] 3.4 `go test ./... -cover` stays >= 80%; run gofmt ## 4. Docs -- [ ] 4.1 Add a Vertex example to `docs/commands/add.md` beside the Bedrock one -- [ ] 4.2 Note ADC/`gcloud auth application-default login` and `GOOGLE_VERTEX_PROJECT` in the relevant docs +- [x] 4.1 Add a Vertex example to `docs/commands/add.md` beside the Bedrock one +- [x] 4.2 Note ADC/`gcloud auth application-default login` and `GOOGLE_VERTEX_PROJECT` in the relevant docs From ce7a57221c7a39b1e9cec3176c1037c02de214d6 Mon Sep 17 00:00:00 2001 From: Pete Cornish Date: Sat, 1 Aug 2026 01:31:18 +0100 Subject: [PATCH 3/5] test: cover Vertex providers and optionsRequired edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for the paths the initial change left uncovered: a required option satisfied by a static value and an unmapped required option (the catalogue-side error), taking BuildProviderBlock to full coverage; and CLI-level tests driving the whole add path — google-vertex-anthropic fails without GOOGLE_VERTEX_PROJECT and, once set, writes a block with project and the global location default and no apiKey — plus Pi rejection of both Vertex providers. --- cmd/outfit/harness_test.go | 48 ++++++++++++++++++++++++++++++++ internal/catalog/catalog_test.go | 28 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/cmd/outfit/harness_test.go b/cmd/outfit/harness_test.go index f01f5799..38a72454 100644 --- a/cmd/outfit/harness_test.go +++ b/cmd/outfit/harness_test.go @@ -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") diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go index b9d86d9e..db51feb1 100644 --- a/internal/catalog/catalog_test.go +++ b/internal/catalog/catalog_test.go @@ -302,6 +302,34 @@ func TestBuildProviderBlock_VertexRequiredOptionMissing(t *testing.T) { } } +func TestBuildProviderBlock_RequiredOptionSources(t *testing.T) { + // A required option is satisfied by a static option value, with no env + // mapping needed. + fromStatic := &Provider{ + Description: "custom", + Options: map[string]any{"project": "static-proj"}, + OptionsRequired: []string{"project"}, + } + block, _, err := BuildProviderBlock("custom", fromStatic, "m", "", noEnv) + if err != nil { + t.Fatalf("a static required option should satisfy the requirement: %v", err) + } + if block["options"].(map[string]any)["project"] != "static-proj" { + t.Errorf("project = %v, want static-proj", block["options"].(map[string]any)["project"]) + } + + // A required option with no source at all fails with the catalogue-side + // message (there is no env var to name). + unmapped := &Provider{ + Description: "custom", + OptionsRequired: []string{"project"}, + } + _, _, err = BuildProviderBlock("custom", unmapped, "m", "", noEnv) + if err == nil || !strings.Contains(err.Error(), "catalogue options") { + t.Fatalf("an unmapped required option should fail with the catalogue message, got %v", err) + } +} + func TestBuildProviderBlock_VertexProjectAndLocation(t *testing.T) { cat, _ := Load() for _, id := range []string{"google-vertex", "google-vertex-anthropic"} { From 247bf26007bcd3a7c36af737ecd55f02a10d44f9 Mon Sep 17 00:00:00 2001 From: Pete Cornish Date: Sat, 1 Aug 2026 01:46:42 +0100 Subject: [PATCH 4/5] docs: add cloud-provider examples for Bedrock and Vertex AI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add runnable Outfit examples with walkthroughs for the ambient-credential cloud providers: Claude on amazon-bedrock, and Claude and Gemini on the new google-vertex-anthropic / google-vertex providers. Each README covers the credential setup (AWS creds; Google ADC plus GOOGLE_VERTEX_PROJECT) and applying the Outfit. The Outfits use MODEL alone — no ALIAS, which would replace the real model id opencode sends to the provider. --- examples/amazon-bedrock/claude-sonnet/Outfit | 7 ++ .../amazon-bedrock/claude-sonnet/README.md | 63 +++++++++++++++++ .../claude-sonnet/Outfit | 8 +++ .../claude-sonnet/README.md | 67 +++++++++++++++++++ examples/google-vertex/gemini-flash/Outfit | 8 +++ examples/google-vertex/gemini-flash/README.md | 57 ++++++++++++++++ 6 files changed, 210 insertions(+) create mode 100644 examples/amazon-bedrock/claude-sonnet/Outfit create mode 100644 examples/amazon-bedrock/claude-sonnet/README.md create mode 100644 examples/google-vertex-anthropic/claude-sonnet/Outfit create mode 100644 examples/google-vertex-anthropic/claude-sonnet/README.md create mode 100644 examples/google-vertex/gemini-flash/Outfit create mode 100644 examples/google-vertex/gemini-flash/README.md diff --git a/examples/amazon-bedrock/claude-sonnet/Outfit b/examples/amazon-bedrock/claude-sonnet/Outfit new file mode 100644 index 00000000..05dc0b65 --- /dev/null +++ b/examples/amazon-bedrock/claude-sonnet/Outfit @@ -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 diff --git a/examples/amazon-bedrock/claude-sonnet/README.md b/examples/amazon-bedrock/claude-sonnet/README.md new file mode 100644 index 00000000..51e3c884 --- /dev/null +++ b/examples/amazon-bedrock/claude-sonnet/README.md @@ -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. diff --git a/examples/google-vertex-anthropic/claude-sonnet/Outfit b/examples/google-vertex-anthropic/claude-sonnet/Outfit new file mode 100644 index 00000000..f49fbe0e --- /dev/null +++ b/examples/google-vertex-anthropic/claude-sonnet/Outfit @@ -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 diff --git a/examples/google-vertex-anthropic/claude-sonnet/README.md b/examples/google-vertex-anthropic/claude-sonnet/README.md new file mode 100644 index 00000000..07a8f483 --- /dev/null +++ b/examples/google-vertex-anthropic/claude-sonnet/README.md @@ -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). diff --git a/examples/google-vertex/gemini-flash/Outfit b/examples/google-vertex/gemini-flash/Outfit new file mode 100644 index 00000000..c63d2317 --- /dev/null +++ b/examples/google-vertex/gemini-flash/Outfit @@ -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 diff --git a/examples/google-vertex/gemini-flash/README.md b/examples/google-vertex/gemini-flash/README.md new file mode 100644 index 00000000..05271f45 --- /dev/null +++ b/examples/google-vertex/gemini-flash/README.md @@ -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). From 94752f0a436e46b3e71dafb30ebc9893b9c8010e Mon Sep 17 00:00:00 2001 From: Pete Cornish Date: Sat, 1 Aug 2026 02:01:57 +0100 Subject: [PATCH 5/5] docs: sync provider-catalog spec and archive the Vertex change Merge the add-vertex-providers delta into the main provider-catalog spec (optionsRequired on Embedded provider definitions, plus the new Vertex AI providers requirement) and move the completed change to openspec/changes/archive/2026-08-01-add-vertex-providers. --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/provider-catalog/spec.md | 0 .../2026-08-01-add-vertex-providers}/tasks.md | 0 openspec/specs/provider-catalog/spec.md | 65 ++++++++++++++++++- 6 files changed, 62 insertions(+), 3 deletions(-) rename openspec/changes/{add-vertex-providers => archive/2026-08-01-add-vertex-providers}/.openspec.yaml (100%) rename openspec/changes/{add-vertex-providers => archive/2026-08-01-add-vertex-providers}/design.md (100%) rename openspec/changes/{add-vertex-providers => archive/2026-08-01-add-vertex-providers}/proposal.md (100%) rename openspec/changes/{add-vertex-providers => archive/2026-08-01-add-vertex-providers}/specs/provider-catalog/spec.md (100%) rename openspec/changes/{add-vertex-providers => archive/2026-08-01-add-vertex-providers}/tasks.md (100%) diff --git a/openspec/changes/add-vertex-providers/.openspec.yaml b/openspec/changes/archive/2026-08-01-add-vertex-providers/.openspec.yaml similarity index 100% rename from openspec/changes/add-vertex-providers/.openspec.yaml rename to openspec/changes/archive/2026-08-01-add-vertex-providers/.openspec.yaml diff --git a/openspec/changes/add-vertex-providers/design.md b/openspec/changes/archive/2026-08-01-add-vertex-providers/design.md similarity index 100% rename from openspec/changes/add-vertex-providers/design.md rename to openspec/changes/archive/2026-08-01-add-vertex-providers/design.md diff --git a/openspec/changes/add-vertex-providers/proposal.md b/openspec/changes/archive/2026-08-01-add-vertex-providers/proposal.md similarity index 100% rename from openspec/changes/add-vertex-providers/proposal.md rename to openspec/changes/archive/2026-08-01-add-vertex-providers/proposal.md diff --git a/openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md b/openspec/changes/archive/2026-08-01-add-vertex-providers/specs/provider-catalog/spec.md similarity index 100% rename from openspec/changes/add-vertex-providers/specs/provider-catalog/spec.md rename to openspec/changes/archive/2026-08-01-add-vertex-providers/specs/provider-catalog/spec.md diff --git a/openspec/changes/add-vertex-providers/tasks.md b/openspec/changes/archive/2026-08-01-add-vertex-providers/tasks.md similarity index 100% rename from openspec/changes/add-vertex-providers/tasks.md rename to openspec/changes/archive/2026-08-01-add-vertex-providers/tasks.md diff --git a/openspec/specs/provider-catalog/spec.md b/openspec/specs/provider-catalog/spec.md index aa98f49b..853b4609 100644 --- a/openspec/specs/provider-catalog/spec.md +++ b/openspec/specs/provider-catalog/spec.md @@ -65,15 +65,24 @@ file (`providers.yaml`) embedded into the binary at build time. Each provider entry declares a description and MAY declare a display name, an npm package, an API key environment variable (with optional required flag, optional flag, and expected key prefix), static options (such as `baseURL`), options resolved from -environment variables, and a `pi` block marking the provider as usable by the Pi -harness. The catalogue SHALL NOT enumerate models: the model a provider serves is -named by the user's selection, not stored in the catalogue. +environment variables, a list of option keys that are required, and a `pi` block +marking the provider as usable by the Pi harness. The catalogue SHALL NOT +enumerate models: the model a provider serves is named by the user's selection, +not stored in the catalogue. A provider whose API key is declared optional is one that also works unauthenticated — the same engine run as a local server and as an authenticated remote endpoint — so an unset key variable SHALL mean "no key", not "a key that is missing". +A provider MAY declare `optionsRequired`: a list of option keys that MUST resolve +to a non-empty value (whether from static options or from an environment-variable +mapping) when the provider is applied. When any listed key is missing or empty, +applying the provider SHALL fail with an error naming the option and the +environment variable that supplies it. This guards cloud providers that +authenticate via ambient credentials and so inject no API key, but still require +a caller-supplied value (such as a GCP project) that has no usable default. + #### Scenario: Catalogue loads without external files - **WHEN** any command that needs the catalogue runs with no `--providers` flag @@ -91,3 +100,53 @@ not "a key that is missing". - **WHEN** the same provider is applied with the key variable unset - **THEN** the configuration is written with no key, and the command succeeds +#### Scenario: A required option that is unset is an error + +- **WHEN** a provider that declares a required option is applied and neither the + option's static value nor its environment-variable source resolves to a + non-empty value +- **THEN** the command fails with an error naming the option and its + environment variable, and no config is written + +#### Scenario: A required option resolved from the environment succeeds + +- **WHEN** a provider that declares a required option is applied with that + option's environment variable set to a non-empty value +- **THEN** the option is injected into the harness config and the command + succeeds + +### Requirement: Vertex AI providers + +The built-in catalogue SHALL include two Google Cloud Vertex AI providers, +keyed by the provider ids opencode resolves natively (so neither declares an npm +package): + +- `google-vertex` — Gemini models on Vertex AI. +- `google-vertex-anthropic` — Anthropic Claude models on Vertex AI. + +Both SHALL authenticate via ambient Google credentials (Application Default +Credentials) and SHALL NOT inject an API key, mirroring `amazon-bedrock`. Both +SHALL declare a `project` option (required, resolved from `GOOGLE_VERTEX_PROJECT`) +and a `location` option (resolved from `GOOGLE_VERTEX_LOCATION`, defaulting to +`global`). Neither SHALL declare a `pi` block: Vertex is not supported by the Pi +harness. + +#### Scenario: Applying a Vertex provider with a project set + +- **WHEN** the user applies `google-vertex-anthropic` (or `google-vertex`) with + `GOOGLE_VERTEX_PROJECT` set to a non-empty value +- **THEN** an opencode provider block is written with the `project` and + `location` options and no `apiKey`, and the command succeeds + +#### Scenario: Applying a Vertex provider without a project fails + +- **WHEN** the user applies a Vertex provider with `GOOGLE_VERTEX_PROJECT` unset + and no `project` override +- **THEN** the command fails with an error naming the `project` option and + `GOOGLE_VERTEX_PROJECT` + +#### Scenario: Vertex is not offered to the Pi harness + +- **WHEN** the user targets the Pi harness with a Vertex provider +- **THEN** the command reports the provider as unsupported by the Pi harness +