Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
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
33 changes: 33 additions & 0 deletions coderd/ai_providers_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,14 @@ func providersFromEnv(ctx context.Context, cfg codersdk.AIBridgeConfig, logger s
Type: database.AIProviderTypeAnthropic,
}
if hasLegacyBedrock {
// The env vars cannot express a protocol, so a seeded Bedrock
// provider always uses InvokeModel, which substitutes the
// configured models into every upstream request. Both options
// carry defaults, so this only fires when an operator sets one
// to the empty string.
if err := validateSeededBedrockModels(bedrock); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-5] The check turns a partially-broken deployment into a deployment that will not boot. (Netero)

SeedAIProvidersFromEnv failure is fatal in both entry points (cli/server.go:1168, enterprise/cli/server.go:186). An operator running today with an indexed Bedrock provider and no BEDROCK_MODEL has one dead provider and a working coderd; after this change coderd refuses to start. That matches the existing fail-fast behavior of this file (drift already aborts startup), so I am not filing it as a finding, but the blast radius is worth stating in the PR body.

Agreed on both counts: consistent with the file's existing behavior, and worth stating in the PR body so whoever rolls this out knows the failure mode changed from one degraded provider to a refused startup. Note only, no change requested.

🤖

return nil, xerrors.Errorf("legacy bedrock provider: %w, set CODER_AI_GATEWAY_BEDROCK_MODEL and CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL", err)
}
dp.Type = database.AIProviderTypeBedrock
if hasAnthropicKey {
logger.Warn(ctx, "ignoring legacy Anthropic API key because Bedrock credentials are configured; Bedrock authenticates via access keys or credential chain",
Expand Down Expand Up @@ -407,6 +415,12 @@ func providersFromEnv(ctx context.Context, cfg codersdk.AIBridgeConfig, logger s
)
isBedrock = codersdk.IsBedrockConfigured(p.BedrockBaseURL, bedrock)
if isBedrock {
// Unlike the legacy CODER_AI_GATEWAY_BEDROCK_* options, the
// indexed ones carry no defaults, so a provider migrated from
// legacy to indexed env vars loses its models silently.
if err := validateSeededBedrockModels(bedrock); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-2] The same dead-row-and-404 failure still ships for an indexed Bedrock provider configured with BEDROCK_BASE_URL only; models are validated, credentials-or-region are not. (Netero)

IsBedrockConfigured(baseURL, b) is true on base URL alone, so CODER_AI_GATEWAY_PROVIDER_0_TYPE=bedrock plus BEDROCK_BASE_URL, BEDROCK_MODEL, BEDROCK_SMALL_FAST_MODEL (region and credentials from the AWS environment, the documented VPC/FIPS case in IsBedrockConfigured's own doc comment) passes ReadAIProvidersFromEnv, passes the new model check, and is inserted. At runtime bedrockConfig gates on AIProviderBedrockSettings.IsConfigured(), which ignores Model/SmallFastModel, returns nil, and buildProvider refuses the spec.

Verified with a throwaway internal test in cli: buildProvider on that exact spec returns bedrock provider has no bedrock credentials configured while codersdk.IsBedrockConfigured reports true for the same input.

Confirmed by reading both predicates: IsBedrockConfigured returns baseURL != "" || b.IsConfigured(), while bedrockConfig consults only IsConfigured(), whose doc comment lists region, role ARN, and access keys. The seed and the runtime disagree on what a buildable Bedrock provider is, and this PR narrows that disagreement by one field instead of closing it. Gating the seed on what the runtime actually requires (bedrockConfig(baseURL, settings) != nil plus config.AWSBedrock.Validate()) closes both this and the missing-model case with one check.

🤖

return nil, xerrors.Errorf("indexed AI provider %q: %w, set BEDROCK_MODEL and BEDROCK_SMALL_FAST_MODEL on it", name, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [CRF-1] The remediation this error prints does not repair a Bedrock row that was already seeded without models, so startup goes green while the provider keeps returning 404. (Netero)

The drift hash deliberately excludes Model and SmallFastModel (canonicalAIProvider, lines 220-227). In the case found: branch the seed compares hashes and continues on equality (lines 143-145); it never updates Settings.

  1. Restart after this PR: startup fails with "set BEDROCK_MODEL and BEDROCK_SMALL_FAST_MODEL on it".
  2. Operator sets both env vars.
  3. Startup succeeds, the stored row is untouched with empty models, buildProvider still refuses it (cli/aibridged.go, bedrock branch), and every request to the provider still 404s.

Verified: I inserted a Bedrock row with {"region":"us-east-1"} settings, ran SeedAIProvidersFromEnv with both models set in the indexed config, and asserted the stored settings afterwards. SeedAIProvidersFromEnv returned nil and stored.Bedrock.Model / stored.Bedrock.SmallFastModel were both still empty.

I verified both halves of the mechanism independently: canonicalAIProvider hashes only type, base URL, region, and keys hash, and the found branch returns early on hash equality without touching Settings.

This is the population the linked issue describes: indexed env vars have no model defaults, so any existing indexed Bedrock provider configured without BEDROCK_MODEL already has the broken row. For them the new error is a detour, not a fix, and the message actively misleads by naming env vars that cannot repair the row. Either include the models in the canonical hash so the stale row surfaces as drift, or detect the models-empty existing row and say the row must be fixed through the API.

🤖

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-4] The indexed error names BEDROCK_MODEL, which is not an environment variable an operator can set. (Netero)

The real variables are CODER_AI_GATEWAY_PROVIDER_<N>_BEDROCK_MODEL and CODER_AI_GATEWAY_PROVIDER_<N>_BEDROCK_SMALL_FAST_MODEL (cli/server.go:3170, key switch at 3383). "on it" refers to a provider identified by name, but the env vars are indexed by number, so the operator has to map name back to index themselves. The sibling validator in ReadAIProvidersFromEnv already solves both problems: it reports provider %d (%s): ... with the index and it knows the active prefix (aiGatewayProviderEnvPrefix vs aiBridgeProviderEnvPrefix).

Confirmed: BEDROCK_MODEL is a key suffix reached only under the CODER_AI_GATEWAY_PROVIDER_<N>_ or CODER_AIBRIDGE_PROVIDER_<N>_ prefix, and the prefix in use is deployment-dependent. An operator following this message literally sets a variable that does nothing. Naming the index and the active prefix, or moving the check next to the existing BEDROCK_* consistency checks in ReadAIProvidersFromEnv, gives the correct name for free.

🤖

}
dp.Bedrock = &bedrock
// Always overwrite the generic BaseURL so removing
// BASE_URL later doesn't trigger drift. Empty is fine:
Expand Down Expand Up @@ -459,3 +473,22 @@ func providersFromEnv(ctx context.Context, cfg codersdk.AIBridgeConfig, logger s
}
return res, nil
}

// validateSeededBedrockModels rejects an env-seeded Bedrock provider
// that omits the model identifiers. Neither env path can express a
// protocol, so a seeded provider always uses InvokeModel, which
// replaces the client's model with the configured one on every
// request. Without them the provider fails to build and is skipped,
// leaving every request to it to return a bare 404, so failing here
// keeps the dead row out of the database.
func validateSeededBedrockModels(b codersdk.AIProviderBedrockSettings) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-3] validateSeededBedrockModels duplicates codersdk.validateAIProviderBedrockModels, added one commit earlier by the PR this follows up. (Netero)

