From b675fd483db5d14d133ee1b0c1e2aaf05c5b057d Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 20:51:54 +0000 Subject: [PATCH 01/16] feat(runtime): support Anthropic, OpenAI, and Gemini model providers --- src/core/project/manager.tsx | 25 ++++++++-- src/core/project/templates/project.ts | 9 +++- src/core/project/templates/runtime.ts | 60 ++++++++++++++++++++--- src/core/project/templates/types.ts | 3 ++ src/handlers/project/add/runtime/index.ts | 10 ++-- src/handlers/project/types.ts | 49 ++++++++++++++++-- 6 files changed, 138 insertions(+), 18 deletions(-) diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index c502cbdae..d001a7e89 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -169,13 +169,18 @@ export class FsProjectManager implements ProjectManager { const destination = join(process.cwd(), input.name); yield { type: "step", message: "Creating project tree" }; - const projectTree = await createProjectTree( + const { tree: projectTree, envEntries } = await createProjectTree( { templateRenderer: this.templateRenderer, assetSource: this.assetSource }, { projectName: input.name }, { runtime: scaffoldRuntimeInput, importBedrockAgent: input.importBedrockAgent }, ); await projectTree.write(destination); + if (envEntries.length > 0) { + yield { type: "step", message: "Writing model provider API key to agentcore/.env.local" }; + await new EnvLocalFile(destination).insertIfNew(envEntries); + } + // A harness project scaffolds through the same addResource flow that // `project add harness` uses, so a create-time harness and an added one can // never drift apart. @@ -306,10 +311,24 @@ export class FsProjectManager implements ProjectManager { const outputPath = join(project.rootPath, "app", input.resourceConfig.name); scaffoldedPaths.push(outputPath); - const spec = await this.scaffoldRuntimeResources(outputPath, input.resourceConfig); + const { spec, envEntries } = await this.scaffoldRuntimeResources( + outputPath, + input.resourceConfig, + ); if (spec.runtimes) projectSpec.runtimes.push(...spec.runtimes); if (spec.memories) projectSpec.memories.push(...spec.memories); if (spec.credentials) projectSpec.credentials.push(...spec.credentials); + if (envEntries.length > 0) { + envFile = new EnvLocalFile(project.rootPath); + yield { type: "step", message: `Updating secrets file at '${envFile.path}'` }; + const { skipped } = await envFile.insertIfNew(envEntries); + for (const key of skipped) { + yield { + type: "step", + message: `'${key}' already exists in ${ENV_LOCAL_RELATIVE_PATH}; left unchanged`, + }; + } + } yield* this.installRuntimeDependencies(outputPath); break; @@ -834,7 +853,7 @@ export class FsProjectManager implements ProjectManager { const result = await resolver.resolve(input); await result.tree.write(dirname(outputPath)); - return result.spec; + return { spec: result.spec, envEntries: result.envEntries ?? [] }; } public async *build(project: Project): AsyncGenerator { diff --git a/src/core/project/templates/project.ts b/src/core/project/templates/project.ts index c2eeab8e4..b7ead7298 100644 --- a/src/core/project/templates/project.ts +++ b/src/core/project/templates/project.ts @@ -8,6 +8,7 @@ import type { import { InputValidationError } from "../../../errors/errors"; import { getRuntimeTemplateResolver } from "./runtime"; import type { SpecEntries, Template, TemplateRenderer } from "./types"; +import type { EnvLocalEntry } from "../../../handlers/project/types"; type CreateProjectConfig = { assetSource: AssetSource; @@ -18,7 +19,7 @@ export async function createProjectTree( config: CreateProjectConfig, input: { projectName: string }, options?: { runtime?: ScaffoldRuntimeInput; importBedrockAgent?: ImportBedrockAgentInput }, -): Promise { +): Promise<{ tree: FsTreeNode; envEntries: EnvLocalEntry[] }> { const templates: Template[] = []; if (options?.runtime) { const runtimeConfig: RuntimeResourceConfig = { @@ -34,7 +35,9 @@ export async function createProjectTree( templates.push(await resolver.resolve(runtimeConfig)); } - return FsTreeNode.createDirectory(".", [ + const envEntries = templates.flatMap((template) => template.envEntries ?? []); + + const tree = FsTreeNode.createDirectory(".", [ FsTreeNode.createFile(".gitignore", () => config.assetSource.read("templates/shared/gitignore.template"), ), @@ -58,6 +61,8 @@ export async function createProjectTree( templates.map((t) => t.tree), ), ]); + + return { tree, envEntries }; } const json = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`; diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 98c733ff8..74d55bc2d 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -2,11 +2,51 @@ import { FsTreeNode } from "./fsTree"; import type { AssetSource } from "../source"; import type { RuntimeResourceConfig } from "../../../handlers/project/add/runtime/types"; import type { ProjectRuntime } from "../../../projectSchemas/runtime"; -import type { TemplateRenderer, TemplateResolver } from "./types"; -import type { ScaffoldRuntimeInput } from "../../../handlers/project/types"; +import type { SpecEntries, TemplateRenderer, TemplateResolver } from "./types"; +import type { EnvLocalEntry, ScaffoldRuntimeInput } from "../../../handlers/project/types"; +import { credentialEnvVarName } from "../../../projectSchemas/credential"; import { InputValidationError } from "../../../errors"; import { toPythonPackageName } from "../fsUtils"; +/** + * The AgentCore Identity wiring a non-Bedrock model provider needs: a Handlebars + * context fragment naming the credential the generated `model/load` reads, the + * `ApiKeyCredentialProvider` credential to register in agentcore.json, and the + * API-key value to write to agentcore/.env.local. Bedrock uses the runtime's IAM + * credentials, so it contributes none of these. + */ +type ModelProviderIdentity = { + context: { hasIdentity: boolean; identityProviders: { name: string; envVarName: string }[] }; + credentials: NonNullable; + envEntries: EnvLocalEntry[]; +}; + +function resolveModelProviderIdentity(input: RuntimeResourceConfig): ModelProviderIdentity { + const { modelProvider, apiKey } = input.scaffoldRuntimeInput; + if (modelProvider === "Bedrock") { + return { + context: { hasIdentity: false, identityProviders: [] }, + credentials: [], + envEntries: [], + }; + } + const credentialName = `${input.name}${modelProvider}ApiKey`; + const envVarName = credentialEnvVarName(credentialName); + return { + context: { hasIdentity: true, identityProviders: [{ name: credentialName, envVarName }] }, + credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }], + envEntries: apiKey + ? [ + { + key: envVarName, + value: apiKey, + comment: `API key for the ${modelProvider} model provider (runtime ${input.name})`, + }, + ] + : [], + }; +} + function buildRuntimeSpec(input: RuntimeResourceConfig): ProjectRuntime { const { scaffoldRuntimeInput, name, ...infra } = input; return { @@ -121,6 +161,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa : [], ); const memory = input.scaffoldRuntimeInput.memory; + const identity = resolveModelProviderIdentity(input); const context = { name: toPythonPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -128,11 +169,10 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - hasIdentity: false, + ...identity.context, hasGateway: false, hasPayment: false, isVpc: input.networkMode === "VPC", - identityProviders: [], gatewayProviders: [], gatewayAuthTypes: [], sessionStorageMountPath, @@ -163,7 +203,9 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], ...(memory && { memories: [memory] }), + ...(identity.credentials.length > 0 && { credentials: identity.credentials }), }, + ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), }; }, [buildResolverKey("strands", "TypeScript", "HTTP")]: async (input: RuntimeResourceConfig) => { @@ -179,6 +221,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa "the agent-typescript-strands template does not support short-term-only memory; add long-term strategies or use --memory none", ); + const identity = resolveModelProviderIdentity(input); const context = { name: toNpmPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -186,8 +229,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - hasIdentity: false, - identityProviders: [], + ...identity.context, }; const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( @@ -208,7 +250,9 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], ...(memory && { memories: [memory] }), + ...(identity.credentials.length > 0 && { credentials: identity.credentials }), }, + ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), }; }, [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { @@ -274,6 +318,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa : [], ); const memory = input.scaffoldRuntimeInput.memory; + const identity = resolveModelProviderIdentity(input); const context = { name: toPythonPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -281,6 +326,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], + ...identity.context, sessionStorageMountPath, efsMounts, s3Mounts, @@ -310,7 +356,9 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "A2A" as const }], ...(memory && { memories: [memory] }), + ...(identity.credentials.length > 0 && { credentials: identity.credentials }), }, + ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), }; }, }); diff --git a/src/core/project/templates/types.ts b/src/core/project/templates/types.ts index 798e4ba4b..d7e386301 100644 --- a/src/core/project/templates/types.ts +++ b/src/core/project/templates/types.ts @@ -4,6 +4,7 @@ import type { MemorySchema } from "../../../projectSchemas/memory"; import type { CredentialSchema } from "../../../projectSchemas/credential"; import type { HarnessRegistryEntry } from "../../../projectSchemas/harness"; import type { Evaluator } from "../../../projectSchemas/evaluator"; +import type { EnvLocalEntry } from "../../../handlers/project/types"; import type z from "zod"; /** AgentCore Project Spec Entries that rendered as part of a {@link Template} **/ @@ -19,6 +20,8 @@ export type SpecEntries = { export type Template = { tree: FsTreeNode; spec: SpecEntries; + /** Secret material for agentcore/.env.local (e.g. a model provider API key). */ + envEntries?: EnvLocalEntry[]; }; /** A standard interface for resolving templates from a given input of paramters **/ diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 4f629e7c5..03c6fd2e1 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -14,7 +14,11 @@ import { RUNTIME_TEMPLATE_SHORTCUT_NAMES, resolveRuntimeTemplateShortcut, } from "../../shortcuts"; -import { ScaffoldRuntimeInputSchema, type ScaffoldRuntimeInput } from "../../types"; +import { + ModelProviderSchema, + ScaffoldRuntimeInputSchema, + type ScaffoldRuntimeInput, +} from "../../types"; import { RuntimeResourceConfigSchema, type ImportBedrockAgentInput } from "./types"; import { importScaffoldRuntimeInput, @@ -63,8 +67,8 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ), flag( "model-provider", - "model provider for the scaffolded runtime code", - z.enum(["Bedrock"]).optional(), + "model provider for the scaffolded runtime code (Bedrock, Anthropic, OpenAI, or Gemini)", + ModelProviderSchema.optional(), ), flag( "api-key", diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index dc2dd279d..f3c7d74ad 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -40,6 +40,28 @@ export type ManagedEvaluatorScaffoldInput = { timeoutSeconds?: number; }; +/** + * Model providers the scaffolded runtime code supports. `Bedrock` uses the + * runtime's IAM credentials; the others authenticate with an API key managed + * through AgentCore Identity. + */ +export const MODEL_PROVIDERS = ["Bedrock", "Anthropic", "OpenAI", "Gemini"] as const; +export type ModelProvider = (typeof MODEL_PROVIDERS)[number]; + +const MODEL_PROVIDER_ALIASES: Record = { + bedrock: "Bedrock", + anthropic: "Anthropic", + openai: "OpenAI", + gemini: "Gemini", +}; + +/** Parses a provider name case-insensitively (e.g. `anthropic`), normalizing to canonical casing. */ +export const ModelProviderSchema = z.preprocess( + (value) => + typeof value === "string" ? (MODEL_PROVIDER_ALIASES[value.toLowerCase()] ?? value) : value, + z.enum(MODEL_PROVIDERS), +); + /** Set of arguments needed to scaffold a new Runtime-based agent. */ export const ScaffoldRuntimeInputSchema = z .object({ @@ -48,14 +70,33 @@ export const ScaffoldRuntimeInputSchema = z language: z.enum(["Python", "TypeScript"]), framework: z.enum(["strands", "none"]), protocol: ProtocolModeSchema.optional(), - modelProvider: z.enum(["Bedrock"]), + modelProvider: ModelProviderSchema, apiKey: z.string().min(1).optional(), memory: MemorySchema.optional(), runtimeVersion: RuntimeVersionSchema.optional(), }) - .refine(({ modelProvider, apiKey }) => !(modelProvider === "Bedrock" && apiKey !== undefined), { - message: "API keys are not compatible with Bedrock model providers", - path: ["apiKey"], + .superRefine(({ modelProvider, apiKey, framework }, ctx) => { + if (modelProvider === "Bedrock" && apiKey !== undefined) { + ctx.addIssue({ + code: "custom", + message: "API keys are not compatible with Bedrock model providers", + path: ["apiKey"], + }); + } + if (modelProvider !== "Bedrock" && apiKey === undefined) { + ctx.addIssue({ + code: "custom", + message: `an API key is required for the ${modelProvider} model provider`, + path: ["apiKey"], + }); + } + if (modelProvider !== "Bedrock" && framework !== "strands") { + ctx.addIssue({ + code: "custom", + message: `the ${modelProvider} model provider requires the strands framework`, + path: ["modelProvider"], + }); + } }) .superRefine(({ build, runtimeVersion }, ctx) => { if (build === "CodeZip" && runtimeVersion === undefined) { From e9f8141c7475322a61832a9a4a2f44872fd3d11d Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 20:51:55 +0000 Subject: [PATCH 02/16] test(runtime): cover non-Bedrock model provider scaffolding --- .../project/add/runtime/index.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 17869ac44..ed0f02606 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -11,6 +11,7 @@ import { } from "../../../../testing"; import { InputValidationError } from "../../../../errors"; import type { BedrockAgentImportPlan } from "../../../../core/project/bedrockAgentImport"; +import { credentialEnvVarName } from "../../../../projectSchemas/credential"; const originalCwd = process.cwd(); const tempDirectories: string[] = []; @@ -781,6 +782,131 @@ describe("project add runtime", () => { ]), ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); }); + + test.each<[string, string, string, string]>([ + ["Anthropic", "Python", "strands.models.anthropic", "AnthropicModel"], + ["OpenAI", "Python", "strands.models.openai", "OpenAIModel"], + ["Gemini", "Python", "strands.models.gemini", "GeminiModel"], + ["Anthropic", "TypeScript", "@strands-agents/sdk/models/anthropic", "AnthropicModel"], + ])( + "scaffolds a strands %s runtime (%s) with an API-key credential", + async (provider, language, expectedImport, expectedModelClass) => { + const projectRoot = await inProject(); + const apiKeyPath = join(projectRoot, "api-key.txt"); + await Bun.write(apiKeyPath, "test-api-key"); + + await run([ + "add", + "runtime", + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + language, + "--framework", + "strands", + "--model-provider", + provider, + "--api-key", + `file://${apiKeyPath}`, + ]); + + const credentialName = `my_agent${provider}ApiKey`; + const envVarName = credentialEnvVarName(credentialName); + + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.credentials).toContainEqual({ + authorizerType: "ApiKeyCredentialProvider", + name: credentialName, + }); + + const envLocal = await Bun.file(join(projectRoot, "agentcore", ".env.local")).text(); + expect(envLocal).toContain(`${envVarName}='test-api-key'`); + + const loadFile = language === "TypeScript" ? "load.ts" : "load.py"; + const loadSource = await Bun.file( + join(projectRoot, "app", "my_agent", "model", loadFile), + ).text(); + expect(loadSource).toContain(expectedImport); + expect(loadSource).toContain(expectedModelClass); + expect(loadSource).toContain(credentialName); + expect(loadSource).toContain(envVarName); + }, + ); + + test("normalizes a lowercase --model-provider to canonical casing", async () => { + const projectRoot = await inProject(); + const apiKeyPath = join(projectRoot, "api-key.txt"); + await Bun.write(apiKeyPath, "test-api-key"); + + await run([ + "add", + "runtime", + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "strands", + "--model-provider", + "anthropic", + "--api-key", + `file://${apiKeyPath}`, + ]); + + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.credentials).toContainEqual({ + authorizerType: "ApiKeyCredentialProvider", + name: "my_agentAnthropicApiKey", + }); + }); + + test("rejects a non-Bedrock provider without an API key", async () => { + await inProject(); + await expect( + run([ + "add", + "runtime", + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "strands", + "--model-provider", + "Anthropic", + ]), + ).rejects.toThrow(/API key is required for the Anthropic model provider/); + }); + + test("rejects a non-Bedrock provider without the strands framework", async () => { + const projectRoot = await inProject(); + const apiKeyPath = join(projectRoot, "api-key.txt"); + await Bun.write(apiKeyPath, "test-api-key"); + await expect( + run([ + "add", + "runtime", + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "none", + "--model-provider", + "Anthropic", + "--api-key", + `file://${apiKeyPath}`, + ]), + ).rejects.toThrow(/requires the strands framework/); + }); }); describe("project add runtime --type import", () => { From 7b52f7c1647a5397fd024308e649e65938ac91e7 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 20:58:29 +0000 Subject: [PATCH 03/16] feat(create): support non-Bedrock model providers on the runtime path --- src/handlers/project/create/index.ts | 52 +++++++++++++++++++++++----- src/handlers/project/project.test.ts | 37 ++++++++++++++++++-- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 4277f34a1..bc7244e13 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -9,8 +9,10 @@ import { resolveRuntimeTemplateShortcut, } from "../shortcuts"; import { + ModelProviderSchema, ScaffoldRuntimeInputSchema, type CreateProjectInput, + type ModelProvider, type ProjectManager, type ScaffoldHarnessInput, type ScaffoldRuntimeInput, @@ -65,7 +67,11 @@ const HARNESS_ONLY_FLAGS = [ "container", ] as const; -const ModelProviderFlagSchema = z.union([z.literal("Bedrock"), HarnessModelProviderSchema]); +// Accepts both the harness provider names (bedrock, open_ai, gemini, lite_llm) +// and the runtime-code provider names (Bedrock, Anthropic, OpenAI, Gemini, +// case-insensitive). The runtime and harness paths each map an accepted value +// into their own domain, rejecting the ones they do not support. +const ModelProviderFlagSchema = z.union([HarnessModelProviderSchema, ModelProviderSchema]); type ModelProviderFlag = z.infer; const HARNESS_DEFAULT_MODEL_IDS: Record = { @@ -116,7 +122,8 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = ), flag( "model-provider", - "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; Bedrock for runtime code", + "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; " + + "Bedrock, Anthropic, OpenAI, or Gemini for runtime code", ModelProviderFlagSchema.optional(), ), flag( @@ -411,18 +418,47 @@ export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): Scaff return input; } +const HARNESS_MODEL_PROVIDERS: Record = { + Bedrock: "bedrock", + bedrock: "bedrock", + OpenAI: "open_ai", + open_ai: "open_ai", + Gemini: "gemini", + gemini: "gemini", + lite_llm: "lite_llm", +}; + function resolveHarnessModelProvider(value: ModelProviderFlag | undefined): HarnessModelProvider { - return value === undefined || value === "Bedrock" ? "bedrock" : value; + if (value === undefined) return "bedrock"; + const provider = HARNESS_MODEL_PROVIDERS[value]; + if (!provider) + throw new InputValidationError( + `the '${value}' model provider is not supported for harness projects`, + ); + return provider; } +const RUNTIME_MODEL_PROVIDERS: Record = { + Bedrock: "Bedrock", + bedrock: "Bedrock", + Anthropic: "Anthropic", + OpenAI: "OpenAI", + open_ai: "OpenAI", + Gemini: "Gemini", + gemini: "Gemini", +}; + function resolveRuntimeModelProvider( value: ModelProviderFlag | undefined, -): ScaffoldRuntimeInput["modelProvider"] | undefined { +): ModelProvider | undefined { if (value === undefined) return undefined; - if (value === "Bedrock" || value === "bedrock") return "Bedrock"; - throw new InputValidationError( - `runtime scaffolding only supports the Bedrock model provider; received '${value}'`, - ); + const provider = RUNTIME_MODEL_PROVIDERS[value]; + if (!provider) + throw new InputValidationError( + `runtime scaffolding does not support the '${value}' model provider ` + + `(expected Bedrock, Anthropic, OpenAI, or Gemini)`, + ); + return provider; } /** A --container value is either an ECR image URI or a local Dockerfile path. */ diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 200f55ce6..5ce1e101f 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -424,7 +424,7 @@ describe("project create", () => { expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true); }); - test("rejects non-Bedrock model providers on the runtime path", async () => { + test("rejects lite_llm on the runtime path", async () => { const directory = await inTempDirectory(); await expect( run([ @@ -434,12 +434,43 @@ describe("project create", () => { "--template", "agent-python-strands", "--model-provider", - "open_ai", + "lite_llm", + "--api-key", + "-", ]), - ).rejects.toThrow(/runtime scaffolding only supports the Bedrock model provider/); + ).rejects.toThrow(/runtime scaffolding does not support the 'lite_llm' model provider/); expect(existsSync(join(directory, "MyProject"))).toBe(false); }); + test("scaffolds a runtime with an OpenAI API-key credential", async () => { + const directory = await inTempDirectory(); + const apiKeyPath = join(directory, "api-key.txt"); + await Bun.write(apiKeyPath, "test-api-key"); + + await run([ + "create", + "--name", + "MyProject", + "--template", + "agent-python-strands", + "--model-provider", + "openai", + "--api-key", + `file://${apiKeyPath}`, + "--skip-install", + "--skip-git", + ]); + + const projectRoot = join(directory, "MyProject"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.credentials).toContainEqual({ + authorizerType: "ApiKeyCredentialProvider", + name: "agent_python_strandsOpenAIApiKey", + }); + const envLocal = await Bun.file(join(projectRoot, "agentcore", ".env.local")).text(); + expect(envLocal).toContain("test-api-key"); + }); + test("scaffolds a Container agent from the strands template", async () => { const directory = await inTempDirectory(); await run([ From 41fbb4b0fd16f83aca9bd0d0b06c7d2408748db4 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 21:06:39 +0000 Subject: [PATCH 04/16] fix(templates): pull provider SDKs via strands-agents extras --- .../templates/a2a-python-strands/pyproject.toml | 13 +++++++------ .../agent-python-strands/pyproject.toml | 17 +++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/assets/templates/a2a-python-strands/pyproject.toml b/src/assets/templates/a2a-python-strands/pyproject.toml index a7b9ac465..a38f44377 100644 --- a/src/assets/templates/a2a-python-strands/pyproject.toml +++ b/src/assets/templates/a2a-python-strands/pyproject.toml @@ -9,15 +9,16 @@ description = "AgentCore A2A Agent using Strands SDK" readme = "README.md" requires-python = ">=3.10" dependencies = [ - {{#if (eq modelProvider "Anthropic")}}"anthropic ~= 0.30.0", - {{/if}}"a2a-sdk[all] >= 0.3.0, < 0.4.0", + "a2a-sdk[all] >= 0.3.0, < 0.4.0", "aws-opentelemetry-distro ~= 0.17.0", "bedrock-agentcore[a2a] ~= 1.9.1", "botocore[crt] ~= 1.43.0", - {{#if (eq modelProvider "Gemini")}}"google-genai ~= 1.0.0", - {{/if}}{{#if (eq modelProvider "OpenAI")}}"openai ~= 1.0.0", - {{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm ~= 1.0.0", - {{/if}}"strands-agents ~= 1.15.0", + {{#if (eq modelProvider "Anthropic")}}"strands-agents[anthropic] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "OpenAI")}}"strands-agents[openai] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "Gemini")}}"strands-agents[gemini] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "LiteLLM")}}"strands-agents[litellm] ~= 1.15.0", + {{else}}"strands-agents ~= 1.15.0", + {{/if}}{{/if}}{{/if}}{{/if}} ] [tool.hatch.build.targets.wheel] diff --git a/src/assets/templates/agent-python-strands/pyproject.toml b/src/assets/templates/agent-python-strands/pyproject.toml index 0d3a70143..88eadd517 100644 --- a/src/assets/templates/agent-python-strands/pyproject.toml +++ b/src/assets/templates/agent-python-strands/pyproject.toml @@ -9,17 +9,18 @@ description = "AgentCore Runtime Application using Strands SDK" readme = "README.md" requires-python = ">=3.10" dependencies = [ - {{#if (eq modelProvider "Anthropic")}}"anthropic ~= 0.30.0", - {{/if}}"aws-opentelemetry-distro ~= 0.18.0", + "aws-opentelemetry-distro ~= 0.18.0", "bedrock-agentcore ~= 1.9.1", "botocore[crt] ~= 1.43.0", - {{#if (eq modelProvider "Gemini")}}"google-genai ~= 1.0.0", - {{/if}}"mcp ~= 1.24.0", - {{#if (eq modelProvider "OpenAI")}}"openai ~= 1.0.0", - {{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm ~= 1.0.0", - {{/if}}{{#if bedrockMantle}}"openai ~= 1.0.0", + "mcp ~= 1.24.0", + {{#if bedrockMantle}}"openai ~= 1.0.0", "aws-bedrock-token-generator ~= 1.0.0", - {{/if}}"strands-agents ~= 1.15.0", + {{/if}}{{#if (eq modelProvider "Anthropic")}}"strands-agents[anthropic] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "OpenAI")}}"strands-agents[openai] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "Gemini")}}"strands-agents[gemini] ~= 1.15.0", + {{else}}{{#if (eq modelProvider "LiteLLM")}}"strands-agents[litellm] ~= 1.15.0", + {{else}}"strands-agents ~= 1.15.0", + {{/if}}{{/if}}{{/if}}{{/if}} {{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools ~= 0.1.0", {{/if}}{{#if hasBrowser}}"nest-asyncio ~= 1.5.0", "playwright ~= 1.42.0", From c3646293748d0545ce6b160d7638f3e522ab50c0 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 22:12:21 +0000 Subject: [PATCH 05/16] refactor(project): resolve model providers via one function and gate templates at resolution --- src/core/project/templates/runtime.ts | 15 +++++ src/handlers/project/create/index.ts | 79 ++++++++++++--------------- src/handlers/project/types.ts | 10 +--- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 74d55bc2d..9b610e5cc 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -47,6 +47,19 @@ function resolveModelProviderIdentity(input: RuntimeResourceConfig): ModelProvid }; } +/** + * The plain (non-strands) templates ship no model abstraction, so they only run + * on Bedrock. A non-Bedrock provider is rejected here, when the template is + * resolved, rather than by the scaffold schema — the schema cannot know which + * template a given framework/language/protocol maps to. + */ +function assertBedrockOnly(input: RuntimeResourceConfig, template: string): void { + if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") + throw new InputValidationError( + `the ${template} template only supports the Bedrock model provider`, + ); +} + function buildRuntimeSpec(input: RuntimeResourceConfig): ProjectRuntime { const { scaffoldRuntimeInput, name, ...infra } = input; return { @@ -129,6 +142,7 @@ const importBedrockAgentResolver = () => async (input: RuntimeResourceConfig) => const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({ [buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => { + assertBedrockOnly(input, "agent-python"); if (input.scaffoldRuntimeInput.memory !== undefined) throw new InputValidationError(`memory is not supported with the agent-python template`); const isContainer = input.scaffoldRuntimeInput.build === "Container"; @@ -256,6 +270,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa }; }, [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { + assertBedrockOnly(input, "mcp-python-fastmcp"); if (input.scaffoldRuntimeInput.memory !== undefined) throw new InputValidationError("memory is not supported with an MCP runtime"); const filesystemConfigurations = input.filesystemConfigurations ?? []; diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index bc7244e13..ef23aee39 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -9,7 +9,6 @@ import { resolveRuntimeTemplateShortcut, } from "../shortcuts"; import { - ModelProviderSchema, ScaffoldRuntimeInputSchema, type CreateProjectInput, type ModelProvider, @@ -67,11 +66,7 @@ const HARNESS_ONLY_FLAGS = [ "container", ] as const; -// Accepts both the harness provider names (bedrock, open_ai, gemini, lite_llm) -// and the runtime-code provider names (Bedrock, Anthropic, OpenAI, Gemini, -// case-insensitive). The runtime and harness paths each map an accepted value -// into their own domain, rejecting the ones they do not support. -const ModelProviderFlagSchema = z.union([HarnessModelProviderSchema, ModelProviderSchema]); +const ModelProviderFlagSchema = z.enum([...HarnessModelProviderSchema.options, "anthropic"]); type ModelProviderFlag = z.infer; const HARNESS_DEFAULT_MODEL_IDS: Record = { @@ -123,7 +118,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = flag( "model-provider", "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; " + - "Bedrock, Anthropic, OpenAI, or Gemini for runtime code", + "bedrock, anthropic, open_ai, or gemini for runtime code", ModelProviderFlagSchema.optional(), ), flag( @@ -347,7 +342,7 @@ async function resolveScaffoldRuntimeInput( config: CreateProjectHandlerConfig, flags: RuntimePathFlagValues, ): Promise { - const modelProvider = resolveRuntimeModelProvider(flags["model-provider"]); + const modelProvider = resolveModelProvider(flags["model-provider"], "runtime"); const source = new SourceResolver({ stdin: config.io.stdin }); const apiKey = await source.resolveSecret("api-key", flags["api-key"]); @@ -383,7 +378,7 @@ async function resolveScaffoldRuntimeInput( // same addResource path. Exported so the TUI create wizard builds its harness // input through the exact same translation as the flag-driven path. export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { - const provider = resolveHarnessModelProvider(flags["model-provider"]); + const provider = resolveModelProvider(flags["model-provider"], "harness"); const additionalParams = parseJsonFlag>( "additional-params", flags["additional-params"], @@ -418,45 +413,41 @@ export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): Scaff return input; } -const HARNESS_MODEL_PROVIDERS: Record = { - Bedrock: "bedrock", - bedrock: "bedrock", - OpenAI: "open_ai", - open_ai: "open_ai", - Gemini: "gemini", - gemini: "gemini", - lite_llm: "lite_llm", +// Each accepted flag value maps into whichever domains support it: the harness +// path keeps the lowercase names its spec uses, the runtime path takes the +// title-cased names the templates render against. A value absent from a domain +// (lite_llm on runtime, anthropic on harness) is unsupported there. +const MODEL_PROVIDERS: Record< + ModelProviderFlag, + { harness?: HarnessModelProvider; runtime?: ModelProvider } +> = { + bedrock: { harness: "bedrock", runtime: "Bedrock" }, + open_ai: { harness: "open_ai", runtime: "OpenAI" }, + gemini: { harness: "gemini", runtime: "Gemini" }, + lite_llm: { harness: "lite_llm" }, + anthropic: { runtime: "Anthropic" }, }; -function resolveHarnessModelProvider(value: ModelProviderFlag | undefined): HarnessModelProvider { - if (value === undefined) return "bedrock"; - const provider = HARNESS_MODEL_PROVIDERS[value]; - if (!provider) - throw new InputValidationError( - `the '${value}' model provider is not supported for harness projects`, - ); - return provider; -} - -const RUNTIME_MODEL_PROVIDERS: Record = { - Bedrock: "Bedrock", - bedrock: "Bedrock", - Anthropic: "Anthropic", - OpenAI: "OpenAI", - open_ai: "OpenAI", - Gemini: "Gemini", - gemini: "Gemini", -}; - -function resolveRuntimeModelProvider( +function resolveModelProvider( + value: ModelProviderFlag | undefined, + domain: "harness", +): HarnessModelProvider; +function resolveModelProvider( + value: ModelProviderFlag | undefined, + domain: "runtime", +): ModelProvider | undefined; +function resolveModelProvider( value: ModelProviderFlag | undefined, -): ModelProvider | undefined { - if (value === undefined) return undefined; - const provider = RUNTIME_MODEL_PROVIDERS[value]; - if (!provider) + domain: "harness" | "runtime", +): HarnessModelProvider | ModelProvider | undefined { + if (value === undefined) return domain === "harness" ? "bedrock" : undefined; + const provider = MODEL_PROVIDERS[value][domain]; + if (provider === undefined) throw new InputValidationError( - `runtime scaffolding does not support the '${value}' model provider ` + - `(expected Bedrock, Anthropic, OpenAI, or Gemini)`, + domain === "harness" + ? `the '${value}' model provider is not supported for harness projects` + : `runtime scaffolding does not support the '${value}' model provider ` + + `(expected bedrock, anthropic, open_ai, or gemini)`, ); return provider; } diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index f3c7d74ad..bafd8c065 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -52,6 +52,7 @@ const MODEL_PROVIDER_ALIASES: Record = { bedrock: "Bedrock", anthropic: "Anthropic", openai: "OpenAI", + open_ai: "OpenAI", gemini: "Gemini", }; @@ -75,7 +76,7 @@ export const ScaffoldRuntimeInputSchema = z memory: MemorySchema.optional(), runtimeVersion: RuntimeVersionSchema.optional(), }) - .superRefine(({ modelProvider, apiKey, framework }, ctx) => { + .superRefine(({ modelProvider, apiKey }, ctx) => { if (modelProvider === "Bedrock" && apiKey !== undefined) { ctx.addIssue({ code: "custom", @@ -90,13 +91,6 @@ export const ScaffoldRuntimeInputSchema = z path: ["apiKey"], }); } - if (modelProvider !== "Bedrock" && framework !== "strands") { - ctx.addIssue({ - code: "custom", - message: `the ${modelProvider} model provider requires the strands framework`, - path: ["modelProvider"], - }); - } }) .superRefine(({ build, runtimeVersion }, ctx) => { if (build === "CodeZip" && runtimeVersion === undefined) { From be435a976625ea67296ca958472c0850163c036b Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 22:12:22 +0000 Subject: [PATCH 06/16] test(project): consolidate model-provider tests and decouple from templates --- .../project/add/runtime/index.test.ts | 104 ++++++------------ src/handlers/project/project.test.ts | 30 +++-- 2 files changed, 49 insertions(+), 85 deletions(-) diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index ed0f02606..4ee2fc9d2 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -783,14 +783,17 @@ describe("project add runtime", () => { ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); }); - test.each<[string, string, string, string]>([ - ["Anthropic", "Python", "strands.models.anthropic", "AnthropicModel"], - ["OpenAI", "Python", "strands.models.openai", "OpenAIModel"], - ["Gemini", "Python", "strands.models.gemini", "GeminiModel"], - ["Anthropic", "TypeScript", "@strands-agents/sdk/models/anthropic", "AnthropicModel"], + // The flag value casing varies deliberately (lowercase and canonical both + // normalize to the same credential name). Behaviour is asserted against the + // agentcore spec and .env.local, not the scaffolded template code. + test.each<[string, string, string]>([ + ["anthropic", "Anthropic", "Python"], + ["OpenAI", "OpenAI", "Python"], + ["gemini", "Gemini", "Python"], + ["Anthropic", "Anthropic", "TypeScript"], ])( - "scaffolds a strands %s runtime (%s) with an API-key credential", - async (provider, language, expectedImport, expectedModelClass) => { + "scaffolds a strands runtime for --model-provider %s (%s) with an API-key credential", + async (flagValue, provider, language) => { const projectRoot = await inProject(); const apiKeyPath = join(projectRoot, "api-key.txt"); await Bun.write(apiKeyPath, "test-api-key"); @@ -807,7 +810,7 @@ describe("project add runtime", () => { "--framework", "strands", "--model-provider", - provider, + flagValue, "--api-key", `file://${apiKeyPath}`, ]); @@ -823,24 +826,28 @@ describe("project add runtime", () => { const envLocal = await Bun.file(join(projectRoot, "agentcore", ".env.local")).text(); expect(envLocal).toContain(`${envVarName}='test-api-key'`); - - const loadFile = language === "TypeScript" ? "load.ts" : "load.py"; - const loadSource = await Bun.file( - join(projectRoot, "app", "my_agent", "model", loadFile), - ).text(); - expect(loadSource).toContain(expectedImport); - expect(loadSource).toContain(expectedModelClass); - expect(loadSource).toContain(credentialName); - expect(loadSource).toContain(envVarName); }, ); - test("normalizes a lowercase --model-provider to canonical casing", async () => { + test.each<[string, string, boolean, RegExp]>([ + [ + "without an API key", + "strands", + false, + /API key is required for the Anthropic model provider/, + ], + [ + "without a provider-capable template", + "none", + true, + /only supports the Bedrock model provider/, + ], + ])("rejects a non-Bedrock provider %s", async (_label, framework, includeApiKey, pattern) => { const projectRoot = await inProject(); const apiKeyPath = join(projectRoot, "api-key.txt"); await Bun.write(apiKeyPath, "test-api-key"); - await run([ + const flags = [ "add", "runtime", "--name", @@ -850,62 +857,13 @@ describe("project add runtime", () => { "--language", "Python", "--framework", - "strands", + framework, "--model-provider", - "anthropic", - "--api-key", - `file://${apiKeyPath}`, - ]); - - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - expect(spec.credentials).toContainEqual({ - authorizerType: "ApiKeyCredentialProvider", - name: "my_agentAnthropicApiKey", - }); - }); + "Anthropic", + ]; + if (includeApiKey) flags.push("--api-key", `file://${apiKeyPath}`); - test("rejects a non-Bedrock provider without an API key", async () => { - await inProject(); - await expect( - run([ - "add", - "runtime", - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "strands", - "--model-provider", - "Anthropic", - ]), - ).rejects.toThrow(/API key is required for the Anthropic model provider/); - }); - - test("rejects a non-Bedrock provider without the strands framework", async () => { - const projectRoot = await inProject(); - const apiKeyPath = join(projectRoot, "api-key.txt"); - await Bun.write(apiKeyPath, "test-api-key"); - await expect( - run([ - "add", - "runtime", - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Anthropic", - "--api-key", - `file://${apiKeyPath}`, - ]), - ).rejects.toThrow(/requires the strands framework/); + await expect(run(flags)).rejects.toThrow(pattern); }); }); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 5ce1e101f..3f066f6cb 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -406,7 +406,7 @@ describe("project create", () => { "--build", "CodeZip", "--model-provider", - "Bedrock", + "bedrock", "--memory", "none", "--skip-install", @@ -424,7 +424,7 @@ describe("project create", () => { expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true); }); - test("rejects lite_llm on the runtime path", async () => { + test.each([["lite_llm"]])("rejects the %s provider on the runtime path", async (provider) => { const directory = await inTempDirectory(); await expect( run([ @@ -434,15 +434,21 @@ describe("project create", () => { "--template", "agent-python-strands", "--model-provider", - "lite_llm", + provider, "--api-key", "-", ]), - ).rejects.toThrow(/runtime scaffolding does not support the 'lite_llm' model provider/); + ).rejects.toThrow( + new RegExp(`runtime scaffolding does not support the '${provider}' model provider`), + ); expect(existsSync(join(directory, "MyProject"))).toBe(false); }); - test("scaffolds a runtime with an OpenAI API-key credential", async () => { + test.each<[string, string]>([ + ["anthropic", "agent_python_strandsAnthropicApiKey"], + ["open_ai", "agent_python_strandsOpenAIApiKey"], + ["gemini", "agent_python_strandsGeminiApiKey"], + ])("scaffolds a runtime with a %s API-key credential", async (provider, credentialName) => { const directory = await inTempDirectory(); const apiKeyPath = join(directory, "api-key.txt"); await Bun.write(apiKeyPath, "test-api-key"); @@ -454,7 +460,7 @@ describe("project create", () => { "--template", "agent-python-strands", "--model-provider", - "openai", + provider, "--api-key", `file://${apiKeyPath}`, "--skip-install", @@ -465,7 +471,7 @@ describe("project create", () => { const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect(spec.credentials).toContainEqual({ authorizerType: "ApiKeyCredentialProvider", - name: "agent_python_strandsOpenAIApiKey", + name: credentialName, }); const envLocal = await Bun.file(join(projectRoot, "agentcore", ".env.local")).text(); expect(envLocal).toContain("test-api-key"); @@ -614,7 +620,7 @@ describe("project create", () => { "--framework", "strands", "--model-provider", - "Bedrock", + "bedrock", ...memoryFlags, "--skip-install", "--skip-git", @@ -650,7 +656,7 @@ describe("project create", () => { "--framework", "none", "--model-provider", - "Bedrock", + "bedrock", "--memory", "none", "--skip-install", @@ -683,7 +689,7 @@ describe("project create", () => { "--framework", "strands", "--model-provider", - "Bedrock", + "bedrock", "--memory", "none", "--skip-install", @@ -723,7 +729,7 @@ describe("project create", () => { "--framework", "none", "--model-provider", - "Bedrock", + "bedrock", "--memory", memoryShortcut, "--skip-install", @@ -757,7 +763,7 @@ describe("project create", () => { "--framework", "none", "--model-provider", - "Bedrock", + "bedrock", "--memory", "none", "--skip-install", From 08116c2d98b7d5af09cbc2159be60ea5133ff915 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 22:49:25 +0000 Subject: [PATCH 07/16] refactor(runtime): inline the Bedrock-only template guard and trim comments --- src/core/project/templates/runtime.ts | 31 ++++++------------- .../project/add/runtime/index.test.ts | 3 -- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 9b610e5cc..f0575e470 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -8,13 +8,7 @@ import { credentialEnvVarName } from "../../../projectSchemas/credential"; import { InputValidationError } from "../../../errors"; import { toPythonPackageName } from "../fsUtils"; -/** - * The AgentCore Identity wiring a non-Bedrock model provider needs: a Handlebars - * context fragment naming the credential the generated `model/load` reads, the - * `ApiKeyCredentialProvider` credential to register in agentcore.json, and the - * API-key value to write to agentcore/.env.local. Bedrock uses the runtime's IAM - * credentials, so it contributes none of these. - */ +/** AgentCore Identity wiring for a non-Bedrock provider; empty for Bedrock. */ type ModelProviderIdentity = { context: { hasIdentity: boolean; identityProviders: { name: string; envVarName: string }[] }; credentials: NonNullable; @@ -47,19 +41,6 @@ function resolveModelProviderIdentity(input: RuntimeResourceConfig): ModelProvid }; } -/** - * The plain (non-strands) templates ship no model abstraction, so they only run - * on Bedrock. A non-Bedrock provider is rejected here, when the template is - * resolved, rather than by the scaffold schema — the schema cannot know which - * template a given framework/language/protocol maps to. - */ -function assertBedrockOnly(input: RuntimeResourceConfig, template: string): void { - if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") - throw new InputValidationError( - `the ${template} template only supports the Bedrock model provider`, - ); -} - function buildRuntimeSpec(input: RuntimeResourceConfig): ProjectRuntime { const { scaffoldRuntimeInput, name, ...infra } = input; return { @@ -142,7 +123,10 @@ const importBedrockAgentResolver = () => async (input: RuntimeResourceConfig) => const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({ [buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => { - assertBedrockOnly(input, "agent-python"); + if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") + throw new InputValidationError( + "the agent-python template only supports the Bedrock model provider", + ); if (input.scaffoldRuntimeInput.memory !== undefined) throw new InputValidationError(`memory is not supported with the agent-python template`); const isContainer = input.scaffoldRuntimeInput.build === "Container"; @@ -270,7 +254,10 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa }; }, [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { - assertBedrockOnly(input, "mcp-python-fastmcp"); + if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") + throw new InputValidationError( + "the mcp-python-fastmcp template only supports the Bedrock model provider", + ); if (input.scaffoldRuntimeInput.memory !== undefined) throw new InputValidationError("memory is not supported with an MCP runtime"); const filesystemConfigurations = input.filesystemConfigurations ?? []; diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 4ee2fc9d2..ea152e847 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -783,9 +783,6 @@ describe("project add runtime", () => { ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); }); - // The flag value casing varies deliberately (lowercase and canonical both - // normalize to the same credential name). Behaviour is asserted against the - // agentcore spec and .env.local, not the scaffolded template code. test.each<[string, string, string]>([ ["anthropic", "Anthropic", "Python"], ["OpenAI", "OpenAI", "Python"], From bb56b170f4bea05d7691156fb84fec8928a34e9c Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 23:40:15 +0000 Subject: [PATCH 08/16] refactor(create): split model-provider resolution into per-domain functions --- .../a2a-python-strands/pyproject.toml | 2 +- src/handlers/project/create/index.ts | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/assets/templates/a2a-python-strands/pyproject.toml b/src/assets/templates/a2a-python-strands/pyproject.toml index a38f44377..85aa6b900 100644 --- a/src/assets/templates/a2a-python-strands/pyproject.toml +++ b/src/assets/templates/a2a-python-strands/pyproject.toml @@ -9,7 +9,7 @@ description = "AgentCore A2A Agent using Strands SDK" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "a2a-sdk[all] >= 0.3.0, < 0.4.0", + "a2a-sdk[all] ~= 0.3.0", "aws-opentelemetry-distro ~= 0.17.0", "bedrock-agentcore[a2a] ~= 1.9.1", "botocore[crt] ~= 1.43.0", diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index ef23aee39..56477bfd1 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -342,7 +342,7 @@ async function resolveScaffoldRuntimeInput( config: CreateProjectHandlerConfig, flags: RuntimePathFlagValues, ): Promise { - const modelProvider = resolveModelProvider(flags["model-provider"], "runtime"); + const modelProvider = resolveRuntimeModelProvider(flags["model-provider"]); const source = new SourceResolver({ stdin: config.io.stdin }); const apiKey = await source.resolveSecret("api-key", flags["api-key"]); @@ -378,7 +378,7 @@ async function resolveScaffoldRuntimeInput( // same addResource path. Exported so the TUI create wizard builds its harness // input through the exact same translation as the flag-driven path. export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { - const provider = resolveModelProvider(flags["model-provider"], "harness"); + const provider = resolveHarnessModelProvider(flags["model-provider"]); const additionalParams = parseJsonFlag>( "additional-params", flags["additional-params"], @@ -413,10 +413,9 @@ export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): Scaff return input; } -// Each accepted flag value maps into whichever domains support it: the harness -// path keeps the lowercase names its spec uses, the runtime path takes the -// title-cased names the templates render against. A value absent from a domain -// (lite_llm on runtime, anthropic on harness) is unsupported there. +// Runtimes and harnesses support different model sets and record them under +// different names in their spec configs, so the shared --model-provider flag is +// mapped to each domain here behind a consistent interface. const MODEL_PROVIDERS: Record< ModelProviderFlag, { harness?: HarnessModelProvider; runtime?: ModelProvider } @@ -428,26 +427,27 @@ const MODEL_PROVIDERS: Record< anthropic: { runtime: "Anthropic" }, }; -function resolveModelProvider( - value: ModelProviderFlag | undefined, - domain: "harness", -): HarnessModelProvider; -function resolveModelProvider( - value: ModelProviderFlag | undefined, - domain: "runtime", -): ModelProvider | undefined; -function resolveModelProvider( - value: ModelProviderFlag | undefined, - domain: "harness" | "runtime", -): HarnessModelProvider | ModelProvider | undefined { - if (value === undefined) return domain === "harness" ? "bedrock" : undefined; - const provider = MODEL_PROVIDERS[value][domain]; +function resolveHarnessModelProvider( + providerFlag: ModelProviderFlag | undefined, +): HarnessModelProvider { + if (providerFlag === undefined) return "bedrock"; + const provider = MODEL_PROVIDERS[providerFlag].harness; if (provider === undefined) throw new InputValidationError( - domain === "harness" - ? `the '${value}' model provider is not supported for harness projects` - : `runtime scaffolding does not support the '${value}' model provider ` + - `(expected bedrock, anthropic, open_ai, or gemini)`, + `the '${providerFlag}' model provider is not supported for harness projects`, + ); + return provider; +} + +function resolveRuntimeModelProvider( + providerFlag: ModelProviderFlag | undefined, +): ModelProvider | undefined { + if (providerFlag === undefined) return undefined; + const provider = MODEL_PROVIDERS[providerFlag].runtime; + if (provider === undefined) + throw new InputValidationError( + `runtime scaffolding does not support the '${providerFlag}' model provider ` + + `(expected bedrock, anthropic, open_ai, or gemini)`, ); return provider; } From c20040846cc85940de4e75ef4a1d2b3e612cf140 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 23:40:16 +0000 Subject: [PATCH 09/16] refactor(runtime): return provider credentials as spec entries and drop redundant hasIdentity --- .../templates/agent-python-strands/README.md | 2 +- .../agent-typescript-strands/README.md | 2 +- src/core/project/templates/project.ts | 16 +--- src/core/project/templates/runtime.ts | 88 +++++++++++-------- src/core/project/templates/types.ts | 15 ++++ 5 files changed, 68 insertions(+), 55 deletions(-) diff --git a/src/assets/templates/agent-python-strands/README.md b/src/assets/templates/agent-python-strands/README.md index 5714aafbf..e3edfd5ae 100644 --- a/src/assets/templates/agent-python-strands/README.md +++ b/src/assets/templates/agent-python-strands/README.md @@ -23,7 +23,7 @@ invoking the agent. | Variable | Required | Description | | --- | --- | --- | -{{#if hasIdentity}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{#if identityProviders.[0]}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | {{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity | # Developing locally diff --git a/src/assets/templates/agent-typescript-strands/README.md b/src/assets/templates/agent-typescript-strands/README.md index 08f22a6fd..4c64ecd57 100644 --- a/src/assets/templates/agent-typescript-strands/README.md +++ b/src/assets/templates/agent-typescript-strands/README.md @@ -22,7 +22,7 @@ validation when extending the request shape, and pass only prompt text to the ag | Variable | Required | Description | | --- | --- | --- | -{{#if hasIdentity}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{#if identityProviders.[0]}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | {{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity | # Developing locally diff --git a/src/core/project/templates/project.ts b/src/core/project/templates/project.ts index b7ead7298..55a2e6442 100644 --- a/src/core/project/templates/project.ts +++ b/src/core/project/templates/project.ts @@ -7,7 +7,7 @@ import type { } from "../../../handlers/project/add/runtime/types"; import { InputValidationError } from "../../../errors/errors"; import { getRuntimeTemplateResolver } from "./runtime"; -import type { SpecEntries, Template, TemplateRenderer } from "./types"; +import { mergeSpecEntries, type Template, type TemplateRenderer } from "./types"; import type { EnvLocalEntry } from "../../../handlers/project/types"; type CreateProjectConfig = { @@ -66,17 +66,3 @@ export async function createProjectTree( } const json = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`; - -function mergeSpecEntries(entries: SpecEntries[]): SpecEntries { - const runtimes = entries.flatMap(({ runtimes }) => runtimes ?? []); - const credentials = entries.flatMap(({ credentials }) => credentials ?? []); - const memories = entries.flatMap(({ memories }) => memories ?? []); - const harnesses = entries.flatMap(({ harnesses }) => harnesses ?? []); - - return { - ...(runtimes.length > 0 && { runtimes }), - ...(credentials.length > 0 && { credentials }), - ...(memories.length > 0 && { memories }), - ...(harnesses.length > 0 && { harnesses }), - }; -} diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index f0575e470..718432df0 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -2,33 +2,39 @@ import { FsTreeNode } from "./fsTree"; import type { AssetSource } from "../source"; import type { RuntimeResourceConfig } from "../../../handlers/project/add/runtime/types"; import type { ProjectRuntime } from "../../../projectSchemas/runtime"; -import type { SpecEntries, TemplateRenderer, TemplateResolver } from "./types"; +import { + mergeSpecEntries, + type SpecEntries, + type TemplateRenderer, + type TemplateResolver, +} from "./types"; import type { EnvLocalEntry, ScaffoldRuntimeInput } from "../../../handlers/project/types"; import { credentialEnvVarName } from "../../../projectSchemas/credential"; import { InputValidationError } from "../../../errors"; import { toPythonPackageName } from "../fsUtils"; -/** AgentCore Identity wiring for a non-Bedrock provider; empty for Bedrock. */ -type ModelProviderIdentity = { - context: { hasIdentity: boolean; identityProviders: { name: string; envVarName: string }[] }; - credentials: NonNullable; +/** + * A non-Bedrock provider's contributions to a scaffolded runtime: the template + * render context (the identity provider the generated model/load reads), the + * spec entry (its ApiKeyCredentialProvider credential), and the .env.local + * secret. Empty for Bedrock, which uses the runtime's IAM credentials. + */ +type ModelProviderScaffold = { + templateRenderContext: { identityProviders: { name: string; envVarName: string }[] }; + spec: SpecEntries; envEntries: EnvLocalEntry[]; }; -function resolveModelProviderIdentity(input: RuntimeResourceConfig): ModelProviderIdentity { +function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProviderScaffold { const { modelProvider, apiKey } = input.scaffoldRuntimeInput; - if (modelProvider === "Bedrock") { - return { - context: { hasIdentity: false, identityProviders: [] }, - credentials: [], - envEntries: [], - }; + if (modelProvider === undefined || modelProvider === "Bedrock") { + return { templateRenderContext: { identityProviders: [] }, spec: {}, envEntries: [] }; } const credentialName = `${input.name}${modelProvider}ApiKey`; const envVarName = credentialEnvVarName(credentialName); return { - context: { hasIdentity: true, identityProviders: [{ name: credentialName, envVarName }] }, - credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }], + templateRenderContext: { identityProviders: [{ name: credentialName, envVarName }] }, + spec: { credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }] }, envEntries: apiKey ? [ { @@ -159,7 +165,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa : [], ); const memory = input.scaffoldRuntimeInput.memory; - const identity = resolveModelProviderIdentity(input); + const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toPythonPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -167,7 +173,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - ...identity.context, + ...modelScaffold.templateRenderContext, hasGateway: false, hasPayment: false, isVpc: input.networkMode === "VPC", @@ -198,12 +204,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa ); return { tree, - spec: { - runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], - ...(memory && { memories: [memory] }), - ...(identity.credentials.length > 0 && { credentials: identity.credentials }), - }, - ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), + spec: mergeSpecEntries([ + { + runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], + ...(memory && { memories: [memory] }), + }, + modelScaffold.spec, + ]), + ...(modelScaffold.envEntries.length > 0 && { envEntries: modelScaffold.envEntries }), }; }, [buildResolverKey("strands", "TypeScript", "HTTP")]: async (input: RuntimeResourceConfig) => { @@ -219,7 +227,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa "the agent-typescript-strands template does not support short-term-only memory; add long-term strategies or use --memory none", ); - const identity = resolveModelProviderIdentity(input); + const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toNpmPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -227,7 +235,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - ...identity.context, + ...modelScaffold.templateRenderContext, }; const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( @@ -245,12 +253,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa ); return { tree, - spec: { - runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], - ...(memory && { memories: [memory] }), - ...(identity.credentials.length > 0 && { credentials: identity.credentials }), - }, - ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), + spec: mergeSpecEntries([ + { + runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }], + ...(memory && { memories: [memory] }), + }, + modelScaffold.spec, + ]), + ...(modelScaffold.envEntries.length > 0 && { envEntries: modelScaffold.envEntries }), }; }, [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { @@ -320,7 +330,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa : [], ); const memory = input.scaffoldRuntimeInput.memory; - const identity = resolveModelProviderIdentity(input); + const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toPythonPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider, @@ -328,7 +338,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - ...identity.context, + ...modelScaffold.templateRenderContext, sessionStorageMountPath, efsMounts, s3Mounts, @@ -355,12 +365,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa ); return { tree, - spec: { - runtimes: [{ ...buildRuntimeSpec(input), protocol: "A2A" as const }], - ...(memory && { memories: [memory] }), - ...(identity.credentials.length > 0 && { credentials: identity.credentials }), - }, - ...(identity.envEntries.length > 0 && { envEntries: identity.envEntries }), + spec: mergeSpecEntries([ + { + runtimes: [{ ...buildRuntimeSpec(input), protocol: "A2A" as const }], + ...(memory && { memories: [memory] }), + }, + modelScaffold.spec, + ]), + ...(modelScaffold.envEntries.length > 0 && { envEntries: modelScaffold.envEntries }), }; }, }); diff --git a/src/core/project/templates/types.ts b/src/core/project/templates/types.ts index d7e386301..ec845b3f4 100644 --- a/src/core/project/templates/types.ts +++ b/src/core/project/templates/types.ts @@ -16,6 +16,21 @@ export type SpecEntries = { evaluators?: Evaluator[]; }; +/** Combines several {@link SpecEntries} into one, concatenating each resource collection. */ +export function mergeSpecEntries(entries: SpecEntries[]): SpecEntries { + const runtimes = entries.flatMap(({ runtimes }) => runtimes ?? []); + const credentials = entries.flatMap(({ credentials }) => credentials ?? []); + const memories = entries.flatMap(({ memories }) => memories ?? []); + const harnesses = entries.flatMap(({ harnesses }) => harnesses ?? []); + + return { + ...(runtimes.length > 0 && { runtimes }), + ...(credentials.length > 0 && { credentials }), + ...(memories.length > 0 && { memories }), + ...(harnesses.length > 0 && { harnesses }), + }; +} + /** A group of files and resources that can be rendered into a project **/ export type Template = { tree: FsTreeNode; From b0a4787ee9c743606e798e8259f4ced172188fe4 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 2 Sep 2026 23:45:12 +0000 Subject: [PATCH 10/16] refactor(runtime): make model provider optional and reject it for MCP runtimes --- src/core/project/templates/runtime.ts | 15 +++++++------ .../project/add/runtime/index.test.ts | 21 +++++++++++++++---- src/handlers/project/shortcuts.ts | 1 - src/handlers/project/types.ts | 9 +++++--- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 718432df0..695af6f6d 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -129,7 +129,8 @@ const importBedrockAgentResolver = () => async (input: RuntimeResourceConfig) => const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({ [buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => { - if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") + const { modelProvider } = input.scaffoldRuntimeInput; + if (modelProvider !== undefined && modelProvider !== "Bedrock") throw new InputValidationError( "the agent-python template only supports the Bedrock model provider", ); @@ -168,7 +169,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toPythonPackageName(input.name), - modelProvider: input.scaffoldRuntimeInput.modelProvider, + modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, @@ -230,7 +231,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toNpmPackageName(input.name), - modelProvider: input.scaffoldRuntimeInput.modelProvider, + modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, @@ -264,10 +265,8 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa }; }, [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { - if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") - throw new InputValidationError( - "the mcp-python-fastmcp template only supports the Bedrock model provider", - ); + if (input.scaffoldRuntimeInput.modelProvider !== undefined) + throw new InputValidationError("an MCP runtime does not use a model provider"); if (input.scaffoldRuntimeInput.memory !== undefined) throw new InputValidationError("memory is not supported with an MCP runtime"); const filesystemConfigurations = input.filesystemConfigurations ?? []; @@ -333,7 +332,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toPythonPackageName(input.name), - modelProvider: input.scaffoldRuntimeInput.modelProvider, + modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index ea152e847..5c0b47192 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -301,8 +301,6 @@ describe("project add runtime", () => { "none", "--protocol", "MCP", - "--model-provider", - "Bedrock", "--memory", "none", ], @@ -699,12 +697,27 @@ describe("project add runtime", () => { "none", "--protocol", "MCP", - "--model-provider", - "Bedrock", "--memory", "shortTerm", ], ], + [ + "MCP runtime rejects a model provider", + [ + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "none", + "--protocol", + "MCP", + "--model-provider", + "Bedrock", + ], + ], [ "--memory shortTerm is not supported with --framework none", [ diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index 72a6e08da..f1ec2d4a0 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -83,7 +83,6 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { language: "Python", framework: "none", protocol: "MCP", - modelProvider: "Bedrock", memory: "none", runtimeVersion: "PYTHON_3_14", }, diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index bafd8c065..2551ea6dd 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -71,20 +71,23 @@ export const ScaffoldRuntimeInputSchema = z language: z.enum(["Python", "TypeScript"]), framework: z.enum(["strands", "none"]), protocol: ProtocolModeSchema.optional(), - modelProvider: ModelProviderSchema, + // Optional: an MCP runtime has no model provider at all, and other runtimes + // default to Bedrock. A resolver rejects a provider its template cannot use. + modelProvider: ModelProviderSchema.optional(), apiKey: z.string().min(1).optional(), memory: MemorySchema.optional(), runtimeVersion: RuntimeVersionSchema.optional(), }) .superRefine(({ modelProvider, apiKey }, ctx) => { - if (modelProvider === "Bedrock" && apiKey !== undefined) { + const usesApiKey = modelProvider !== undefined && modelProvider !== "Bedrock"; + if (apiKey !== undefined && !usesApiKey) { ctx.addIssue({ code: "custom", message: "API keys are not compatible with Bedrock model providers", path: ["apiKey"], }); } - if (modelProvider !== "Bedrock" && apiKey === undefined) { + if (apiKey === undefined && usesApiKey) { ctx.addIssue({ code: "custom", message: `an API key is required for the ${modelProvider} model provider`, From c50a5135b7ed2fac4d9991e17272f9d0b4bac0ff Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 00:10:09 +0000 Subject: [PATCH 11/16] feat(runtime): support LiteLLM as a runtime model provider --- src/core/project/templates/project.ts | 3 +- src/core/project/templates/runtime.ts | 32 ++++++++--------- src/core/project/templates/spec.ts | 16 +++++++++ src/core/project/templates/types.ts | 15 -------- .../project/add/runtime/index.test.ts | 28 ++++++--------- src/handlers/project/create/index.ts | 8 +++-- src/handlers/project/project.test.ts | 35 ++++++++++--------- src/handlers/project/types.ts | 22 ++++++------ 8 files changed, 80 insertions(+), 79 deletions(-) create mode 100644 src/core/project/templates/spec.ts diff --git a/src/core/project/templates/project.ts b/src/core/project/templates/project.ts index 55a2e6442..980e3b865 100644 --- a/src/core/project/templates/project.ts +++ b/src/core/project/templates/project.ts @@ -7,7 +7,8 @@ import type { } from "../../../handlers/project/add/runtime/types"; import { InputValidationError } from "../../../errors/errors"; import { getRuntimeTemplateResolver } from "./runtime"; -import { mergeSpecEntries, type Template, type TemplateRenderer } from "./types"; +import { mergeSpecEntries } from "./spec"; +import type { Template, TemplateRenderer } from "./types"; import type { EnvLocalEntry } from "../../../handlers/project/types"; type CreateProjectConfig = { diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 695af6f6d..2190e6267 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -2,12 +2,8 @@ import { FsTreeNode } from "./fsTree"; import type { AssetSource } from "../source"; import type { RuntimeResourceConfig } from "../../../handlers/project/add/runtime/types"; import type { ProjectRuntime } from "../../../projectSchemas/runtime"; -import { - mergeSpecEntries, - type SpecEntries, - type TemplateRenderer, - type TemplateResolver, -} from "./types"; +import { mergeSpecEntries } from "./spec"; +import type { SpecEntries, TemplateRenderer, TemplateResolver } from "./types"; import type { EnvLocalEntry, ScaffoldRuntimeInput } from "../../../handlers/project/types"; import { credentialEnvVarName } from "../../../projectSchemas/credential"; import { InputValidationError } from "../../../errors"; @@ -27,7 +23,9 @@ type ModelProviderScaffold = { function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProviderScaffold { const { modelProvider, apiKey } = input.scaffoldRuntimeInput; - if (modelProvider === undefined || modelProvider === "Bedrock") { + // Only a keyed provider needs identity wiring; Bedrock — and a keyless LiteLLM, + // which routes through Bedrock by default — uses the runtime's IAM role. + if (apiKey === undefined) { return { templateRenderContext: { identityProviders: [] }, spec: {}, envEntries: [] }; } const credentialName = `${input.name}${modelProvider}ApiKey`; @@ -35,15 +33,13 @@ function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProvid return { templateRenderContext: { identityProviders: [{ name: credentialName, envVarName }] }, spec: { credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }] }, - envEntries: apiKey - ? [ - { - key: envVarName, - value: apiKey, - comment: `API key for the ${modelProvider} model provider (runtime ${input.name})`, - }, - ] - : [], + envEntries: [ + { + key: envVarName, + value: apiKey, + comment: `API key for the ${modelProvider} model provider (runtime ${input.name})`, + }, + ], }; } @@ -218,6 +214,10 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa [buildResolverKey("strands", "TypeScript", "HTTP")]: async (input: RuntimeResourceConfig) => { if (input.protocol !== undefined && input.protocol !== "HTTP") throw new InputValidationError("the agent-typescript-strands template only supports HTTP"); + if (input.scaffoldRuntimeInput.modelProvider === "LiteLLM") + throw new InputValidationError( + "the agent-typescript-strands template does not support the LiteLLM model provider", + ); const memory = input.scaffoldRuntimeInput.memory; // The TypeScript strands SDK's createAgentCoreMemoryStores requires at least one diff --git a/src/core/project/templates/spec.ts b/src/core/project/templates/spec.ts new file mode 100644 index 000000000..a412ca74f --- /dev/null +++ b/src/core/project/templates/spec.ts @@ -0,0 +1,16 @@ +import type { SpecEntries } from "./types"; + +/** Combines several {@link SpecEntries} into one, concatenating each resource collection. */ +export function mergeSpecEntries(entries: SpecEntries[]): SpecEntries { + const runtimes = entries.flatMap(({ runtimes }) => runtimes ?? []); + const credentials = entries.flatMap(({ credentials }) => credentials ?? []); + const memories = entries.flatMap(({ memories }) => memories ?? []); + const harnesses = entries.flatMap(({ harnesses }) => harnesses ?? []); + + return { + ...(runtimes.length > 0 && { runtimes }), + ...(credentials.length > 0 && { credentials }), + ...(memories.length > 0 && { memories }), + ...(harnesses.length > 0 && { harnesses }), + }; +} diff --git a/src/core/project/templates/types.ts b/src/core/project/templates/types.ts index ec845b3f4..d7e386301 100644 --- a/src/core/project/templates/types.ts +++ b/src/core/project/templates/types.ts @@ -16,21 +16,6 @@ export type SpecEntries = { evaluators?: Evaluator[]; }; -/** Combines several {@link SpecEntries} into one, concatenating each resource collection. */ -export function mergeSpecEntries(entries: SpecEntries[]): SpecEntries { - const runtimes = entries.flatMap(({ runtimes }) => runtimes ?? []); - const credentials = entries.flatMap(({ credentials }) => credentials ?? []); - const memories = entries.flatMap(({ memories }) => memories ?? []); - const harnesses = entries.flatMap(({ harnesses }) => harnesses ?? []); - - return { - ...(runtimes.length > 0 && { runtimes }), - ...(credentials.length > 0 && { credentials }), - ...(memories.length > 0 && { memories }), - ...(harnesses.length > 0 && { harnesses }), - }; -} - /** A group of files and resources that can be rendered into a project **/ export type Template = { tree: FsTreeNode; diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 5c0b47192..59118c54a 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -839,24 +839,18 @@ describe("project add runtime", () => { }, ); - test.each<[string, string, boolean, RegExp]>([ - [ - "without an API key", - "strands", - false, - /API key is required for the Anthropic model provider/, - ], - [ - "without a provider-capable template", - "none", - true, - /only supports the Bedrock model provider/, - ], - ])("rejects a non-Bedrock provider %s", async (_label, framework, includeApiKey, pattern) => { + test.each<[string, string, string, boolean]>([ + ["Anthropic without an API key", "Anthropic", "strands", false], + ["OpenAI without an API key", "OpenAI", "strands", false], + ["Gemini without an API key", "Gemini", "strands", false], + ["a non-Bedrock provider on a provider-less template", "Anthropic", "none", true], + ["LiteLLM on the TypeScript template", "LiteLLM", "strands", false], + ])("rejects %s", async (_label, provider, framework, includeApiKey) => { const projectRoot = await inProject(); const apiKeyPath = join(projectRoot, "api-key.txt"); await Bun.write(apiKeyPath, "test-api-key"); + const language = provider === "LiteLLM" ? "TypeScript" : "Python"; const flags = [ "add", "runtime", @@ -865,15 +859,15 @@ describe("project add runtime", () => { "--build", "CodeZip", "--language", - "Python", + language, "--framework", framework, "--model-provider", - "Anthropic", + provider, ]; if (includeApiKey) flags.push("--api-key", `file://${apiKeyPath}`); - await expect(run(flags)).rejects.toThrow(pattern); + await expect(run(flags)).rejects.toBeInstanceOf(InputValidationError); }); }); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 56477bfd1..038c133c2 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -423,10 +423,14 @@ const MODEL_PROVIDERS: Record< bedrock: { harness: "bedrock", runtime: "Bedrock" }, open_ai: { harness: "open_ai", runtime: "OpenAI" }, gemini: { harness: "gemini", runtime: "Gemini" }, - lite_llm: { harness: "lite_llm" }, + lite_llm: { harness: "lite_llm", runtime: "LiteLLM" }, anthropic: { runtime: "Anthropic" }, }; +const RUNTIME_PROVIDER_FLAGS = (Object.keys(MODEL_PROVIDERS) as ModelProviderFlag[]).filter( + (flag) => MODEL_PROVIDERS[flag].runtime !== undefined, +); + function resolveHarnessModelProvider( providerFlag: ModelProviderFlag | undefined, ): HarnessModelProvider { @@ -447,7 +451,7 @@ function resolveRuntimeModelProvider( if (provider === undefined) throw new InputValidationError( `runtime scaffolding does not support the '${providerFlag}' model provider ` + - `(expected bedrock, anthropic, open_ai, or gemini)`, + `(expected ${RUNTIME_PROVIDER_FLAGS.join(", ")})`, ); return provider; } diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 3f066f6cb..7b7c24231 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -424,30 +424,31 @@ describe("project create", () => { expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true); }); - test.each([["lite_llm"]])("rejects the %s provider on the runtime path", async (provider) => { + test("scaffolds a keyless LiteLLM runtime with no credential", async () => { const directory = await inTempDirectory(); - await expect( - run([ - "create", - "--name", - "MyProject", - "--template", - "agent-python-strands", - "--model-provider", - provider, - "--api-key", - "-", - ]), - ).rejects.toThrow( - new RegExp(`runtime scaffolding does not support the '${provider}' model provider`), - ); - expect(existsSync(join(directory, "MyProject"))).toBe(false); + await run([ + "create", + "--name", + "MyProject", + "--template", + "agent-python-strands", + "--model-provider", + "lite_llm", + "--skip-install", + "--skip-git", + ]); + + const projectRoot = join(directory, "MyProject"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes).toHaveLength(1); + expect(spec.credentials ?? []).toEqual([]); }); test.each<[string, string]>([ ["anthropic", "agent_python_strandsAnthropicApiKey"], ["open_ai", "agent_python_strandsOpenAIApiKey"], ["gemini", "agent_python_strandsGeminiApiKey"], + ["lite_llm", "agent_python_strandsLiteLLMApiKey"], ])("scaffolds a runtime with a %s API-key credential", async (provider, credentialName) => { const directory = await inTempDirectory(); const apiKeyPath = join(directory, "api-key.txt"); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 2551ea6dd..d2649a328 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -40,12 +40,8 @@ export type ManagedEvaluatorScaffoldInput = { timeoutSeconds?: number; }; -/** - * Model providers the scaffolded runtime code supports. `Bedrock` uses the - * runtime's IAM credentials; the others authenticate with an API key managed - * through AgentCore Identity. - */ -export const MODEL_PROVIDERS = ["Bedrock", "Anthropic", "OpenAI", "Gemini"] as const; +/** Model providers the scaffolded runtime code can target. */ +export const MODEL_PROVIDERS = ["Bedrock", "Anthropic", "OpenAI", "Gemini", "LiteLLM"] as const; export type ModelProvider = (typeof MODEL_PROVIDERS)[number]; const MODEL_PROVIDER_ALIASES: Record = { @@ -54,6 +50,8 @@ const MODEL_PROVIDER_ALIASES: Record = { openai: "OpenAI", open_ai: "OpenAI", gemini: "Gemini", + litellm: "LiteLLM", + lite_llm: "LiteLLM", }; /** Parses a provider name case-insensitively (e.g. `anthropic`), normalizing to canonical casing. */ @@ -71,23 +69,25 @@ export const ScaffoldRuntimeInputSchema = z language: z.enum(["Python", "TypeScript"]), framework: z.enum(["strands", "none"]), protocol: ProtocolModeSchema.optional(), - // Optional: an MCP runtime has no model provider at all, and other runtimes - // default to Bedrock. A resolver rejects a provider its template cannot use. modelProvider: ModelProviderSchema.optional(), apiKey: z.string().min(1).optional(), memory: MemorySchema.optional(), runtimeVersion: RuntimeVersionSchema.optional(), }) .superRefine(({ modelProvider, apiKey }, ctx) => { - const usesApiKey = modelProvider !== undefined && modelProvider !== "Bedrock"; - if (apiKey !== undefined && !usesApiKey) { + // LiteLLM routes to any provider (Bedrock by default), so its key is optional; + // the other non-Bedrock providers always call their own API and require one. + const requiresApiKey = + modelProvider !== undefined && modelProvider !== "Bedrock" && modelProvider !== "LiteLLM"; + const allowsApiKey = requiresApiKey || modelProvider === "LiteLLM"; + if (apiKey !== undefined && !allowsApiKey) { ctx.addIssue({ code: "custom", message: "API keys are not compatible with Bedrock model providers", path: ["apiKey"], }); } - if (apiKey === undefined && usesApiKey) { + if (apiKey === undefined && requiresApiKey) { ctx.addIssue({ code: "custom", message: `an API key is required for the ${modelProvider} model provider`, From 63a6705101e580574e9e86b072249e0b8f3bddf4 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 02:11:10 +0000 Subject: [PATCH 12/16] refactor(runtime): rename provider template-config type and tighten comments --- src/core/project/templates/runtime.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 2190e6267..01c5b2ba7 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -9,22 +9,15 @@ import { credentialEnvVarName } from "../../../projectSchemas/credential"; import { InputValidationError } from "../../../errors"; import { toPythonPackageName } from "../fsUtils"; -/** - * A non-Bedrock provider's contributions to a scaffolded runtime: the template - * render context (the identity provider the generated model/load reads), the - * spec entry (its ApiKeyCredentialProvider credential), and the .env.local - * secret. Empty for Bedrock, which uses the runtime's IAM credentials. - */ -type ModelProviderScaffold = { +/** A model provider's render context, spec entries, and .env.local secrets for a scaffolded runtime. */ +type ModelProviderTemplateConfig = { templateRenderContext: { identityProviders: { name: string; envVarName: string }[] }; spec: SpecEntries; envEntries: EnvLocalEntry[]; }; -function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProviderScaffold { +function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProviderTemplateConfig { const { modelProvider, apiKey } = input.scaffoldRuntimeInput; - // Only a keyed provider needs identity wiring; Bedrock — and a keyless LiteLLM, - // which routes through Bedrock by default — uses the runtime's IAM role. if (apiKey === undefined) { return { templateRenderContext: { identityProviders: [] }, spec: {}, envEntries: [] }; } From 50c11d07f265ae6dbfe3f007457cf3cddfe38eb5 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 03:28:17 +0000 Subject: [PATCH 13/16] fix(templates): install provider npm SDKs for the TypeScript strands template --- .../{package.json => package.json.template} | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) rename src/assets/templates/agent-typescript-strands/{package.json => package.json.template} (68%) diff --git a/src/assets/templates/agent-typescript-strands/package.json b/src/assets/templates/agent-typescript-strands/package.json.template similarity index 68% rename from src/assets/templates/agent-typescript-strands/package.json rename to src/assets/templates/agent-typescript-strands/package.json.template index 77eb3bc8e..804fec0f1 100644 --- a/src/assets/templates/agent-typescript-strands/package.json +++ b/src/assets/templates/agent-typescript-strands/package.json.template @@ -10,11 +10,14 @@ "dev": "tsx watch main.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "~1.25.2", + {{#if (eq modelProvider "Anthropic")}}"@anthropic-ai/sdk": "~0.92.0", + {{/if}}{{#if (eq modelProvider "Gemini")}}"@google/genai": "~1.40.0", + {{/if}}"@modelcontextprotocol/sdk": "~1.25.2", "@opentelemetry/api": "~1.9.0", "@strands-agents/sdk": "~1.5.0", "bedrock-agentcore": "~0.3.0", - "tsx": "~4.19.0", + {{#if (eq modelProvider "OpenAI")}}"openai": "~6.7.0", + {{/if}}"tsx": "~4.19.0", "zod": "~4.4.3" }, "devDependencies": { From acb532b6203f519f8779430a4fb62176c69a486b Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 03:28:17 +0000 Subject: [PATCH 14/16] test(project): cover harness-provider rejection and TypeScript provider SDK deps --- src/handlers/project/project.test.ts | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 7b7c24231..a73b5fca2 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -182,6 +182,36 @@ describe("project create", () => { }, ); + test("rejects a runtime-only provider on the harness path", async () => { + await inTempDirectory(); + await expect( + run(["create", "--name", "MyAgent", "--model-provider", "anthropic"]), + ).rejects.toThrow(/'anthropic' model provider is not supported for harness projects/); + }); + + test("scaffolds a TypeScript strands runtime with the provider's npm SDK", async () => { + const directory = await inTempDirectory(); + const apiKeyPath = join(directory, "api-key.txt"); + await Bun.write(apiKeyPath, "test-key"); + await run([ + "create", + "--name", + "TsProj", + "--template", + "agent-typescript-strands", + "--model-provider", + "anthropic", + "--api-key", + `file://${apiKeyPath}`, + "--skip-install", + "--skip-git", + ]); + const pkg = await Bun.file( + join(directory, "TsProj", "app", "agent_typescript_strands", "package.json"), + ).text(); + expect(pkg).toContain("@anthropic-ai/sdk"); + }); + test("supports LiteLLM model configuration on the harness path", async () => { const directory = await inTempDirectory(); await run([ From b92ae9ec3dd44b7722d931b19f851d1fef250012 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 03:43:36 +0000 Subject: [PATCH 15/16] test(project): drop redundant TypeScript provider SDK scaffold test --- src/handlers/project/project.test.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index a73b5fca2..20f0a4aeb 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -189,29 +189,6 @@ describe("project create", () => { ).rejects.toThrow(/'anthropic' model provider is not supported for harness projects/); }); - test("scaffolds a TypeScript strands runtime with the provider's npm SDK", async () => { - const directory = await inTempDirectory(); - const apiKeyPath = join(directory, "api-key.txt"); - await Bun.write(apiKeyPath, "test-key"); - await run([ - "create", - "--name", - "TsProj", - "--template", - "agent-typescript-strands", - "--model-provider", - "anthropic", - "--api-key", - `file://${apiKeyPath}`, - "--skip-install", - "--skip-git", - ]); - const pkg = await Bun.file( - join(directory, "TsProj", "app", "agent_typescript_strands", "package.json"), - ).text(); - expect(pkg).toContain("@anthropic-ai/sdk"); - }); - test("supports LiteLLM model configuration on the harness path", async () => { const directory = await inTempDirectory(); await run([ From 5247c789d8bb3338e2dc6e778411ca1206105825 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Thu, 3 Sep 2026 04:08:30 +0000 Subject: [PATCH 16/16] refactor(create): drop unreachable runtime-provider guard --- src/handlers/project/create/index.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 038c133c2..083e2de97 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -427,10 +427,6 @@ const MODEL_PROVIDERS: Record< anthropic: { runtime: "Anthropic" }, }; -const RUNTIME_PROVIDER_FLAGS = (Object.keys(MODEL_PROVIDERS) as ModelProviderFlag[]).filter( - (flag) => MODEL_PROVIDERS[flag].runtime !== undefined, -); - function resolveHarnessModelProvider( providerFlag: ModelProviderFlag | undefined, ): HarnessModelProvider { @@ -446,14 +442,7 @@ function resolveHarnessModelProvider( function resolveRuntimeModelProvider( providerFlag: ModelProviderFlag | undefined, ): ModelProvider | undefined { - if (providerFlag === undefined) return undefined; - const provider = MODEL_PROVIDERS[providerFlag].runtime; - if (provider === undefined) - throw new InputValidationError( - `runtime scaffolding does not support the '${providerFlag}' model provider ` + - `(expected ${RUNTIME_PROVIDER_FLAGS.join(", ")})`, - ); - return provider; + return providerFlag === undefined ? undefined : MODEL_PROVIDERS[providerFlag].runtime; } /** A --container value is either an ECR image URI or a local Dockerfile path. */