Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b675fd4
feat(runtime): support Anthropic, OpenAI, and Gemini model providers
Hweinstock Sep 2, 2026
e9f8141
test(runtime): cover non-Bedrock model provider scaffolding
Hweinstock Sep 2, 2026
7b52f7c
feat(create): support non-Bedrock model providers on the runtime path
Hweinstock Sep 2, 2026
41fbb4b
fix(templates): pull provider SDKs via strands-agents extras
Hweinstock Sep 2, 2026
c364629
refactor(project): resolve model providers via one function and gate …
Hweinstock Sep 2, 2026
be435a9
test(project): consolidate model-provider tests and decouple from tem…
Hweinstock Sep 2, 2026
08116c2
refactor(runtime): inline the Bedrock-only template guard and trim co…
Hweinstock Sep 2, 2026
bb56b17
refactor(create): split model-provider resolution into per-domain fun…
Hweinstock Sep 2, 2026
c200408
refactor(runtime): return provider credentials as spec entries and dr…
Hweinstock Sep 2, 2026
b0a4787
refactor(runtime): make model provider optional and reject it for MCP…
Hweinstock Sep 2, 2026
c50a513
feat(runtime): support LiteLLM as a runtime model provider
Hweinstock Sep 3, 2026
63a6705
refactor(runtime): rename provider template-config type and tighten c…
Hweinstock Sep 3, 2026
50c11d0
fix(templates): install provider npm SDKs for the TypeScript strands …
Hweinstock Sep 3, 2026
acb532b
test(project): cover harness-provider rejection and TypeScript provid…
Hweinstock Sep 3, 2026
b92ae9e
test(project): drop redundant TypeScript provider SDK scaffold test
Hweinstock Sep 3, 2026
5247c78
refactor(create): drop unreachable runtime-provider guard
Hweinstock Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/assets/templates/a2a-python-strands/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"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]
Expand Down
2 changes: 1 addition & 1 deletion src/assets/templates/agent-python-strands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions src/assets/templates/agent-python-strands/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/templates/agent-typescript-strands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
25 changes: 22 additions & 3 deletions src/core/project/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<ProjectEvent, void> {
Expand Down
26 changes: 9 additions & 17 deletions src/core/project/templates/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ 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 } from "./spec";
import type { Template, TemplateRenderer } from "./types";
import type { EnvLocalEntry } from "../../../handlers/project/types";

type CreateProjectConfig = {
assetSource: AssetSource;
Expand All @@ -18,7 +20,7 @@ export async function createProjectTree(
config: CreateProjectConfig,
input: { projectName: string },
options?: { runtime?: ScaffoldRuntimeInput; importBedrockAgent?: ImportBedrockAgentInput },
): Promise<FsTreeNode> {
): Promise<{ tree: FsTreeNode; envEntries: EnvLocalEntry[] }> {
const templates: Template[] = [];
if (options?.runtime) {
const runtimeConfig: RuntimeResourceConfig = {
Expand All @@ -34,7 +36,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"),
),
Expand All @@ -58,20 +62,8 @@ export async function createProjectTree(
templates.map((t) => t.tree),
),
]);

return { tree, envEntries };
}

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 }),
};
}
96 changes: 75 additions & 21 deletions src/core/project/templates/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,40 @@ 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 { 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";
import { toPythonPackageName } from "../fsUtils";

/** 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): ModelProviderTemplateConfig {
const { modelProvider, apiKey } = input.scaffoldRuntimeInput;
if (apiKey === undefined) {
return { templateRenderContext: { identityProviders: [] }, spec: {}, envEntries: [] };
}
const credentialName = `${input.name}${modelProvider}ApiKey`;
const envVarName = credentialEnvVarName(credentialName);
return {
templateRenderContext: { identityProviders: [{ name: credentialName, envVarName }] },
spec: { credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }] },
envEntries: [
{
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 {
Expand Down Expand Up @@ -89,6 +118,11 @@ const importBedrockAgentResolver = () => async (input: RuntimeResourceConfig) =>

const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({
[buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => {
const { modelProvider } = input.scaffoldRuntimeInput;
if (modelProvider !== undefined && 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";
Expand Down Expand Up @@ -121,18 +155,18 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
: [],
);
const memory = input.scaffoldRuntimeInput.memory;
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,
memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [],
hasIdentity: false,
...modelScaffold.templateRenderContext,
hasGateway: false,
hasPayment: false,
isVpc: input.networkMode === "VPC",
identityProviders: [],
gatewayProviders: [],
gatewayAuthTypes: [],
sessionStorageMountPath,
Expand Down Expand Up @@ -160,15 +194,23 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
);
return {
tree,
spec: {
runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }],
...(memory && { memories: [memory] }),
},
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) => {
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
Expand All @@ -179,15 +221,15 @@ 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 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,
memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [],
hasIdentity: false,
identityProviders: [],
...modelScaffold.templateRenderContext,
};
const isContainer = input.scaffoldRuntimeInput.build === "Container";
const tree = await FsTreeNode.fromAssetSource(
Expand All @@ -205,13 +247,19 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
);
return {
tree,
spec: {
runtimes: [{ ...buildRuntimeSpec(input), protocol: "HTTP" as const }],
...(memory && { memories: [memory] }),
},
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) => {
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 ?? [];
Expand Down Expand Up @@ -274,13 +322,15 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
: [],
);
const memory = input.scaffoldRuntimeInput.memory;
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,
memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [],
...modelScaffold.templateRenderContext,
sessionStorageMountPath,
efsMounts,
s3Mounts,
Expand All @@ -307,10 +357,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
);
return {
tree,
spec: {
runtimes: [{ ...buildRuntimeSpec(input), protocol: "A2A" as const }],
...(memory && { memories: [memory] }),
},
spec: mergeSpecEntries([
{
runtimes: [{ ...buildRuntimeSpec(input), protocol: "A2A" as const }],
...(memory && { memories: [memory] }),
},
modelScaffold.spec,
]),
...(modelScaffold.envEntries.length > 0 && { envEntries: modelScaffold.envEntries }),
};
},
});
Expand Down
16 changes: 16 additions & 0 deletions src/core/project/templates/spec.ts
Original file line number Diff line number Diff line change
@@ -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 }),
};
}
Loading
Loading