codersdk/aiproviders.go:402 already encodes "invoke-model requires Model and SmallFastModel", is called from both CreateAIProviderRequest.Validate and UpdateAIProviderRequest.Validate, and carries the same rationale in its doc comment ("would be skipped at gateway startup and every request to it would 404"). git log -S confirms it landed in 1142706 (#27846), the immediate predecessor commit.

The copy is protocol-unaware: it requires both models unconditionally, where the original skips the check for non-invoke-model protocols.

Verified: the codersdk validator returns early unless ResolvedProtocol() == invoke-model, and the new copy has no protocol branch. That is harmless today only because the indexed env key switch has no BEDROCK_PROTOCOL case, and nothing in either file records that dependency. The day a protocol env key is added, seeding rejects valid mantle providers. Export the codersdk validator (or a thin wrapper over it) and call it from both paths so the requirement and its protocol condition live in one place.

🤖

switch {
case b.Model == "" && b.SmallFastModel == "":
return xerrors.New("bedrock model and small fast model are required")
case b.Model == "":
return xerrors.New("bedrock model is required")
case b.SmallFastModel == "":
return xerrors.New("bedrock small fast model is required")
}
return nil
}
61 changes: 59 additions & 2 deletions coderd/ai_providers_migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
AccessKey: serpent.String("AKIA-original"),
AccessKeySecret: serpent.String("secret-original"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
SmallFastModel: serpent.String("anthropic.claude-3-5-haiku"),
},
}
require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)))
Expand Down Expand Up @@ -213,8 +214,9 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
// via the AWS environment (instance profile, AWS_PROFILE, etc.).
cfg := codersdk.AIBridgeConfig{
LegacyBedrock: codersdk.AIBridgeBedrockConfig{
Region: serpent.String("us-east-1"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
Region: serpent.String("us-east-1"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
SmallFastModel: serpent.String("anthropic.claude-3-5-haiku"),
},
}
require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)))
Expand All @@ -229,6 +231,58 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
require.Empty(t, keys, "Bedrock provider must not seed bearer keys")
})

