Skip to content
Merged
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 @@ -113,6 +113,7 @@ async function main() {
apiKeyArn?: string;
efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
}[] = [];
for (const entry of specAny.harnesses ?? []) {
const harnessDir = path.resolve(projectRoot, entry.path);
Expand All @@ -131,6 +132,7 @@ async function main() {
apiKeyArn: harnessSpec.model?.apiKeyArn,
efsAccessPoints: harnessSpec.efsAccessPoints,
s3AccessPoints: harnessSpec.s3AccessPoints,
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 @@ -68,6 +68,7 @@ async function main() {
apiKeyArn?: string;
efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
}[] = [];
for (const entry of specAny.harnesses ?? []) {
const harnessDir = path.resolve(projectRoot, entry.path);
Expand All @@ -86,6 +87,7 @@ async function main() {
apiKeyArn: harnessSpec.model?.apiKeyArn,
efsAccessPoints: harnessSpec.efsAccessPoints,
s3AccessPoints: harnessSpec.s3AccessPoints,
apiFormat: harnessSpec.model?.apiFormat,
});
} catch (err) {
throw new Error(
Expand Down
2 changes: 2 additions & 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 All @@ -27,6 +28,7 @@ export interface BedrockModelConfig {
export interface OpenAiModelConfig {
modelId: string;
apiKeyArn?: string;
apiFormat?: 'responses' | 'chat_completions';
temperature?: number;
topP?: number;
maxTokens?: number;
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/add/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface AddHarnessCliOptions {
name?: string;
modelProvider?: string;
modelId?: string;
apiFormat?: string;
apiKeyArn?: string;
container?: string;
memory?: boolean;
Expand Down
9 changes: 9 additions & 0 deletions src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getSupportedModelProviders,
isValidKmsKeyArn,
matchEnumValue,
validateApiFormat,
} from '../../../schema';
import { ARN_VALIDATION_MESSAGE, isValidArn } from '../shared/arn-utils';
import { validateHeaderAllowlist } from '../shared/header-utils';
Expand Down Expand Up @@ -892,6 +893,14 @@ const VALID_HARNESS_TOOLS = [
const VALID_GATEWAY_OUTBOUND_AUTH = ['awsIam', 'none', 'oauth'] as const;

export function validateAddHarnessOptions(options: AddHarnessCliOptions): ValidationResult {
if (options.apiFormat) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this validation logic live on the schema itself as either a discriminated union or part of the superRefine block? curious if we can get zod to do some of the heavy lifting here.

const provider = options.modelProvider ?? 'bedrock';
const formatResult = validateApiFormat(options.apiFormat, provider);
if (!formatResult.valid) {
return { valid: false, error: formatResult.error };
}
}

Comment thread
notgitika marked this conversation as resolved.
if (options.tools) {
const toolNames = options.tools.split(',').map(s => s.trim());
for (const tool of toolNames) {
Expand Down
86 changes: 86 additions & 0 deletions src/cli/commands/create/__tests__/harness-validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,92 @@ const vpcOptions = {
securityGroups: 'sg-07234e16e36d51629',
};

// ─────────────────────────────────────────────────────────────────────────────
// apiFormat validation
// ─────────────────────────────────────────────────────────────────────────────

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

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

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

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

it('accepts responses format for open_ai provider', () => {
const result = validateCreateHarnessOptions(
{
...baseOptions,
modelProvider: 'open_ai',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'responses',
},
makeCwd()
);
expect(result.valid).toBe(true);
});

it('accepts chat_completions format for open_ai provider', () => {
const result = validateCreateHarnessOptions(
{
...baseOptions,
modelProvider: 'open_ai',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'chat_completions',
},
makeCwd()
);
expect(result.valid).toBe(true);
});

it('rejects converse_stream for open_ai provider', () => {
const result = validateCreateHarnessOptions(
{
...baseOptions,
modelProvider: 'open_ai',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'converse_stream',
},
makeCwd()
);
expect(result.valid).toBe(false);
expect(result.error).toContain('Invalid API format for open_ai');
});

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

it('passes when apiFormat is not specified', () => {
const result = validateCreateHarnessOptions(baseOptions, makeCwd());
expect(result.valid).toBe(true);
});
});

// ─────────────────────────────────────────────────────────────────────────────
// EFS access point validation
// ─────────────────────────────────────────────────────────────────────────────
Expand Down
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 { HarnessApiFormat, 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?: HarnessApiFormat;
apiKeyArn?: string;
skipMemory?: boolean;
containerUri?: string;
Expand Down Expand Up @@ -59,6 +60,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
10 changes: 9 additions & 1 deletion src/cli/commands/create/harness-validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MAX_EFS_MOUNTS, MAX_S3_MOUNTS } from '../../../schema';
import { MAX_EFS_MOUNTS, MAX_S3_MOUNTS, validateApiFormat } from '../../../schema';

Check warning on line 1 in src/cli/commands/create/harness-validate.ts

View workflow job for this annotation

GitHub Actions / lint

'/home/runner/work/agentcore-cli/agentcore-cli/src/schema/index.ts' imported multiple times
import { HarnessNameSchema, ProjectNameSchema } from '../../../schema';

Check warning on line 2 in src/cli/commands/create/harness-validate.ts

View workflow job for this annotation

GitHub Actions / lint

'/home/runner/work/agentcore-cli/agentcore-cli/src/schema/index.ts' imported multiple times
import {
validateAccessPointMounts,
Expand All @@ -13,6 +13,7 @@
projectName?: string;
modelProvider?: string;
modelId?: string;
apiFormat?: string;
apiKeyArn?: string;
container?: string;
noMemory?: boolean;
Expand Down Expand Up @@ -102,6 +103,13 @@
return { valid: false, error: `--api-key-arn is required for ${options.modelProvider} provider` };
}

if (options.apiFormat) {
const formatResult = validateApiFormat(options.apiFormat, options.modelProvider ?? 'bedrock');
if (!formatResult.valid) {
return { valid: false, error: formatResult.error };
}
}

// Validate EFS access point ARN/path pairs
const efsArns = options.efsAccessPointArn ?? [];
const efsPaths = options.efsMountPath ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,95 @@ 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('maps open_ai with apiFormat chat_completions', async () => {
const opts = baseOptions({
harnessSpec: {
name: 'h',
model: {
provider: 'open_ai',
modelId: 'gpt-5',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'chat_completions',
},
tools: [],
skills: [],
} as any,
});
const result = await mapHarnessSpecToCreateOptions(opts);
expect(result.model).toEqual({
openAiModelConfig: {
modelId: 'gpt-5',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'chat_completions',
},
});
});

it('omits apiFormat for open_ai when responses (default)', async () => {
const opts = baseOptions({
harnessSpec: {
name: 'h',
model: {
provider: 'open_ai',
modelId: 'gpt-5',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key',
apiFormat: 'responses',
},
tools: [],
skills: [],
} as any,
});
const result = await mapHarnessSpecToCreateOptions(opts);
expect(result.model).toEqual({
openAiModelConfig: { modelId: 'gpt-5', apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key' },
});
});

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 @@ -153,13 +153,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 All @@ -170,6 +171,7 @@ function mapModel(model: HarnessSpec['model']): HarnessModelConfiguration {
openAiModelConfig: {
modelId,
...(apiKeyArn && { apiKeyArn }),
...(apiFormat && apiFormat !== 'responses' && { apiFormat: apiFormat as 'responses' | 'chat_completions' }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this cast is only necessary because the the validation logic isn't on the schema.

...(temperature !== undefined && { temperature }),
...(topP !== undefined && { topP }),
...(maxTokens !== undefined && { maxTokens }),
Expand Down
Loading
Loading