feat: Amazon Bedrock support via routing slug (#720)
* add Amazon Bedrock as a routing slug introduces a single `bedrock/byok` catalog entry that the harness translates to the appropriate Bedrock model ID at run time via `BEDROCK_MODEL_ID`. routes Anthropic IDs through claude-code (with `CLAUDE_CODE_USE_BEDROCK=1`) and everything else through opencode's `amazon-bedrock` provider — keeps the catalog flat for an audience that needs version pinning rather than aliasing. accepts either `AWS_BEARER_TOKEN_BEDROCK` or `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` for auth; both validated alongside `AWS_REGION` and `BEDROCK_MODEL_ID` in `validateAgentApiKey`. catalog drift tests, the bumps cron, and per-alias smoke scripts all skip routing slugs since there's no fixed `resolve` to validate. docs/bedrock.mdx walks through setup; wiki/model-resolution.md has a section explaining why bedrock breaks the usual alias pattern. closes pullfrog/pullfrog#40 * ci: add bedrock env vars to test workflows mirrors the new bedrock provider's required env vars (AWS_BEARER_TOKEN_BEDROCK inherited from org secret + AWS_REGION + BEDROCK_MODEL_ID hardcoded) into both .github/workflows/test.yml files so the ci.test "env vars cover all provider API keys" assertion passes. * docs(bedrock): clearer setup flow + screenshot of model selector restructures the setup section into three concrete steps in execution order: select Bedrock from the dropdown, store the bearer token as a secret (Pullfrog or GitHub — links to keys.mdx for the trade-off), then add region + model id directly in pullfrog.yml since neither is sensitive. enable-model-access in the Bedrock console moved to step 4 (only required once per model and only when AWS rejects the call, not blocking on first run). adds a screenshot of the console model selector with Amazon Bedrock selected so readers can recognize the UI state they're aiming for. * fix(bedrock): tolerate raw Bedrock model IDs in validateAgentApiKey main.ts passes the resolved model into validateAgentApiKey (`payload.proxyModel ?? resolvedModel ?? payload.model`). For Bedrock, `resolveModel` translates `bedrock/byok` into the raw AWS model ID (e.g. `us.anthropic.claude-opus-4-6-v1`), which has no `/` and so trips parseModel inside getModelEnvVars. Detect the no-slash case and re-run the bedrock setup check (auth + region; BEDROCK_MODEL_ID is already enforced upstream by resolveModel). Caught by PR #720 e2e dispatch on pullfrog/preview-720-bedrock — "invalid model slug 'us.anthropic.claude-opus-4-6-v1' — expected 'provider/model'". Two regression tests cover the raw-ID path. * fix(bedrock): always prepend amazon-bedrock/ prefix when bedrock-routed opencode.ts was gating the prefix-injection on `!isBedrockAnthropicId(rawModel)`, on the theory that Anthropic Bedrock IDs always go through claude-code. But `PULLFROG_AGENT=opencode` is a documented escape hatch — when it forces opencode for an Anthropic Bedrock model, the prefix still has to be added or opencode fails with 'Model not found: <modelId>/.'. The Anthropic-vs-other discriminant only belongs in resolveAgent. Once an agent is selected, it should consistently honor the bedrock route. Caught by the PULLFROG_AGENT=opencode + Opus 4.6 e2e on pullfrog/preview-720-bedrock — run 25823437606. * ui+docs(bedrock): bespoke setup callout + clearer docs UI: - BedrockSetupCallout in components/AgentSettings.tsx covers both the Model costs section and the onboarding card. Detects bedrock via resolveDisplayAlias().routing === "bedrock", shows a dedicated message ("store AWS_BEARER_TOKEN_BEDROCK as a secret, then put AWS_REGION + BEDROCK_MODEL_ID directly in pullfrog.yml") + link to the setup guide. Replaces the generic "X, Y, or Z is required" prompt that misrepresented the three values as three separate secrets to add (and used the wrong "or" connector for what's actually an AND). - OnboardingCard re-uses the same callout with the gradient-card variant. Docs: - Drop the obsolete "Enable model access" step. AWS retired the manual enrollment page; foundation models auto-enable on first invocation. Anthropic models still need a one-time use-case form for first-time users — surfaced under the AccessDenied troubleshooting entry. - Drop the "Testing a different model in one run" PULLFROG_MODEL note. It introduced the secrets-vs-vars distinction we want to keep out of the bedrock setup story. - Step 3 already recommends hardcoding region + model id in pullfrog.yml. Workflow template: - The default pullfrog.yml customers receive (utils/github/pullfrog.yml.ts) now references AWS_BEARER_TOKEN_BEDROCK from secrets but inlines AWS_REGION and BEDROCK_MODEL_ID as plain values. Matches the docs. * fix(bedrock): three review-caught edges in routing + UI copy Addresses three real issues from PR #720 review: 1. agent.ts: PULLFROG_MODEL=bedrock/byok no longer leaks the literal sentinel "bedrock" downstream. resolveCliModel returns the alias's resolve field verbatim, which for routing entries IS the sentinel. Refactored both the env-override and slug-lookup paths through a shared resolveSlug() that recognizes routing aliases and defers to their backing env var (BEDROCK_MODEL_ID). 2. models.ts: isBedrockAnthropicId() now anchors on a discrete dot/slash/colon-segment match (case-insensitive) instead of a substring contains. The substring check was fragile in both directions for inference-profile ARNs (BEDROCK_MODEL_ID accepts ARNs per AWS docs) — a non-Anthropic profile whose user-chosen name contained "anthropic" would mis-route to claude-code, and an Anthropic profile whose name omitted it would miss CLAUDE_CODE_USE_BEDROCK=1. 3. AgentSettings.tsx: BedrockSetupCallout's configured-state copy showed "AWS_BEARER_TOKEN_BEDROCK configured" even when the user satisfied the gate via AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, gaslighting access-key users about a secret they never set. Detect which auth method is actually present and name the right secret(s) in the success message. Regression tests in models.test.ts (5 new isBedrockAnthropicId cases including positive and negative ARN forms) and agent.test.ts (2 new PULLFROG_MODEL=bedrock/byok cases). 171/171 action tests pass. * yml template: add commented AWS access-key alternative for Bedrock auth Mirrors the IAM access-key path verified end-to-end on PR #720 e2e run 25830764987. Bearer token stays as the primary nudge; the access-key pair is the fallback for users who can't mint Bedrock API keys. * yml template: drop redundant 'or, alternatively' annotation * ui+docs(bedrock): rewrite callout copy + refresh screenshot Reframes the BedrockSetupCallout away from generic BYOK language to a Bedrock-specific message: leads with "Amazon Bedrock is configured entirely via environment variables", lists all four (auth, region, model id), and ends with the requested CTA sentence ("click below to learn more about Bedrock support in Pullfrog"). Promotes the "Bedrock setup guide" docs link from an inline anchor to a prominent button (always visible, regardless of auth state). The "Add AWS_BEARER_TOKEN_BEDROCK" affordance is now a secondary chip shown only when no auth secret is configured. Refreshes docs/images/model-selector-bedrock.png to capture the new callout — the prior screenshot still showed the old generic "BYOK / X, Y, or Z required" wording.
This commit is contained in:
committed by
pullfrog[bot]
parent
1a9d3c1f82
commit
8f9208bd3f
@@ -7,6 +7,22 @@
|
||||
|
||||
// ── types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* routing discriminant for entries whose `resolve` is dynamic — looked up
|
||||
* from a separate env var at run time rather than fixed in the catalog.
|
||||
*
|
||||
* `"bedrock"` means the actual model ID comes from `BEDROCK_MODEL_ID`
|
||||
* (an AWS-canonical Bedrock model ID like `us.anthropic.claude-opus-4-7`
|
||||
* or `amazon.nova-pro-v1:0`). enterprise Bedrock customers self-select for
|
||||
* version control — silent alias bumps would break compliance review,
|
||||
* model-access enrollment, and provisioned-throughput contracts. so the
|
||||
* single `bedrock/byok` entry is a routing slug, not a model alias: the
|
||||
* harness reads `BEDROCK_MODEL_ID` and routes to claude-code (when the ID
|
||||
* contains "anthropic") or opencode (everything else, with an
|
||||
* `amazon-bedrock/` prefix).
|
||||
*/
|
||||
export type ModelRouting = "bedrock";
|
||||
|
||||
export interface ModelAlias {
|
||||
/** stable alias stored in DB, e.g. "anthropic/claude-opus" */
|
||||
slug: string;
|
||||
@@ -14,9 +30,9 @@ export interface ModelAlias {
|
||||
provider: string;
|
||||
/** human-readable name shown in dropdowns */
|
||||
displayName: string;
|
||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6". sentinel for routing entries — never passed to a CLI directly. */
|
||||
resolve: string;
|
||||
/** full models.dev specifier for the OpenRouter equivalent (undefined for free models) */
|
||||
/** full models.dev specifier for the OpenRouter equivalent (undefined for free models and routing entries) */
|
||||
openRouterResolve: string | undefined;
|
||||
/** top-tier pick for this provider — preferred during auto-select */
|
||||
preferred: boolean;
|
||||
@@ -24,6 +40,8 @@ export interface ModelAlias {
|
||||
isFree: boolean;
|
||||
/** slug of a replacement model — presence implies this model is deprecated */
|
||||
fallback: string | undefined;
|
||||
/** dynamic-resolution discriminant — see ModelRouting docs */
|
||||
routing: ModelRouting | undefined;
|
||||
/** alias key (within same provider) of the cheaper sibling reviewfrog should
|
||||
* use as its lens-fanout subagent. e.g. claude-opus → "claude-sonnet". */
|
||||
subagentModel: string | undefined;
|
||||
@@ -44,6 +62,8 @@ interface ModelDef {
|
||||
isFree?: boolean;
|
||||
/** slug of a replacement model — presence implies this model is deprecated */
|
||||
fallback?: string;
|
||||
/** dynamic-resolution discriminant — see ModelRouting docs */
|
||||
routing?: ModelRouting;
|
||||
/** alias key (within same provider) of the cheaper sibling reviewfrog should
|
||||
* use as its lens-fanout subagent (e.g. claude-opus → "claude-sonnet"). */
|
||||
subagentModel?: string;
|
||||
@@ -324,6 +344,20 @@ export const providers = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
bedrock: provider({
|
||||
displayName: "Amazon Bedrock",
|
||||
envVars: ["AWS_BEARER_TOKEN_BEDROCK", "AWS_REGION", "BEDROCK_MODEL_ID"],
|
||||
models: {
|
||||
// single routing entry — the actual Bedrock model ID is read from
|
||||
// BEDROCK_MODEL_ID at run time. see ModelRouting docs for why we
|
||||
// don't catalog individual Bedrock models.
|
||||
byok: {
|
||||
displayName: "Amazon Bedrock",
|
||||
resolve: "bedrock",
|
||||
routing: "bedrock",
|
||||
},
|
||||
},
|
||||
}),
|
||||
openrouter: provider({
|
||||
displayName: "OpenRouter",
|
||||
envVars: ["OPENROUTER_API_KEY"],
|
||||
@@ -479,6 +513,7 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
|
||||
preferred: def.preferred ?? false,
|
||||
isFree: def.isFree ?? false,
|
||||
fallback: def.fallback,
|
||||
routing: def.routing,
|
||||
// subagentModel is stored as an alias key local to the provider; expand
|
||||
// here to a fully-qualified slug so callers can look up the target alias
|
||||
// directly without re-deriving the provider.
|
||||
@@ -537,3 +572,40 @@ export function resolveCliModel(slug: string): string | undefined {
|
||||
export function resolveOpenRouterModel(slug: string): string | undefined {
|
||||
return resolveDisplayAlias(slug)?.openRouterResolve;
|
||||
}
|
||||
|
||||
// ── bedrock routing ────────────────────────────────────────────────────────────
|
||||
|
||||
/** env var that supplies the Bedrock model ID for the `bedrock/byok` slug. */
|
||||
export const BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
||||
|
||||
/**
|
||||
* the Bedrock model ID passed to claude-code or opencode is whatever the
|
||||
* user set in `BEDROCK_MODEL_ID` — Pullfrog never resolves or upgrades it.
|
||||
* we route by checking whether the ID names an Anthropic model: claude-code
|
||||
* handles Anthropic-on-Bedrock natively (with `CLAUDE_CODE_USE_BEDROCK=1`),
|
||||
* everything else goes through opencode's `amazon-bedrock` provider.
|
||||
*
|
||||
* AWS Bedrock IDs come in two shapes:
|
||||
* - dotted foundation IDs: `us.anthropic.claude-opus-4-7`,
|
||||
* `anthropic.claude-haiku-4-5-20251001-v1:0`, `amazon.nova-pro-v1:0`,
|
||||
* `meta.llama4-scout-17b-instruct-v1:0`. AWS-published, lowercase, the
|
||||
* foundation provider always appears as a discrete dot-segment.
|
||||
* - inference-profile ARNs: `arn:aws:bedrock:us-east-2:<acct>:application-inference-profile/<user-name>`.
|
||||
* `<user-name>` is operator-chosen, so a naive substring check is fragile
|
||||
* in both directions (Anthropic profile named without "anthropic" → routes
|
||||
* to opencode and misses CLAUDE_CODE_USE_BEDROCK; non-Anthropic profile
|
||||
* whose name happens to contain "anthropic" → routes to claude-code).
|
||||
*
|
||||
* we anchor on a discrete dot-segment match (case-insensitive). this catches
|
||||
* every published foundation ID and is conservative for ARN-form IDs: ARN
|
||||
* names that don't include "anthropic" as their own dot-segment route to
|
||||
* opencode by default. operators using ARN-form IDs whose backing model is
|
||||
* Anthropic should set `PULLFROG_AGENT=claude` to force the right route, or
|
||||
* include the foundation segment in the profile name.
|
||||
*/
|
||||
export function isBedrockAnthropicId(bedrockModelId: string): boolean {
|
||||
// split on `.`, `/`, and `:` so the check works for both dotted foundation
|
||||
// IDs (anthropic.* / us.anthropic.*) and ARN-form IDs (where the relevant
|
||||
// foundation segment sits between `/` and `.` inside the resource name).
|
||||
return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user