Context
Forge today supports operator-set custom LLM endpoints via two well-known patterns:
| Provider |
Custom URL knob |
Wire format |
Auth |
| OpenAI-compatible (OpenRouter, vLLM, litellm, self-hosted Kimi/Llama, Together.ai, Anyscale, Bedrock OpenAI mode, …) |
provider: openai + model.base_url OR OPENAI_BASE_URL |
OpenAI Chat Completions |
Authorization: Bearer <OPENAI_API_KEY> |
| Anthropic |
provider: anthropic only (api.anthropic.com hardcoded as default) |
Anthropic Messages |
x-api-key: <ANTHROPIC_API_KEY> |
The forge init wizard's "Custom" option only sets provider: openai. The docs (forge-yaml-schema.md, forge.md skill) similarly guide operators to provider: openai + OPENAI_BASE_URL as the only "custom endpoint" path. There is no symmetric "Anthropic-format custom URL" surface — even though the underlying AnthropicClient already accepts cfg.BaseURL (forge-core/llm/providers/anthropic.go:27).
This issue closes the asymmetry plus adds the most-asked-for concrete target: AWS Bedrock with Anthropic-format requests.
Why "use OpenAI-compatible at Bedrock" isn't the answer
Bedrock now supports BOTH the OpenAI Chat Completions shape AND the native Anthropic Messages shape. Operators reach for the Anthropic shape when they want full access to Anthropic-specific features that don't round-trip cleanly through OpenAI's schema:
- Prompt caching (
cache_control blocks)
- Tool use in Anthropic's structured-content format
- Extended thinking (
thinking blocks)
- Vision with Anthropic's
image content blocks
- System prompts as a separate top-level field (cleaner than OpenAI's role=system message-mix)
- Token / cost reporting that matches what the Anthropic dashboard / Bedrock console shows (per-tier input tokens incl. cache hits, etc.)
For an agent already targeting Claude models, forcing the OpenAI translation layer is a lossy detour. The user wants provider: anthropic + a custom base URL.
Scope (proposed three-phase rollout)
Phase 1 — Documentation + wizard parity (small; no new code paths in the request shape)
Make provider: anthropic + model.base_url (and the ANTHROPIC_BASE_URL env safety net) a first-class documented pattern. The plumbing is already there; what's missing is discoverability.
- Update
docs/reference/forge-yaml-schema.md to spell out the Anthropic-format custom URL pattern alongside the OpenAI one — current text only mentions OpenAI-compatible endpoints.
- Update
.claude/skills/forge.md LLM provider section.
- Update
forge init wizard's "Custom" phase: after the operator enters a Base URL, ask "OpenAI shape or Anthropic shape?" — default OpenAI for back-compat. The wizard writes provider: openai or provider: anthropic accordingly into the generated forge.yaml.
- Honor
ANTHROPIC_BASE_URL in the env-derived BaseURL fallback path (same way OPENAI_BASE_URL is honored today via runner.go:482 LLMProviderEnvDomains).
Use case Phase 1 unlocks: any Anthropic-compatible endpoint that uses the standard x-api-key header — internal proxies, litellm forwarders, locally-running Anthropic-shape gateways, third-party Anthropic-Messages-API-compatible vendors.
Phase 2 — AWS Bedrock with Anthropic-format (the real motivating ask; needs SigV4)
Bedrock URLs look like https://bedrock-runtime.<region>.amazonaws.com/model/<bedrock-model-id>/invoke and authenticate via AWS Signature Version 4, not x-api-key. Even with model.base_url pointed at Bedrock, today's AnthropicClient would 403 because it stamps x-api-key on every request.
Two design options to evaluate:
Option A: New bedrock provider that wraps the request shape
model:
provider: bedrock
format: anthropic # or "openai"; selects the wire shape
name: anthropic.claude-sonnet-4-20250514-v1:0
region: us-east-1
# AWS creds resolved via the standard chain (env, ~/.aws/credentials, IAM role, …)
Pros: clean separation, room for Bedrock-specific knobs (guardrails ID, trace, throughput type), maps to how Bedrock is conceptually a distinct provider.
Cons: a new provider type to maintain. Duplicates wire shape logic with the existing openai / anthropic clients.
Option B: Generic auth_scheme field on the existing providers
model:
provider: anthropic
base_url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-sonnet-4-20250514-v1%3A0/invoke"
auth_scheme: aws_sigv4 # default "x_api_key"; "aws_sigv4" enables SigV4 signing on every request
aws_region: us-east-1
Pros: reuses the AnthropicClient's wire-shape code; symmetric auth_scheme knob would also unlock OpenAI-format on Bedrock (via provider: openai + auth_scheme: aws_sigv4).
Cons: stretches the provider semantics — operator picks a wire shape and an auth scheme orthogonally, which is the cleaner model long-term but more cognitive surface.
Recommended: Option B, because the symmetric auth_scheme knob also helps OAuth-on-bedrock, mTLS-fronted internal proxies, IAP-fronted endpoints, etc. without needing one provider type per new auth shape. But this is a design call worth landing in the issue before code.
Either way, the aws_sigv4 signing implementation reuses forge-core/auth/providers/aws_sigv4 (already in the repo for inbound auth). SigV4 signing for outbound is the inverse direction but uses the same library primitives.
Phase 3 — Wizard "AWS Bedrock" first-class option (polish)
After Phase 2 lands, add an explicit "AWS Bedrock" choice in the forge init provider picker. The wizard then asks region + format (anthropic / openai) + model ID and writes the resolved config. Operator never has to know the URL template.
Out of scope
- Native Bedrock InvokeModel API (non-passthrough): the request/response shape differs from both OpenAI and Anthropic. If we add it later it's a third format alongside
openai and anthropic. The motivating use case here is passthrough (Anthropic shape on Anthropic-hosted models, OpenAI shape on the Bedrock OpenAI compatibility endpoint).
- Bedrock guardrails wiring — Bedrock has its own guardrails system, distinct from Forge's. If we want Forge to apply Bedrock guardrail IDs on outbound calls, file a separate issue.
- Per-region failover / multi-region — operator-side concern; Bedrock client config carries one region per agent.
- Cross-account role assumption — handled via the AWS credentials chain in the operator's environment (
AWS_PROFILE, role-arn env, IRSA on K8s), not Forge config.
Files (rough)
Phase 1 (small)
forge-cli/internal/tui/steps/provider_step.go — "Custom" path asks shape (OpenAI / Anthropic); writes provider: openai|anthropic.
forge-cli/runtime/runner.go — confirm ANTHROPIC_BASE_URL env safety net is symmetric with OPENAI_BASE_URL (current LLMProviderEnvDomains lists it; verify the runtime client construction path picks it up).
docs/reference/forge-yaml-schema.md + docs/getting-started/* + .claude/skills/forge.md — document the Anthropic-format custom URL pattern explicitly.
- Tests: wizard test exercising the Anthropic-shape custom path; runtime test confirming
ANTHROPIC_BASE_URL=... reaches AnthropicClient.baseURL.
Phase 2 (Bedrock SigV4)
forge-core/llm/providers/sigv4_transport.go (new) — http.RoundTripper that signs outbound requests using AWS SigV4. Hosted as a transport wrapper so it composes with the existing EgressTransport + otelhttp stack.
forge-core/llm/client.go — ClientConfig.AuthScheme field ("" | "x_api_key" | "bearer" | "aws_sigv4") + AWSRegion field.
forge-core/llm/providers/anthropic.go — when AuthScheme == "aws_sigv4", replace the x-api-key header logic with the SigV4 transport wrap.
forge-core/llm/providers/openai.go — symmetric change so Bedrock OpenAI compatibility works too.
forge-core/types/config.go — ModelRef gains AuthScheme + AWSRegion.
forge-cli/runtime/runner.go — populate the new fields from cfg.Model into ClientConfig.
- Egress allowlist auto-extension to cover
*.bedrock-runtime.amazonaws.com patterns (already covered by LLMProviderDomains + base_url parsing; verify).
docs/security/authentication.md — note that SigV4-for-outbound shares the inbound provider's keychain conventions.
Phase 3 (Bedrock wizard)
forge-cli/internal/tui/steps/provider_step.go — new top-level "AWS Bedrock" choice with region + format + model-id sub-phases.
Verification
Phase 1
forge init with custom URL + Anthropic shape → generated forge.yaml carries provider: anthropic + model.base_url.
ANTHROPIC_BASE_URL=https://my-anthropic-proxy.internal forge run → AnthropicClient sends requests to my-anthropic-proxy.internal.
- Run an agent against Anthropic's quickstart compatible API endpoint via an internal proxy → response round-trips.
Phase 2 (Bedrock-specific)
ANTHROPIC_API_KEY unset; AWS_REGION=us-east-1 + AWS_PROFILE=foo set; forge.yaml carries provider: anthropic + auth_scheme: aws_sigv4 + base_url: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-sonnet-4-20250514-v1%3A0/invoke → Forge sends a SigV4-signed Anthropic-shape request that Bedrock accepts.
- Same flow under
provider: openai + auth_scheme: aws_sigv4 pointed at Bedrock's OpenAI compatibility endpoint.
- Prompt caching headers (
anthropic-beta: prompt-caching-2024-07-31) survive the SigV4 sign step.
Phase 3
forge init → pick "AWS Bedrock" → choose Anthropic format → enter region + model id → generated forge.yaml is valid + forge run works against real Bedrock.
Related
Context
Forge today supports operator-set custom LLM endpoints via two well-known patterns:
provider: openai+model.base_urlOROPENAI_BASE_URLAuthorization: Bearer <OPENAI_API_KEY>provider: anthropiconly (api.anthropic.com hardcoded as default)x-api-key: <ANTHROPIC_API_KEY>The
forge initwizard's "Custom" option only setsprovider: openai. The docs (forge-yaml-schema.md,forge.mdskill) similarly guide operators toprovider: openai + OPENAI_BASE_URLas the only "custom endpoint" path. There is no symmetric "Anthropic-format custom URL" surface — even though the underlyingAnthropicClientalready acceptscfg.BaseURL(forge-core/llm/providers/anthropic.go:27).This issue closes the asymmetry plus adds the most-asked-for concrete target: AWS Bedrock with Anthropic-format requests.
Why "use OpenAI-compatible at Bedrock" isn't the answer
Bedrock now supports BOTH the OpenAI Chat Completions shape AND the native Anthropic Messages shape. Operators reach for the Anthropic shape when they want full access to Anthropic-specific features that don't round-trip cleanly through OpenAI's schema:
cache_controlblocks)thinkingblocks)imagecontent blocksFor an agent already targeting Claude models, forcing the OpenAI translation layer is a lossy detour. The user wants
provider: anthropic+ a custom base URL.Scope (proposed three-phase rollout)
Phase 1 — Documentation + wizard parity (small; no new code paths in the request shape)
Make
provider: anthropic+model.base_url(and theANTHROPIC_BASE_URLenv safety net) a first-class documented pattern. The plumbing is already there; what's missing is discoverability.docs/reference/forge-yaml-schema.mdto spell out the Anthropic-format custom URL pattern alongside the OpenAI one — current text only mentions OpenAI-compatible endpoints..claude/skills/forge.mdLLM provider section.forge initwizard's "Custom" phase: after the operator enters a Base URL, ask "OpenAI shape or Anthropic shape?" — default OpenAI for back-compat. The wizard writesprovider: openaiorprovider: anthropicaccordingly into the generatedforge.yaml.ANTHROPIC_BASE_URLin the env-derived BaseURL fallback path (same wayOPENAI_BASE_URLis honored today viarunner.go:482LLMProviderEnvDomains).Use case Phase 1 unlocks: any Anthropic-compatible endpoint that uses the standard
x-api-keyheader — internal proxies, litellm forwarders, locally-running Anthropic-shape gateways, third-party Anthropic-Messages-API-compatible vendors.Phase 2 — AWS Bedrock with Anthropic-format (the real motivating ask; needs SigV4)
Bedrock URLs look like
https://bedrock-runtime.<region>.amazonaws.com/model/<bedrock-model-id>/invokeand authenticate via AWS Signature Version 4, notx-api-key. Even withmodel.base_urlpointed at Bedrock, today'sAnthropicClientwould 403 because it stampsx-api-keyon every request.Two design options to evaluate:
Option A: New
bedrockprovider that wraps the request shapePros: clean separation, room for Bedrock-specific knobs (guardrails ID, trace, throughput type), maps to how Bedrock is conceptually a distinct provider.
Cons: a new provider type to maintain. Duplicates wire shape logic with the existing
openai/anthropicclients.Option B: Generic
auth_schemefield on the existing providersPros: reuses the AnthropicClient's wire-shape code; symmetric
auth_schemeknob would also unlock OpenAI-format on Bedrock (viaprovider: openai + auth_scheme: aws_sigv4).Cons: stretches the
providersemantics — operator picks a wire shape and an auth scheme orthogonally, which is the cleaner model long-term but more cognitive surface.Recommended: Option B, because the symmetric
auth_schemeknob also helps OAuth-on-bedrock, mTLS-fronted internal proxies, IAP-fronted endpoints, etc. without needing one provider type per new auth shape. But this is a design call worth landing in the issue before code.Either way, the
aws_sigv4signing implementation reusesforge-core/auth/providers/aws_sigv4(already in the repo for inbound auth). SigV4 signing for outbound is the inverse direction but uses the same library primitives.Phase 3 — Wizard "AWS Bedrock" first-class option (polish)
After Phase 2 lands, add an explicit "AWS Bedrock" choice in the
forge initprovider picker. The wizard then asks region + format (anthropic / openai) + model ID and writes the resolved config. Operator never has to know the URL template.Out of scope
openaiandanthropic. The motivating use case here is passthrough (Anthropic shape on Anthropic-hosted models, OpenAI shape on the Bedrock OpenAI compatibility endpoint).AWS_PROFILE, role-arn env, IRSA on K8s), not Forge config.Files (rough)
Phase 1 (small)
forge-cli/internal/tui/steps/provider_step.go— "Custom" path asks shape (OpenAI / Anthropic); writesprovider: openai|anthropic.forge-cli/runtime/runner.go— confirmANTHROPIC_BASE_URLenv safety net is symmetric withOPENAI_BASE_URL(currentLLMProviderEnvDomainslists it; verify the runtime client construction path picks it up).docs/reference/forge-yaml-schema.md+docs/getting-started/*+.claude/skills/forge.md— document the Anthropic-format custom URL pattern explicitly.ANTHROPIC_BASE_URL=...reachesAnthropicClient.baseURL.Phase 2 (Bedrock SigV4)
forge-core/llm/providers/sigv4_transport.go(new) —http.RoundTripperthat signs outbound requests using AWS SigV4. Hosted as a transport wrapper so it composes with the existingEgressTransport+otelhttpstack.forge-core/llm/client.go—ClientConfig.AuthSchemefield ("" | "x_api_key" | "bearer" | "aws_sigv4") +AWSRegionfield.forge-core/llm/providers/anthropic.go— whenAuthScheme == "aws_sigv4", replace thex-api-keyheader logic with the SigV4 transport wrap.forge-core/llm/providers/openai.go— symmetric change so Bedrock OpenAI compatibility works too.forge-core/types/config.go—ModelRefgainsAuthScheme+AWSRegion.forge-cli/runtime/runner.go— populate the new fields fromcfg.ModelintoClientConfig.*.bedrock-runtime.amazonaws.compatterns (already covered byLLMProviderDomains+ base_url parsing; verify).docs/security/authentication.md— note that SigV4-for-outbound shares the inbound provider's keychain conventions.Phase 3 (Bedrock wizard)
forge-cli/internal/tui/steps/provider_step.go— new top-level "AWS Bedrock" choice with region + format + model-id sub-phases.Verification
Phase 1
forge initwith custom URL + Anthropic shape → generatedforge.yamlcarriesprovider: anthropic+model.base_url.ANTHROPIC_BASE_URL=https://my-anthropic-proxy.internal forge run→AnthropicClientsends requests tomy-anthropic-proxy.internal.Phase 2 (Bedrock-specific)
ANTHROPIC_API_KEYunset;AWS_REGION=us-east-1+AWS_PROFILE=fooset;forge.yamlcarriesprovider: anthropic + auth_scheme: aws_sigv4 + base_url: https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-sonnet-4-20250514-v1%3A0/invoke→ Forge sends a SigV4-signed Anthropic-shape request that Bedrock accepts.provider: openai + auth_scheme: aws_sigv4pointed at Bedrock's OpenAI compatibility endpoint.anthropic-beta: prompt-caching-2024-07-31) survive the SigV4 sign step.Phase 3
forge init→ pick "AWS Bedrock" → choose Anthropic format → enter region + model id → generatedforge.yamlis valid +forge runworks against real Bedrock.Related
OPENAI_BASE_URL/ANTHROPIC_BASE_URL/OLLAMA_BASE_URL/GEMINI_BASE_URL) always passed through to skill subprocesses. The env-var convention is already established; this issue extends it to the LLM-client construction path symmetrically.forge runfalls through to the custom OpenAI-compatible endpoint whenOPENAI_BASE_URLis set; this issue adds the symmetric Anthropic path.