From 27f5bee3c6b59e93db11d0f71767a21f5e76e18e Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 13:48:52 +0000 Subject: [PATCH 1/3] fix(create): default the LiteLLM harness model to a keyless bedrock model The lite_llm default was anthropic/claude-sonnet-4-5, which routes LiteLLM at Anthropic directly and needs an Anthropic API key. Because apiKeyArn is optional for LiteLLM, a default project deployed READY and then failed its first invoke with a LiteLLM AuthenticationError. A bedrock/ model ID routes LiteLLM at Bedrock, which it signs with SigV4 using the harness execution role, so no API key is required. Derive the default from DEFAULT_HARNESS_MODEL so the bedrock and lite_llm defaults cannot drift. --- src/handlers/project/create/index.ts | 2 +- src/handlers/project/project.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 28514f9d6..207ffcee6 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -76,7 +76,7 @@ const HARNESS_DEFAULT_MODEL_IDS: Record = { bedrock: DEFAULT_HARNESS_MODEL.modelId, open_ai: "gpt-5", gemini: "gemini-2.5-flash", - lite_llm: "anthropic/claude-sonnet-4-5", + lite_llm: `bedrock/${DEFAULT_HARNESS_MODEL.modelId}`, }; export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) => diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 669723379..412b58a51 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -185,7 +185,7 @@ describe("project create", () => { ).json(); expect(harness.model).toEqual({ provider: "lite_llm", - modelId: "anthropic/claude-sonnet-4-5", + modelId: "bedrock/global.anthropic.claude-sonnet-4-6", apiBase: "https://litellm.example.com/v1", additionalParams: { max_retries: 2 }, }); From 0e920b49549b1a2bd8bf48b6e1290ac338d4c7f8 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 13:55:47 +0000 Subject: [PATCH 2/3] fix(create): offer the shared LiteLLM default in the project wizard The wizard declared its own copy of every provider default, so changing the flag path's lite_llm default left the TUI still pre-filling anthropic/claude-sonnet-4-5. That value is form state rather than placeholder text, so pressing through the wizard deployed a harness that reached READY and then failed its first invoke with a LiteLLM AuthenticationError. Export HARNESS_DEFAULT_MODEL_IDS and read it for both the pre-filled value and the placeholder, leaving MODEL_PROVIDERS to describe only labels. The two paths can no longer drift. --- .../project/create/create.screen.test.tsx | 42 +++++++++++++++++++ src/handlers/project/create/index.ts | 2 +- src/handlers/project/create/screen.tsx | 17 +++----- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index 10cb73945..5e178259a 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -22,6 +22,7 @@ import { InputValidationError } from "../../../errors"; import type { AppIO } from "../../../io"; import { resolveRuntimeTemplateShortcut } from "../shortcuts"; import type { CreateProjectInput } from "../types"; +import { HARNESS_DEFAULT_MODEL_IDS } from "./index"; afterEach(cleanupScreens); @@ -220,6 +221,47 @@ describe("project create wizard", () => { r.unmount(); }, 10000); + // The wizard pre-fills a model id, so pressing through it deploys that value. + // It must therefore match the flag path's default rather than a second literal + // — a keyless bedrock/ id, not one that needs an Anthropic key it never asks for. + test("the litellm default offered by the wizard matches the flag path", async () => { + const core = new TestCoreClient(); + const inputs = spyOnCreate(core); + const r = renderScreen("/agentcore/project/create", { core }); + await inTempDirectory(); + + await waitForText(r.lastFrame, "name your project"); + await r.write("LiteLlmDefaultApp"); + await r.press("return"); + await waitForText(r.lastFrame, "what should the project be built around?"); + await r.press("return"); + + await waitForText(r.lastFrame, "choose a model"); + await r.press("down"); + await r.press("down"); + await r.press("down"); + expect(r.lastFrame()).toContain("● litellm"); + + await r.press("return"); // focus model id, pre-filled with the shared default + expect(r.lastFrame()).toContain(HARNESS_DEFAULT_MODEL_IDS.lite_llm); + await r.press("return"); // accept it; api key arn and api base are optional + await r.press("return"); + await r.press("return"); + + await waitForText(r.lastFrame, "this project will be created"); + await r.press("return"); + await waitForText(r.lastFrame, "project created in ./LiteLlmDefaultApp", 5000); + + expect(inputs[0]?.scaffoldHarnessInput?.model).toEqual({ + provider: "lite_llm", + modelId: HARNESS_DEFAULT_MODEL_IDS.lite_llm, + }); + // bedrock/ is the property that makes the default keyless; an anthropic/ id + // would deploy READY and then fail its first invoke. + expect(HARNESS_DEFAULT_MODEL_IDS.lite_llm).toStartWith("bedrock/"); + r.unmount(); + }, 10000); + test("switching providers preserves each provider's model input", async () => { const r = renderScreen("/agentcore/project/create"); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 207ffcee6..77f76e992 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -72,7 +72,7 @@ const HARNESS_ONLY_FLAGS = [ const ModelProviderFlagSchema = z.union([z.literal("Bedrock"), HarnessModelProviderSchema]); type ModelProviderFlag = z.infer; -const HARNESS_DEFAULT_MODEL_IDS: Record = { +export const HARNESS_DEFAULT_MODEL_IDS: Record = { bedrock: DEFAULT_HARNESS_MODEL.modelId, open_ai: "gpt-5", gemini: "gemini-2.5-flash", diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index bb31f661f..3a66eb487 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -6,13 +6,12 @@ import { ProjectNameSchema } from "../../../projectSchemas/project"; import type { HarnessModelProvider } from "../../../projectSchemas/harness"; import type { ScreenProps } from "../../types"; import type { CreateProjectInput } from "../types"; -import { DEFAULT_HARNESS_MODEL } from "../add/harness"; import { resolveRuntimeTemplateShortcut, type MemoryShortcutName, type RuntimeTemplateShortcutName, } from "../shortcuts"; -import { resolveScaffoldHarnessInput } from "./index"; +import { HARNESS_DEFAULT_MODEL_IDS, resolveScaffoldHarnessInput } from "./index"; import { Layout } from "../../../components/Layout"; import { FormTextInput } from "../../../components/FormTextInput"; import { FormRadioGroup, type FormRadioOption } from "../../../components/FormRadioGroup"; @@ -50,35 +49,32 @@ interface CreateProjectFormValues { memory: MemoryShortcutName; } +// defaultModelId is not declared here: the wizard and the flag path must offer +// the same default, so both read HARNESS_DEFAULT_MODEL_IDS. const MODEL_PROVIDERS: { provider: HarnessModelProvider; label: string; description: string; - defaultModelId: string; }[] = [ { provider: "bedrock", label: "bedrock (recommended)", description: "an Amazon Bedrock model or inference profile", - defaultModelId: DEFAULT_HARNESS_MODEL.modelId, }, { provider: "open_ai", label: "openai", description: "an OpenAI model using an API-key credential ARN", - defaultModelId: "gpt-5", }, { provider: "gemini", label: "gemini", description: "a Google Gemini model using an API-key credential ARN", - defaultModelId: "gemini-2.5-flash", }, { provider: "lite_llm", label: "litellm", description: "a third-party provider through LiteLLM", - defaultModelId: "anthropic/claude-sonnet-4-5", }, ]; @@ -86,9 +82,9 @@ function emptyProjectModel(): ProjectModelValues { return { provider: "bedrock", configs: Object.fromEntries( - MODEL_PROVIDERS.map(({ provider, defaultModelId }) => [ + MODEL_PROVIDERS.map(({ provider }) => [ provider, - { modelId: defaultModelId, apiKeyArn: "", apiBase: "" }, + { modelId: HARNESS_DEFAULT_MODEL_IDS[provider], apiKeyArn: "", apiBase: "" }, ]), ) as Record, }; @@ -553,7 +549,6 @@ interface ModelField { } function modelFields(provider: HarnessModelProvider): ModelField[] { - const option = MODEL_PROVIDERS.find((candidate) => candidate.provider === provider)!; const fields: ModelField[] = [ { key: "modelId", @@ -562,7 +557,7 @@ function modelFields(provider: HarnessModelProvider): ModelField[] { provider === "bedrock" ? "a Bedrock model or inference profile id" : `the ${providerLabel(provider)} model to use`, - placeholder: option.defaultModelId, + placeholder: HARNESS_DEFAULT_MODEL_IDS[provider], required: true, requiredError: `enter a model id for ${providerLabel(provider)}`, }, From 4ab1b2b3690357a4ace8818f46155fa5ae52b239 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 2 Sep 2026 20:57:01 +0000 Subject: [PATCH 3/3] test(create): drop the redundant wizard default assertion MODEL_PROVIDERS no longer declares a per-provider default, so the wizard and the flag path read one table and cannot disagree. project.test.ts already pins the value, which is the only remaining way to regress it. --- .../project/create/create.screen.test.tsx | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index 5e178259a..10cb73945 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -22,7 +22,6 @@ import { InputValidationError } from "../../../errors"; import type { AppIO } from "../../../io"; import { resolveRuntimeTemplateShortcut } from "../shortcuts"; import type { CreateProjectInput } from "../types"; -import { HARNESS_DEFAULT_MODEL_IDS } from "./index"; afterEach(cleanupScreens); @@ -221,47 +220,6 @@ describe("project create wizard", () => { r.unmount(); }, 10000); - // The wizard pre-fills a model id, so pressing through it deploys that value. - // It must therefore match the flag path's default rather than a second literal - // — a keyless bedrock/ id, not one that needs an Anthropic key it never asks for. - test("the litellm default offered by the wizard matches the flag path", async () => { - const core = new TestCoreClient(); - const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); - await inTempDirectory(); - - await waitForText(r.lastFrame, "name your project"); - await r.write("LiteLlmDefaultApp"); - await r.press("return"); - await waitForText(r.lastFrame, "what should the project be built around?"); - await r.press("return"); - - await waitForText(r.lastFrame, "choose a model"); - await r.press("down"); - await r.press("down"); - await r.press("down"); - expect(r.lastFrame()).toContain("● litellm"); - - await r.press("return"); // focus model id, pre-filled with the shared default - expect(r.lastFrame()).toContain(HARNESS_DEFAULT_MODEL_IDS.lite_llm); - await r.press("return"); // accept it; api key arn and api base are optional - await r.press("return"); - await r.press("return"); - - await waitForText(r.lastFrame, "this project will be created"); - await r.press("return"); - await waitForText(r.lastFrame, "project created in ./LiteLlmDefaultApp", 5000); - - expect(inputs[0]?.scaffoldHarnessInput?.model).toEqual({ - provider: "lite_llm", - modelId: HARNESS_DEFAULT_MODEL_IDS.lite_llm, - }); - // bedrock/ is the property that makes the default keyless; an anthropic/ id - // would deploy READY and then fail its first invoke. - expect(HARNESS_DEFAULT_MODEL_IDS.lite_llm).toStartWith("bedrock/"); - r.unmount(); - }, 10000); - test("switching providers preserves each provider's model input", async () => { const r = renderScreen("/agentcore/project/create");