Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async function main() {
codeLocation?: string;
tools?: { type: string; name: string }[];
apiKeyArn?: string;
apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
}[] = [];
for (const entry of specAny.harnesses ?? []) {
const harnessDir = path.resolve(projectRoot, entry.path);
Expand All @@ -127,6 +128,7 @@ async function main() {
codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
tools: harnessSpec.tools,
apiKeyArn: harnessSpec.model?.apiKeyArn,
apiFormat: harnessSpec.model?.apiFormat,
});
} catch (err) {
throw new Error(
Expand Down
2 changes: 2 additions & 0 deletions src/assets/cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async function main() {
codeLocation?: string;
tools?: { type: string; name: string }[];
apiKeyArn?: string;
apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
}[] = [];
for (const entry of specAny.harnesses ?? []) {
const harnessDir = path.resolve(projectRoot, entry.path);
Expand All @@ -82,6 +83,7 @@ async function main() {
codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
tools: harnessSpec.tools,
apiKeyArn: harnessSpec.model?.apiKeyArn,
apiFormat: harnessSpec.model?.apiFormat,
});
} catch (err) {
throw new Error(
Expand Down
1 change: 1 addition & 0 deletions src/cli/aws/agentcore-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type HarnessStatus = 'CREATING' | 'READY' | 'UPDATING' | 'DELETING' | 'DE

export interface BedrockModelConfig {
modelId: string;
apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
temperature?: number;
topP?: number;
maxTokens?: number;
Expand Down
49 changes: 49 additions & 0 deletions src/cli/commands/create/__tests__/harness-validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { validateCreateHarnessOptions } from '../harness-validate';
import { describe, expect, it } from 'vitest';

describe('validateCreateHarnessOptions', () => {
const validOptions = {
name: 'MyHarness',
modelProvider: 'bedrock',
modelId: 'anthropic.claude-v2',
};

describe('apiFormat validation', () => {
it('accepts valid apiFormat for bedrock provider', () => {
const result = validateCreateHarnessOptions({ ...validOptions, apiFormat: 'responses' });
expect(result.valid).toBe(true);
});

it('accepts chat_completions format', () => {
const result = validateCreateHarnessOptions({ ...validOptions, apiFormat: 'chat_completions' });
expect(result.valid).toBe(true);
});

it('accepts converse_stream format', () => {
const result = validateCreateHarnessOptions({ ...validOptions, apiFormat: 'converse_stream' });
expect(result.valid).toBe(true);
});

it('rejects invalid apiFormat value', () => {
const result = validateCreateHarnessOptions({ ...validOptions, apiFormat: 'invalid_format' });
expect(result.valid).toBe(false);
expect(result.error).toContain('Invalid API format');
});

it('rejects apiFormat for non-bedrock provider', () => {
const result = validateCreateHarnessOptions({
...validOptions,
modelProvider: 'open_ai',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'responses',
});
expect(result.valid).toBe(false);
expect(result.error).toContain('only supported for the bedrock provider');
});

it('passes when apiFormat is not specified', () => {
const result = validateCreateHarnessOptions(validOptions);
expect(result.valid).toBe(true);
});
});
});
4 changes: 3 additions & 1 deletion src/cli/commands/create/harness-action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CONFIG_DIR } from '../../../lib';
import type { HarnessModelProvider, NetworkMode } from '../../../schema';
import type { BedrockApiFormat, HarnessModelProvider, NetworkMode } from '../../../schema';
import { harnessPrimitive } from '../../primitives/registry';
import { type ProgressCallback, createProject } from './action';
import type { CreateResult } from './types';
Expand All @@ -12,6 +12,7 @@ export interface CreateHarnessProjectOptions {
cwd: string;
modelProvider: HarnessModelProvider;
modelId: string;
apiFormat?: BedrockApiFormat;
apiKeyArn?: string;
skipMemory?: boolean;
containerUri?: string;
Expand Down Expand Up @@ -57,6 +58,7 @@ export async function createProjectWithHarness(options: CreateHarnessProjectOpti
name: options.name,
modelProvider: options.modelProvider,
modelId: options.modelId,
apiFormat: options.apiFormat,
apiKeyArn: options.apiKeyArn,
containerUri: options.containerUri,
dockerfilePath: options.dockerfilePath,
Expand Down
14 changes: 14 additions & 0 deletions src/cli/commands/create/harness-validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface CreateHarnessCliOptions {
projectName?: string;
modelProvider?: string;
modelId?: string;
apiFormat?: string;
apiKeyArn?: string;
container?: string;
noMemory?: boolean;
Expand Down Expand Up @@ -90,5 +91,18 @@ export function validateCreateHarnessOptions(options: CreateHarnessCliOptions, c
return { valid: false, error: `--api-key-arn is required for ${options.modelProvider} provider` };
}

if (options.apiFormat) {
const validFormats = ['converse_stream', 'responses', 'chat_completions'];
if (!validFormats.includes(options.apiFormat)) {
return {
valid: false,
error: `Invalid API format: ${options.apiFormat}. Use converse_stream, responses, or chat_completions`,
};
}
if (options.modelProvider !== 'bedrock') {
return { valid: false, error: '--api-format is only supported for the bedrock provider' };
}
}

return { valid: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,51 @@ describe('mapHarnessSpecToCreateOptions', () => {
});
});

it('maps bedrock with apiFormat responses', async () => {
const opts = baseOptions({
harnessSpec: {
name: 'h',
model: { provider: 'bedrock', modelId: 'openai.gpt-oss-120b', apiFormat: 'responses' },
tools: [],
skills: [],
} as any,
});
const result = await mapHarnessSpecToCreateOptions(opts);
expect(result.model).toEqual({
bedrockModelConfig: { modelId: 'openai.gpt-oss-120b', apiFormat: 'responses' },
});
});

it('maps bedrock with apiFormat chat_completions', async () => {
const opts = baseOptions({
harnessSpec: {
name: 'h',
model: { provider: 'bedrock', modelId: 'openai.gpt-oss-120b', apiFormat: 'chat_completions' },
tools: [],
skills: [],
} as any,
});
const result = await mapHarnessSpecToCreateOptions(opts);
expect(result.model).toEqual({
bedrockModelConfig: { modelId: 'openai.gpt-oss-120b', apiFormat: 'chat_completions' },
});
});

it('omits apiFormat when converse_stream (default)', async () => {
const opts = baseOptions({
harnessSpec: {
name: 'h',
model: { provider: 'bedrock', modelId: 'claude', apiFormat: 'converse_stream' },
tools: [],
skills: [],
} as any,
});
const result = await mapHarnessSpecToCreateOptions(opts);
expect(result.model).toEqual({
bedrockModelConfig: { modelId: 'claude' },
});
});

it('includes optional model params when set', async () => {
const opts = baseOptions({
harnessSpec: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,14 @@ export async function mapHarnessSpecToCreateOptions(options: MapHarnessOptions):
// ============================================================================

function mapModel(model: HarnessSpec['model']): HarnessModelConfiguration {
const { provider, modelId, apiKeyArn, temperature, topP, topK, maxTokens } = model;
const { provider, modelId, apiKeyArn, apiFormat, temperature, topP, topK, maxTokens } = model;

switch (provider) {
case 'bedrock':
return {
bedrockModelConfig: {
modelId,
...(apiFormat && apiFormat !== 'converse_stream' && { apiFormat }),
...(temperature !== undefined && { temperature }),
...(topP !== undefined && { topP }),
...(maxTokens !== undefined && { maxTokens }),
Expand Down
14 changes: 12 additions & 2 deletions src/cli/primitives/HarnessPrimitive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APP_DIR, ConfigIO, type Result, findConfigRoot } from '../../lib';
import type {
BedrockApiFormat,
HarnessGatewayOutboundAuth,
HarnessModelProvider,
HarnessSpec,
Expand Down Expand Up @@ -27,6 +28,7 @@ export interface AddHarnessOptions {
name: string;
modelProvider: HarnessModelProvider;
modelId: string;
apiFormat?: BedrockApiFormat;
apiKeyArn?: string;
systemPrompt?: string;
skipMemory?: boolean;
Expand Down Expand Up @@ -149,6 +151,7 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
model: {
provider: options.modelProvider,
modelId: options.modelId,
...(options.apiFormat && { apiFormat: options.apiFormat }),
...(options.apiKeyArn && { apiKeyArn: options.apiKeyArn }),
},
tools,
Expand Down Expand Up @@ -345,6 +348,7 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
.option('--name <name>', 'Harness name (start with letter, alphanumeric + underscores, max 48 chars)')
.option('--model-provider <provider>', 'Model provider: bedrock, open_ai, gemini')
.option('--model-id <id>', 'Model ID (e.g., anthropic.claude-3-5-sonnet-20240620-v1:0)')
.option('--api-format <format>', 'API format for Bedrock: converse_stream, responses, chat_completions')
.option('--api-key-arn <arn>', 'API key ARN for non-Bedrock providers')
.option('--container <uri-or-path>', 'Container image URI or path to a Dockerfile')
.option('--no-memory', 'Skip auto-creating memory')
Expand Down Expand Up @@ -390,6 +394,7 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
name?: string;
modelProvider?: string;
modelId?: string;
apiFormat?: string;
apiKeyArn?: string;
container?: string;
memory?: boolean;
Expand Down Expand Up @@ -454,16 +459,21 @@ export class HarnessPrimitive extends BasePrimitive<AddHarnessOptions, Removable
process.exit(1);
}

const { DEFAULT_MODEL_IDS } = await import('../tui/screens/harness/types');
const { DEFAULT_BEDROCK_MANTLE_MODEL_ID, DEFAULT_MODEL_IDS } =
await import('../tui/screens/harness/types');
const provider = (cliOptions.modelProvider ?? 'bedrock') as HarnessModelProvider;
const modelId = cliOptions.modelId ?? DEFAULT_MODEL_IDS[provider];
const isMantleFormat =
cliOptions.apiFormat === 'responses' || cliOptions.apiFormat === 'chat_completions';
const modelId =
cliOptions.modelId ?? (isMantleFormat ? DEFAULT_BEDROCK_MANTLE_MODEL_ID : DEFAULT_MODEL_IDS[provider]);

const containerOption = this.parseContainerFlag(cliOptions.container);

const result = await this.add({
name: cliOptions.name,
modelProvider: provider,
modelId,
apiFormat: cliOptions.apiFormat as BedrockApiFormat | undefined,
apiKeyArn: cliOptions.apiKeyArn,
containerUri: containerOption.containerUri,
dockerfilePath: containerOption.dockerfilePath,
Expand Down
1 change: 1 addition & 0 deletions src/cli/tui/screens/create/useCreateFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ export function useCreateFlow(cwd: string): CreateFlowState {
name: addHarnessConfig.name,
modelProvider: addHarnessConfig.modelProvider,
modelId: addHarnessConfig.modelId,
apiFormat: addHarnessConfig.apiFormat,
apiKeyArn: addHarnessConfig.apiKeyArn,
skipMemory: addHarnessConfig.skipMemory,
containerUri: addHarnessConfig.containerUri,
Expand Down
1 change: 1 addition & 0 deletions src/cli/tui/screens/harness/AddHarnessFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function AddHarnessFlow({ isInteractive = true, onExit, onBack, onDev, on
name: config.name,
modelProvider: config.modelProvider,
modelId: config.modelId,
apiFormat: config.apiFormat,
apiKeyArn: config.apiKeyArn,
skipMemory: config.skipMemory,
containerUri: config.containerUri,
Expand Down
30 changes: 29 additions & 1 deletion src/cli/tui/screens/harness/AddHarnessScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HarnessModelProvider, RuntimeAuthorizerType } from '../../../../schema';
import { NetworkModeSchema } from '../../../../schema';
import { BedrockApiFormatSchema, NetworkModeSchema } from '../../../../schema';
import { HarnessNameSchema, HarnessTruncationStrategySchema } from '../../../../schema/schemas/primitives/harness';
import { ARN_VALIDATION_MESSAGE, isValidArn } from '../../../commands/shared/arn-utils';
import { computeManagedOAuthCredentialName } from '../../../primitives/credential-utils';
Expand All @@ -20,6 +20,7 @@ import { generateUniqueName } from '../../utils';
import type { AddHarnessConfig, AdvancedSetting, ContainerMode } from './types';
import {
ADVANCED_SETTING_OPTIONS,
API_FORMAT_OPTIONS,
AUTHORIZER_TYPE_OPTIONS,
CONTAINER_MODE_OPTIONS,
GATEWAY_OUTBOUND_AUTH_OPTIONS,
Expand Down Expand Up @@ -52,6 +53,11 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A
[]
);

const apiFormatItems: SelectableItem[] = useMemo(
() => API_FORMAT_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })),
[]
);

const containerModeItems: SelectableItem[] = useMemo(
() => CONTAINER_MODE_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })),
[]
Expand Down Expand Up @@ -94,6 +100,7 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A

const isNameStep = wizard.step === 'name';
const isModelProviderStep = wizard.step === 'model-provider';
const isApiFormatStep = wizard.step === 'api-format';
const isApiKeyArnStep = wizard.step === 'api-key-arn';
const isContainerStep = wizard.step === 'container';
const isContainerUriStep = wizard.step === 'container-uri';
Expand Down Expand Up @@ -128,6 +135,13 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A
isActive: isModelProviderStep,
});

const apiFormatNav = useListNavigation({
items: apiFormatItems,
onSelect: item => wizard.setApiFormat(BedrockApiFormatSchema.parse(item.id)),
onExit: () => wizard.goBack(),
isActive: isApiFormatStep,
});

const containerModeNav = useListNavigation({
items: containerModeItems,
onSelect: item => wizard.setContainerMode(item.id as ContainerMode),
Expand Down Expand Up @@ -206,6 +220,7 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A
: isAdvancedStep || isToolsSelectStep
? 'Space toggle · Enter confirm · Esc back'
: isModelProviderStep ||
isApiFormatStep ||
isMemoryStep ||
isContainerStep ||
isNetworkModeStep ||
Expand All @@ -226,6 +241,10 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A
{ label: 'Model ID', value: wizard.config.modelId },
];

if (wizard.config.apiFormat) {
fields.push({ label: 'API Format', value: wizard.config.apiFormat });
}

if (wizard.config.apiKeyArn) {
fields.push({ label: 'API Key ARN', value: wizard.config.apiKeyArn });
}
Expand Down Expand Up @@ -372,6 +391,15 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A
/>
)}

{isApiFormatStep && (
<WizardSelect
title="Select API format"
description="Choose the API format for model invocation (Responses and ChatCompletions use Bedrock Mantle)"
items={apiFormatItems}
selectedIndex={apiFormatNav.selectedIndex}
/>
)}

{isApiKeyArnStep && (
<TextInput
key="api-key-arn"
Expand Down
Loading
Loading