t.Run("BedrockModelsRequired", func(t *testing.T) {
t.Parallel()

// A seeded Bedrock provider always uses InvokeModel, which replaces
// the client's model with the configured one. Seeding it without
// models writes a row that cannot be built, so every request to the
// provider would return a bare 404. Fail before touching the database.
t.Run("Legacy", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)

// Both legacy options carry defaults, so this state is only
// reachable by setting one to the empty string.
cfg := codersdk.AIBridgeConfig{
LegacyBedrock: codersdk.AIBridgeBedrockConfig{
Region: serpent.String("us-east-1"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
},
}
err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))
require.ErrorContains(t, err, "small fast model is required")
require.ErrorContains(t, err, "CODER_AI_GATEWAY_BEDROCK_SMALL_FAST_MODEL")

_, err = db.GetAIProviderByName(ctx, "anthropic")
require.Error(t, err, "no row may be written")
})

t.Run("Indexed", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)

// The indexed options carry no defaults, so this is the state an
// operator reaches by migrating off the legacy env vars.
cfg := codersdk.AIBridgeConfig{
Providers: []codersdk.AIProviderConfig{{
Type: "bedrock",
Name: "bedrock-indexed",
BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com/",
BedrockRegion: "us-east-1",
}},
}
err := coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t))
require.ErrorContains(t, err, `indexed AI provider "bedrock-indexed"`)
require.ErrorContains(t, err, "bedrock model and small fast model are required")

_, err = db.GetAIProviderByName(ctx, "bedrock-indexed")
require.Error(t, err, "no row may be written")
})
})

t.Run("BedrockOnlyAnthropic", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
Expand All @@ -240,6 +294,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
AccessKey: serpent.String("AKIAONLY"),
AccessKeySecret: serpent.String("secretonly"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
SmallFastModel: serpent.String("anthropic.claude-3-5-haiku"),
},
}
require.NoError(t, coderd.SeedAIProvidersFromEnv(ctx, db, cfg, testLogger(t)))
Expand Down Expand Up @@ -363,6 +418,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com/",
BedrockRegion: "us-east-1",
BedrockModel: "anthropic.claude-3-5-sonnet",
BedrockSmallFastModel: "anthropic.claude-3-5-haiku",
BedrockAccessKeys: []string{"AKIA-indexed"},
BedrockAccessKeySecrets: []string{"indexed-secret"},
},
Expand Down Expand Up @@ -615,6 +671,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
AccessKey: serpent.String("AKIA"),
AccessKeySecret: serpent.String("secret"),
Model: serpent.String("anthropic.claude-3-5-sonnet"),
SmallFastModel: serpent.String("anthropic.claude-3-5-haiku"),
},
}

Expand Down
Loading