feat(runtime): support Anthropic, OpenAI, and Gemini model providers - #20
feat(runtime): support Anthropic, OpenAI, and Gemini model providers#20Hweinstock wants to merge 12 commits into
Conversation
35468c0 to
b5d6e88
Compare
| 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"; |
There was a problem hiding this comment.
lets avoid coupling this to the underlying templates. lets just check the agentcore spec, not the scaffolded code.
| }, | ||
| ); | ||
|
|
||
| test("normalizes a lowercase --model-provider to canonical casing", async () => { |
There was a problem hiding this comment.
could this be combined with above by generalizing the test.each slightly.
| ).rejects.toThrow(/API key is required for the Anthropic model provider/); | ||
| }); | ||
|
|
||
| test("rejects a non-Bedrock provider without the strands framework", async () => { |
There was a problem hiding this comment.
this should be a test.each
| ] as const; | ||
|
|
||
| const ModelProviderFlagSchema = z.union([z.literal("Bedrock"), HarnessModelProviderSchema]); | ||
| // Accepts both the harness provider names (bedrock, open_ai, gemini, lite_llm) |
| 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; " + |
There was a problem hiding this comment.
lets accept a consistent format (lowercase) and convert it to the upper case as needed.
| lite_llm: "lite_llm", | ||
| }; | ||
|
|
||
| function resolveHarnessModelProvider(value: ModelProviderFlag | undefined): HarnessModelProvider { |
There was a problem hiding this comment.
why can't there a be a single resolveModelProvider function?
| }); | ||
|
|
||
| test("rejects non-Bedrock model providers on the runtime path", async () => { | ||
| test("rejects lite_llm on the runtime path", async () => { |
There was a problem hiding this comment.
can we make this a test.each with a single entry of lite_llm?
| expect(existsSync(join(directory, "MyProject"))).toBe(false); | ||
| }); | ||
|
|
||
| test("scaffolds a runtime with an OpenAI API-key credential", async () => { |
There was a problem hiding this comment.
this should be a test.each for all of the non-bedrock providers.
| path: ["apiKey"], | ||
| }); | ||
| } | ||
| if (modelProvider !== "Bedrock" && framework !== "strands") { |
There was a problem hiding this comment.
lets remove this. this check should happen when resolving the template. I.e. if I get passed a custom model provider in a template that doesn't support, we should reject, not upfront here.
b5d6e88 to
1425fec
Compare
f0e9909 to
605088d
Compare
| export const MODEL_PROVIDERS = ["Bedrock", "Anthropic", "OpenAI", "Gemini"] as const; | ||
| export type ModelProvider = (typeof MODEL_PROVIDERS)[number]; | ||
|
|
||
| const MODEL_PROVIDER_ALIASES: Record<string, ModelProvider> = { |
There was a problem hiding this comment.
should this be moved to shortcuts?
| * 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; |
There was a problem hiding this comment.
should be a const enum, then we can use it for the definitions below?
| }); | ||
|
|
||
| test("rejects non-Bedrock model providers on the runtime path", async () => { | ||
| test.each([["lite_llm"]])("rejects the %s provider on the runtime path", async (provider) => { |
There was a problem hiding this comment.
i see lite_llm in the pyproject.toml? Why don't we support lite_llm for the runtime path?
| // 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< |
There was a problem hiding this comment.
we should align the names so that we don't need this. Unless they are written as different values to the agentcore.json spec?
| ] as const; | ||
|
|
||
| const ModelProviderFlagSchema = z.union([z.literal("Bedrock"), HarnessModelProviderSchema]); | ||
| const ModelProviderFlagSchema = z.enum([...HarnessModelProviderSchema.options, "anthropic"]); |
There was a problem hiding this comment.
does this mean harness doesn't support anthropic?
| ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); | ||
| }); | ||
|
|
||
| // The flag value casing varies deliberately (lowercase and canonical both |
| * 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 { |
There was a problem hiding this comment.
just inline this, and remove the comment.
| import { toPythonPackageName } from "../fsUtils"; | ||
|
|
||
| /** | ||
| * The AgentCore Identity wiring a non-Bedrock model provider needs: a Handlebars |
There was a problem hiding this comment.
this is way too verbose.
| 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", |
There was a problem hiding this comment.
can we swap this ~= compatible releases to be consistent?
| const credentialName = `${input.name}${modelProvider}ApiKey`; | ||
| const envVarName = credentialEnvVarName(credentialName); | ||
| return { | ||
| context: { hasIdentity: true, identityProviders: [{ name: credentialName, envVarName }] }, |
There was a problem hiding this comment.
hasIdentity and identityProviders feel redundant. is there a way we can combine into one, then do length checks to determine?
| const envVarName = credentialEnvVarName(credentialName); | ||
| return { | ||
| context: { hasIdentity: true, identityProviders: [{ name: credentialName, envVarName }] }, | ||
| credentials: [{ authorizerType: "ApiKeyCredentialProvider", name: credentialName }], |
There was a problem hiding this comment.
could we generalize this to spec, and out credentials under it. Then we re-use the same merge spec logic that we do elsewhere.
|
|
||
| /** AgentCore Identity wiring for a non-Bedrock provider; empty for Bedrock. */ | ||
| type ModelProviderIdentity = { | ||
| context: { hasIdentity: boolean; identityProviders: { name: string; envVarName: string }[] }; |
There was a problem hiding this comment.
can we rename this more specific, like templateRenderContext
| }; | ||
| }, | ||
| [buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { | ||
| if (input.scaffoldRuntimeInput.modelProvider !== "Bedrock") |
There was a problem hiding this comment.
an MCP server, shouldn't even accept a modelProvider. We should make modelProvider optional, validate it exists elsewhere, and reject if its defined here.
| function resolveHarnessModelProvider(value: ModelProviderFlag | undefined): HarnessModelProvider { | ||
| return value === undefined || value === "Bedrock" ? "bedrock" : value; | ||
| } | ||
| // Each accepted flag value maps into whichever domains support it: the harness |
There was a problem hiding this comment.
make this comment more concise. basically the issue is that runtimes and harness support different models, with different names in their spec configs, Therefore, we map them here behind a consistent interface.
| }; | ||
|
|
||
| function resolveRuntimeModelProvider( | ||
| function resolveModelProvider( |
There was a problem hiding this comment.
ok actually split this back into two functions.
|
|
||
| function resolveRuntimeModelProvider( | ||
| function resolveModelProvider( | ||
| value: ModelProviderFlag | undefined, |
There was a problem hiding this comment.
lets do a more specific name than value.
…templates at resolution
ffa5de4 to
08116c2
Compare
| }; | ||
|
|
||
| /** Combines several {@link SpecEntries} into one, concatenating each resource collection. */ | ||
| export function mergeSpecEntries(entries: SpecEntries[]): SpecEntries { |
There was a problem hiding this comment.
lets move this to a seperate file called spec.ts in the same directory, it does not belong in types.
| }, | ||
| ); | ||
|
|
||
| test.each<[string, string, boolean, RegExp]>([ |
There was a problem hiding this comment.
shouldn't this parameterize over the model provider as well? Then maybe we can just check for InputValidationError to simplify
| if (provider === undefined) | ||
| throw new InputValidationError( | ||
| `runtime scaffolding does not support the '${providerFlag}' model provider ` + | ||
| `(expected bedrock, anthropic, open_ai, or gemini)`, |
There was a problem hiding this comment.
can we get this list from a type or value somewhere rather than hardcoding it? it may diverge.
|
|
||
| /** | ||
| * Model providers the scaffolded runtime code supports. `Bedrock` uses the | ||
| * runtime's IAM credentials; the others authenticate with an API key managed |
| framework: z.enum(["strands", "none"]), | ||
| protocol: ProtocolModeSchema.optional(), | ||
| modelProvider: z.enum(["Bedrock"]), | ||
| // Optional: an MCP runtime has no model provider at all, and other runtimes |
| import { toPythonPackageName } from "../fsUtils"; | ||
|
|
||
| /** | ||
| * A non-Bedrock provider's contributions to a scaffolded runtime: the template |
There was a problem hiding this comment.
make this comment more concise.
|
|
||
| function resolveModelProviderScaffold(input: RuntimeResourceConfig): ModelProviderScaffold { | ||
| const { modelProvider, apiKey } = input.scaffoldRuntimeInput; | ||
| // Only a keyed provider needs identity wiring; Bedrock — and a keyless LiteLLM, |
| * spec entry (its ApiKeyCredentialProvider credential), and the .env.local | ||
| * secret. Empty for Bedrock, which uses the runtime's IAM credentials. | ||
| */ | ||
| type ModelProviderScaffold = { |
There was a problem hiding this comment.
this name should communicate what the comment above is, but currently it doesn't.
Maybe ModelProviderTemplateConfig or something.
Spec
Problem: The refactored Strands templates weren't confirmed to work end-to-end with non-Bedrock model providers. The
--model-provider/--api-keyflags (runtime/index.ts) only acceptedBedrock.Definition of done: pass a non-Bedrock provider (
anthropic,openai/open_ai,gemini) with its API key and get a working agent scaffold — creatable, runnable locally, and deployable/invokable remotely.What changed
--model-providernow acceptsBedrock,Anthropic,OpenAI,Gemini,LiteLLM(case-insensitive;createtakes the lowercaseopen_ai/lite_llmspellings its harness flag already uses). It is optional and defaults to Bedrock; an MCP runtime rejects a provider entirely.--api-key; the resolver registers anApiKeyCredentialProvidercredential, wires the template's identity provider, and writes the key toagentcore/.env.local(keyed bycredentialEnvVarName) — which serves both local dev (LOCAL_DEV=1) and deploy-time provisioning. LiteLLM's key is optional (keyless routes through Bedrock via IAM).strands-agents[anthropic|openai|gemini|litellm]instead of stale direct pins (anthropic ~= 0.30.0broke on modern httpx;openai ~= 1.0.0conflicted withmcp'sanyio).model-id/api-base/params remain export-path-only, at parity with mainline.Verification
A reviewer can reproduce the full loop with the steps below. Uses the compiled binary;
us-east-1.0. Build
Provide API keys (any provider you want to test):
1. Create + scaffold (repeat per provider)
Expected:
agentcore/agentcore.jsoncontainsagentcore/.env.localcontainsAGENTCORE_CREDENTIAL_AGENT_PYTHON_STRANDSANTHROPICAPIKEY='<key>', andapp/agent_python_strands/model/load.pyrenders theAnthropicModelbranch (OpenAI/Gemini →OpenAIModel/GeminiModel).2. Run locally + invoke
Expected: a streamed completion ending in
"text": "PONG"with real token usage — i.e. a live call to the provider using the key from.env.local.3. Deploy + invoke remotely (AWS CLI)
Expected:
statusCode 200and a streamed"text": "PONG"— the deployed agent authenticated via the AgentCore Identity provider provisioned from.env.local. Tear down withaws cloudformation delete-stack --stack-name AgentCore-AnthropicAgent-default(+aws bedrock-agentcore-control delete-api-key-credential-provider --name agent_python_strandsAnthropicApiKey).LiteLLM (no key needed)
"$BIN" project create --name LiteLLMAgent --template agent-python-strands --model-provider lite_llm --skip-gitExpected: a runtime with no credential;
load.pyusesLiteLLMModel(defaultbedrock/..., IAM) andpyproject.tomlpullsstrands-agents[litellm]. Passing--api-keyadditionally provisions a...LiteLLMApiKeycredential.Author's results
All three keyed providers returned
PONGlocally (Anthropic 1119 in-tokens, OpenAI 388, Gemini 450). Anthropic was deployed to a dev account and invoked remotely (statusCode 200); resources torn down.Automated checks
bun test→ 2802 pass / 0 fail (provider cases inadd/runtime/index.test.tsandproject.test.ts);tsc --noEmit,oxlint,prettier --checkall clean.