From 2d9b936e7bfcde521dc94053222ca4c2fb34f2a9 Mon Sep 17 00:00:00 2001 From: notgitika Date: Thu, 28 May 2026 11:11:42 -0400 Subject: [PATCH 1/7] feat: add Bedrock Mantle API format support for harness model config Add apiFormat field to harness bedrockModelConfig that allows users to select between converse_stream (default Bedrock), responses, or chat_completions (Bedrock Mantle) when creating a harness. - Schema: add BedrockApiFormatSchema enum and apiFormat field to HarnessModelSchema - API types: add apiFormat to BedrockModelConfig interface - Mapper: include apiFormat in bedrockModelConfig when not converse_stream - TUI: add api-format wizard step for bedrock provider (gated behind isPreviewEnabled) - CDK: add BedrockMantleInference/CallWithBearerToken IAM policies when mantle format selected - CLI: add --api-format flag, default model ID to openai.gpt-oss-120b for mantle formats - Validation: reject apiFormat for non-bedrock providers E2E tested against account 998846730471 in ap-southeast-2. --- .../assets.snapshot.test.ts.snap | 2 ++ src/assets/cdk/bin/cdk.ts | 2 ++ src/cli/aws/agentcore-harness.ts | 1 + src/cli/commands/create/harness-action.ts | 4 ++- src/cli/commands/create/harness-validate.ts | 14 +++++++++ .../imperative/deployers/harness-mapper.ts | 3 +- src/cli/primitives/HarnessPrimitive.ts | 14 +++++++-- src/cli/tui/screens/create/useCreateFlow.ts | 1 + .../tui/screens/harness/AddHarnessFlow.tsx | 1 + .../tui/screens/harness/AddHarnessScreen.tsx | 30 ++++++++++++++++++- src/cli/tui/screens/harness/types.ts | 25 +++++++++++++++- .../screens/harness/useAddHarnessWizard.ts | 26 +++++++++++++--- src/schema/schemas/agentcore-project.ts | 6 ++-- src/schema/schemas/primitives/harness.ts | 11 +++++++ src/schema/schemas/primitives/index.ts | 2 ++ 15 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 992e8d19f..93d66ac19 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -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); @@ -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( diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index f2518baf3..987d1234b 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -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); @@ -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( diff --git a/src/cli/aws/agentcore-harness.ts b/src/cli/aws/agentcore-harness.ts index 608e52dde..1a4118938 100644 --- a/src/cli/aws/agentcore-harness.ts +++ b/src/cli/aws/agentcore-harness.ts @@ -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; diff --git a/src/cli/commands/create/harness-action.ts b/src/cli/commands/create/harness-action.ts index 86bdd1161..8bd9b1c7d 100644 --- a/src/cli/commands/create/harness-action.ts +++ b/src/cli/commands/create/harness-action.ts @@ -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'; @@ -12,6 +12,7 @@ export interface CreateHarnessProjectOptions { cwd: string; modelProvider: HarnessModelProvider; modelId: string; + apiFormat?: BedrockApiFormat; apiKeyArn?: string; skipMemory?: boolean; containerUri?: string; @@ -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, diff --git a/src/cli/commands/create/harness-validate.ts b/src/cli/commands/create/harness-validate.ts index f31f01870..c8ef6ed25 100644 --- a/src/cli/commands/create/harness-validate.ts +++ b/src/cli/commands/create/harness-validate.ts @@ -13,6 +13,7 @@ export interface CreateHarnessCliOptions { projectName?: string; modelProvider?: string; modelId?: string; + apiFormat?: string; apiKeyArn?: string; container?: string; noMemory?: boolean; @@ -102,6 +103,19 @@ 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' }; + } + } + // Validate EFS access point ARN/path pairs const efsArns = options.efsAccessPointArn ?? []; const efsPaths = options.efsMountPath ?? []; diff --git a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts index 51a34760f..2788bb686 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts @@ -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 }), diff --git a/src/cli/primitives/HarnessPrimitive.ts b/src/cli/primitives/HarnessPrimitive.ts index 5164d148e..3851eca49 100644 --- a/src/cli/primitives/HarnessPrimitive.ts +++ b/src/cli/primitives/HarnessPrimitive.ts @@ -1,5 +1,6 @@ import { APP_DIR, ConfigIO, type Result, findConfigRoot } from '../../lib'; import type { + BedrockApiFormat, HarnessGatewayOutboundAuth, HarnessModelProvider, HarnessSpec, @@ -27,6 +28,7 @@ export interface AddHarnessOptions { name: string; modelProvider: HarnessModelProvider; modelId: string; + apiFormat?: BedrockApiFormat; apiKeyArn?: string; systemPrompt?: string; skipMemory?: boolean; @@ -151,6 +153,7 @@ export class HarnessPrimitive extends BasePrimitive', 'Harness name (start with letter, alphanumeric + underscores, max 48 chars)') .option('--model-provider ', 'Model provider: bedrock, open_ai, gemini') .option('--model-id ', 'Model ID (e.g., anthropic.claude-3-5-sonnet-20240620-v1:0)') + .option('--api-format ', 'API format for Bedrock: converse_stream, responses, chat_completions') .option('--api-key-arn ', 'API key ARN for non-Bedrock providers') .option('--container ', 'Container image URI or path to a Dockerfile') .option('--no-memory', 'Skip auto-creating memory') @@ -394,6 +398,7 @@ export class HarnessPrimitive extends BasePrimitive 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 })), [] @@ -102,6 +108,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'; @@ -142,6 +149,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), @@ -220,6 +234,7 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A : isAdvancedStep || isToolsSelectStep ? 'Space toggle · Enter confirm · Esc back' : isModelProviderStep || + isApiFormatStep || isMemoryStep || isContainerStep || isNetworkModeStep || @@ -240,6 +255,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 }); } @@ -418,6 +437,15 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A /> )} + {isApiFormatStep && ( + + )} + {isApiKeyArnStep && ( = { name: 'Name', 'model-provider': 'Model provider', + 'api-format': 'API format', 'api-key-arn': 'API key ARN', container: 'Custom environment', 'container-uri': 'Container URI', @@ -114,6 +117,8 @@ export const DEFAULT_MODEL_IDS: Record = { gemini: 'gemini-2.5-flash', }; +export const DEFAULT_BEDROCK_MANTLE_MODEL_ID = 'openai.gpt-oss-120b'; + export const MODEL_PROVIDER_OPTIONS = [ { id: 'bedrock' as const, title: 'Amazon Bedrock', description: `Default: ${DEFAULT_MODEL_IDS.bedrock}` }, { @@ -128,6 +133,24 @@ export const MODEL_PROVIDER_OPTIONS = [ }, ] as const; +export const API_FORMAT_OPTIONS = [ + { + id: 'converse_stream' as const, + title: 'Converse Stream', + description: 'Standard Bedrock Converse API (default)', + }, + { + id: 'responses' as const, + title: 'Responses', + description: 'OpenAI Responses API via Bedrock Mantle', + }, + { + id: 'chat_completions' as const, + title: 'Chat Completions', + description: 'OpenAI Chat Completions API via Bedrock Mantle', + }, +] as const; + export const TRUNCATION_STRATEGY_OPTIONS = [ { id: 'sliding_window' as const, title: 'Sliding window', description: 'Keep most recent messages' }, { id: 'summarization' as const, title: 'Summarization', description: 'Compress older context' }, diff --git a/src/cli/tui/screens/harness/useAddHarnessWizard.ts b/src/cli/tui/screens/harness/useAddHarnessWizard.ts index 0d3da4a48..f3a275dcc 100644 --- a/src/cli/tui/screens/harness/useAddHarnessWizard.ts +++ b/src/cli/tui/screens/harness/useAddHarnessWizard.ts @@ -1,8 +1,9 @@ -import type { HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; +import type { BedrockApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; +import { isPreviewEnabled } from '../../../feature-flags'; import type { JwtConfig } from '../../components/jwt-config'; import { HARNESS_FILESYSTEM_STEP_NAMES, useFilesystemMountState } from '../../hooks/useFilesystemMountState'; import type { AddHarnessConfig, AddHarnessStep, AdvancedSetting, ContainerMode } from './types'; -import { DEFAULT_MODEL_IDS } from './types'; +import { DEFAULT_BEDROCK_MANTLE_MODEL_ID, DEFAULT_MODEL_IDS } from './types'; import { useCallback, useMemo, useState } from 'react'; const ADVANCED_SETTING_ORDER: AdvancedSetting[] = [ @@ -57,6 +58,10 @@ export function useAddHarnessWizard() { const allSteps = useMemo(() => { const steps: AddHarnessStep[] = ['name', 'model-provider']; + if (config.modelProvider === 'bedrock' && isPreviewEnabled()) { + steps.push('api-format'); + } + if (config.modelProvider !== 'bedrock') { steps.push('api-key-arn'); } @@ -249,14 +254,26 @@ export function useAddHarnessWizard() { ); const setModelProvider = useCallback((modelProvider: HarnessModelProvider) => { - setConfig(c => ({ ...c, modelProvider, modelId: DEFAULT_MODEL_IDS[modelProvider] })); - if (modelProvider !== 'bedrock') { + setConfig(c => ({ ...c, modelProvider, modelId: DEFAULT_MODEL_IDS[modelProvider], apiFormat: undefined })); + if (modelProvider === 'bedrock' && isPreviewEnabled()) { + setStep('api-format'); + } else if (modelProvider !== 'bedrock') { setStep('api-key-arn'); } else { setStep('container'); } }, []); + const setApiFormat = useCallback((apiFormat: BedrockApiFormat) => { + const isMantle = apiFormat !== 'converse_stream'; + setConfig(c => ({ + ...c, + apiFormat: isMantle ? apiFormat : undefined, + modelId: isMantle ? DEFAULT_BEDROCK_MANTLE_MODEL_ID : DEFAULT_MODEL_IDS.bedrock, + })); + setStep('container'); + }, []); + const setApiKeyArn = useCallback( (apiKeyArn: string) => { setConfig(c => ({ ...c, apiKeyArn })); @@ -532,6 +549,7 @@ export function useAddHarnessWizard() { goBack, setName, setModelProvider, + setApiFormat, setApiKeyArn, setContainerMode, setContainerUri, diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 4ef4458a6..95732ef40 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -79,19 +79,21 @@ export { ABTestModeSchema, TargetRefSchema, GatewayFilterSchema } from './primit export type { HttpGatewayTarget } from './primitives/http-gateway'; export { HttpGatewayTargetSchema } from './primitives/http-gateway'; export type { + BedrockApiFormat, HarnessGatewayOutboundAuth, HarnessMemoryRef, HarnessModel, - HarnessSpec, HarnessModelProvider, + HarnessSpec, } from './primitives/harness'; export { + BedrockApiFormatSchema, GatewayOAuthGrantTypeSchema, HarnessGatewayOutboundAuthSchema, + HarnessModelProviderSchema, HarnessNameSchema, HarnessSpecSchema, HarnessToolTypeSchema, - HarnessModelProviderSchema, } from './primitives/harness'; // ============================================================================ diff --git a/src/schema/schemas/primitives/harness.ts b/src/schema/schemas/primitives/harness.ts index 132c0c0c4..ddd6d1e07 100644 --- a/src/schema/schemas/primitives/harness.ts +++ b/src/schema/schemas/primitives/harness.ts @@ -30,11 +30,15 @@ export const HarnessNameSchema = z export const HarnessModelProviderSchema = z.enum(['bedrock', 'open_ai', 'gemini']); export type HarnessModelProvider = z.infer; +export const BedrockApiFormatSchema = z.enum(['converse_stream', 'responses', 'chat_completions']); +export type BedrockApiFormat = z.infer; + export const HarnessModelSchema = z .object({ provider: HarnessModelProviderSchema, modelId: z.string().min(1, 'Model ID is required'), apiKeyArn: z.string().optional(), + apiFormat: BedrockApiFormatSchema.optional(), temperature: z.number().min(0).max(2).optional(), topP: z.number().min(0).max(1).optional(), topK: z.number().min(0).max(1).optional(), @@ -48,6 +52,13 @@ export const HarnessModelSchema = z path: ['topK'], }); } + if (model.apiFormat !== undefined && model.provider !== 'bedrock') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'apiFormat is only supported for the "bedrock" provider', + path: ['apiFormat'], + }); + } }); export type HarnessModel = z.infer; diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index b25fe3666..41687b973 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -72,6 +72,7 @@ export { } from './policy'; export type { + BedrockApiFormat, HarnessGatewayOutboundAuth, HarnessMemoryRef, HarnessModel, @@ -83,6 +84,7 @@ export type { } from './harness'; export { AllowedToolSchema, + BedrockApiFormatSchema, GatewayOAuthGrantTypeSchema, HarnessGatewayOutboundAuthSchema, HarnessMemoryRefSchema, From 3029d2a42e37baf34ba89d45d2b4b428cc42f101 Mon Sep 17 00:00:00 2001 From: notgitika Date: Thu, 28 May 2026 15:29:59 -0400 Subject: [PATCH 2/7] test: add unit tests for Bedrock Mantle apiFormat support - Schema: accepts/rejects apiFormat for bedrock/non-bedrock providers - Mapper: verifies apiFormat included in bedrockModelConfig, omitted for converse_stream - Validate: tests CLI --api-format flag validation --- .../create/__tests__/harness-validate.test.ts | 46 ++++++++++++++++++ .../__tests__/harness-mapper.test.ts | 45 +++++++++++++++++ .../primitives/__tests__/harness.test.ts | 48 +++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/src/cli/commands/create/__tests__/harness-validate.test.ts b/src/cli/commands/create/__tests__/harness-validate.test.ts index 3e1cd5898..227f8176e 100644 --- a/src/cli/commands/create/__tests__/harness-validate.test.ts +++ b/src/cli/commands/create/__tests__/harness-validate.test.ts @@ -26,6 +26,52 @@ 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('rejects apiFormat for non-bedrock 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(false); + expect(result.error).toContain('only supported for the bedrock provider'); + }); + + it('passes when apiFormat is not specified', () => { + const result = validateCreateHarnessOptions(baseOptions, makeCwd()); + expect(result.valid).toBe(true); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // EFS access point validation // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts index ab9c0bb1e..5ffaaaea0 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts @@ -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: { diff --git a/src/schema/schemas/primitives/__tests__/harness.test.ts b/src/schema/schemas/primitives/__tests__/harness.test.ts index 8ec96a1c8..0faacbabf 100644 --- a/src/schema/schemas/primitives/__tests__/harness.test.ts +++ b/src/schema/schemas/primitives/__tests__/harness.test.ts @@ -691,4 +691,52 @@ describe('HarnessSpecSchema', () => { }); expect(result.success).toBe(true); }); + + it('accepts bedrock model with apiFormat responses', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { provider: 'bedrock', modelId: 'openai.gpt-oss-120b', apiFormat: 'responses' }, + }); + expect(result.success).toBe(true); + }); + + it('accepts bedrock model with apiFormat chat_completions', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { provider: 'bedrock', modelId: 'openai.gpt-oss-120b', apiFormat: 'chat_completions' }, + }); + expect(result.success).toBe(true); + }); + + it('accepts bedrock model with apiFormat converse_stream', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { provider: 'bedrock', modelId: 'anthropic.claude-v2', apiFormat: 'converse_stream' }, + }); + expect(result.success).toBe(true); + }); + + it('rejects apiFormat for non-bedrock providers', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { + provider: 'open_ai', + modelId: 'gpt-4o', + apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key', + apiFormat: 'responses', + }, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some(i => i.message.includes('apiFormat is only supported'))).toBe(true); + } + }); + + it('rejects invalid apiFormat value', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { provider: 'bedrock', modelId: 'anthropic.claude-v2', apiFormat: 'invalid_format' }, + }); + expect(result.success).toBe(false); + }); }); From f1ab114d0e8b7598f61f439307ebf3baecb3cb75 Mon Sep 17 00:00:00 2001 From: notgitika Date: Tue, 2 Jun 2026 16:22:03 -0400 Subject: [PATCH 3/7] fix: use BedrockApiFormatSchema constants and add apiFormat validation to add harness path Address review feedback: - Replace hardcoded format array with BedrockApiFormatSchema.options in create harness validation - Add upfront apiFormat validation to validateAddHarnessOptions (add harness path) so invalid values fail early instead of falling through to a Zod ConfigValidationError --- src/cli/commands/add/types.ts | 1 + src/cli/commands/add/validate.ts | 15 +++++++++++++++ src/cli/commands/create/harness-validate.ts | 8 ++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index 5ec6e044a..6bb3b95b8 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -97,6 +97,7 @@ export interface AddHarnessCliOptions { name?: string; modelProvider?: string; modelId?: string; + apiFormat?: string; apiKeyArn?: string; container?: string; memory?: boolean; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index cf339257c..9c8f123e7 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -1,6 +1,7 @@ import { ConfigIO, findConfigRoot } from '../../../lib'; import { AgentNameSchema, + BedrockApiFormatSchema, BuildTypeSchema, DatasetNameSchema, DatasetSchemaTypeSchema, @@ -892,6 +893,20 @@ const VALID_HARNESS_TOOLS = [ const VALID_GATEWAY_OUTBOUND_AUTH = ['awsIam', 'none', 'oauth'] as const; export function validateAddHarnessOptions(options: AddHarnessCliOptions): ValidationResult { + if (options.apiFormat) { + const validFormats = BedrockApiFormatSchema.options; + if (!validFormats.includes(options.apiFormat as (typeof validFormats)[number])) { + return { + valid: false, + error: `Invalid API format: ${options.apiFormat}. Use ${validFormats.join(', ')}`, + }; + } + const provider = options.modelProvider ?? 'bedrock'; + if (provider !== 'bedrock') { + return { valid: false, error: '--api-format is only supported for the bedrock provider' }; + } + } + if (options.tools) { const toolNames = options.tools.split(',').map(s => s.trim()); for (const tool of toolNames) { diff --git a/src/cli/commands/create/harness-validate.ts b/src/cli/commands/create/harness-validate.ts index c8ef6ed25..62d296048 100644 --- a/src/cli/commands/create/harness-validate.ts +++ b/src/cli/commands/create/harness-validate.ts @@ -1,4 +1,4 @@ -import { MAX_EFS_MOUNTS, MAX_S3_MOUNTS } from '../../../schema'; +import { BedrockApiFormatSchema, MAX_EFS_MOUNTS, MAX_S3_MOUNTS } from '../../../schema'; import { HarnessNameSchema, ProjectNameSchema } from '../../../schema'; import { validateAccessPointMounts, @@ -104,11 +104,11 @@ export function validateCreateHarnessOptions(options: CreateHarnessCliOptions, c } if (options.apiFormat) { - const validFormats = ['converse_stream', 'responses', 'chat_completions']; - if (!validFormats.includes(options.apiFormat)) { + const validFormats = BedrockApiFormatSchema.options; + if (!validFormats.includes(options.apiFormat as (typeof validFormats)[number])) { return { valid: false, - error: `Invalid API format: ${options.apiFormat}. Use converse_stream, responses, or chat_completions`, + error: `Invalid API format: ${options.apiFormat}. Use ${validFormats.join(', ')}`, }; } if (options.modelProvider !== 'bedrock') { From 7f867689c90aaa7176f4ff292b57e82e81d80281 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 8 Jun 2026 14:41:57 -0400 Subject: [PATCH 4/7] feat: extend apiFormat support to OpenAI provider The Loopy service Smithy model defines HarnessOpenAiApiFormat with responses and chat_completions values. This change extends the CLI to support apiFormat for the open_ai provider in addition to bedrock. - Add OpenAiApiFormatSchema (responses | chat_completions) and HarnessApiFormatSchema (union of all formats) to schema - Update HarnessModelSchema superRefine to validate per-provider formats - Add apiFormat field to OpenAiModelConfig API type - Update deploy mapper to pass apiFormat in openAiModelConfig - Update CLI validation (create + add) to accept apiFormat for open_ai - Update TUI wizard to show format step for open_ai with correct options - Fix default model ID logic: only use Bedrock Mantle model ID for bedrock provider, not open_ai - Add tests for OpenAI apiFormat in validation and mapper --- src/cli/aws/agentcore-harness.ts | 1 + src/cli/commands/add/validate.ts | 28 +++++++++--- .../create/__tests__/harness-validate.test.ts | 44 ++++++++++++++++++- src/cli/commands/create/harness-action.ts | 4 +- src/cli/commands/create/harness-validate.ts | 34 +++++++++++--- .../__tests__/harness-mapper.test.ts | 44 +++++++++++++++++++ .../imperative/deployers/harness-mapper.ts | 1 + src/cli/primitives/HarnessPrimitive.ts | 18 +++++--- .../tui/screens/harness/AddHarnessScreen.tsx | 16 ++++--- src/cli/tui/screens/harness/types.ts | 21 +++++++-- .../screens/harness/useAddHarnessWizard.ts | 23 ++++++---- src/schema/schemas/agentcore-project.ts | 4 ++ src/schema/schemas/primitives/harness.ts | 29 +++++++++--- src/schema/schemas/primitives/index.ts | 4 ++ 14 files changed, 225 insertions(+), 46 deletions(-) diff --git a/src/cli/aws/agentcore-harness.ts b/src/cli/aws/agentcore-harness.ts index 1a4118938..55f321f43 100644 --- a/src/cli/aws/agentcore-harness.ts +++ b/src/cli/aws/agentcore-harness.ts @@ -28,6 +28,7 @@ export interface BedrockModelConfig { export interface OpenAiModelConfig { modelId: string; apiKeyArn?: string; + apiFormat?: 'responses' | 'chat_completions'; temperature?: number; topP?: number; maxTokens?: number; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 9c8f123e7..16bdb10c4 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -8,7 +8,9 @@ import { GatewayAuthorizerTypeSchema, GatewayExceptionLevelSchema, GatewayNameSchema, + HarnessApiFormatSchema, ModelProviderSchema, + OpenAiApiFormatSchema, ProtocolModeSchema, RuntimeAuthorizerTypeSchema, SDKFrameworkSchema, @@ -894,16 +896,32 @@ const VALID_GATEWAY_OUTBOUND_AUTH = ['awsIam', 'none', 'oauth'] as const; export function validateAddHarnessOptions(options: AddHarnessCliOptions): ValidationResult { if (options.apiFormat) { - const validFormats = BedrockApiFormatSchema.options; - if (!validFormats.includes(options.apiFormat as (typeof validFormats)[number])) { + const allFormats = HarnessApiFormatSchema.options; + if (!allFormats.includes(options.apiFormat as (typeof allFormats)[number])) { return { valid: false, - error: `Invalid API format: ${options.apiFormat}. Use ${validFormats.join(', ')}`, + error: `Invalid API format: ${options.apiFormat}. Use ${allFormats.join(', ')}`, }; } const provider = options.modelProvider ?? 'bedrock'; - if (provider !== 'bedrock') { - return { valid: false, error: '--api-format is only supported for the bedrock provider' }; + if (provider === 'bedrock') { + const bedrockFormats = BedrockApiFormatSchema.options; + if (!bedrockFormats.includes(options.apiFormat as (typeof bedrockFormats)[number])) { + return { + valid: false, + error: `Invalid API format for bedrock: ${options.apiFormat}. Use ${bedrockFormats.join(', ')}`, + }; + } + } else if (provider === 'open_ai') { + const openAiFormats = OpenAiApiFormatSchema.options; + if (!openAiFormats.includes(options.apiFormat as (typeof openAiFormats)[number])) { + return { + valid: false, + error: `Invalid API format for open_ai: ${options.apiFormat}. Use ${openAiFormats.join(', ')}`, + }; + } + } else { + return { valid: false, error: '--api-format is only supported for bedrock and open_ai providers' }; } } diff --git a/src/cli/commands/create/__tests__/harness-validate.test.ts b/src/cli/commands/create/__tests__/harness-validate.test.ts index 227f8176e..61ab2d87c 100644 --- a/src/cli/commands/create/__tests__/harness-validate.test.ts +++ b/src/cli/commands/create/__tests__/harness-validate.test.ts @@ -52,7 +52,7 @@ describe('validateCreateHarnessOptions - apiFormat', () => { expect(result.error).toContain('Invalid API format'); }); - it('rejects apiFormat for non-bedrock provider', () => { + it('accepts responses format for open_ai provider', () => { const result = validateCreateHarnessOptions( { ...baseOptions, @@ -62,8 +62,48 @@ describe('validateCreateHarnessOptions - apiFormat', () => { }, 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 the bedrock provider'); + expect(result.error).toContain('only supported for bedrock and open_ai'); }); it('passes when apiFormat is not specified', () => { diff --git a/src/cli/commands/create/harness-action.ts b/src/cli/commands/create/harness-action.ts index 8bd9b1c7d..3354ae641 100644 --- a/src/cli/commands/create/harness-action.ts +++ b/src/cli/commands/create/harness-action.ts @@ -1,5 +1,5 @@ import { CONFIG_DIR } from '../../../lib'; -import type { BedrockApiFormat, 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'; @@ -12,7 +12,7 @@ export interface CreateHarnessProjectOptions { cwd: string; modelProvider: HarnessModelProvider; modelId: string; - apiFormat?: BedrockApiFormat; + apiFormat?: HarnessApiFormat; apiKeyArn?: string; skipMemory?: boolean; containerUri?: string; diff --git a/src/cli/commands/create/harness-validate.ts b/src/cli/commands/create/harness-validate.ts index 62d296048..d0968f303 100644 --- a/src/cli/commands/create/harness-validate.ts +++ b/src/cli/commands/create/harness-validate.ts @@ -1,4 +1,10 @@ -import { BedrockApiFormatSchema, MAX_EFS_MOUNTS, MAX_S3_MOUNTS } from '../../../schema'; +import { + BedrockApiFormatSchema, + HarnessApiFormatSchema, + MAX_EFS_MOUNTS, + MAX_S3_MOUNTS, + OpenAiApiFormatSchema, +} from '../../../schema'; import { HarnessNameSchema, ProjectNameSchema } from '../../../schema'; import { validateAccessPointMounts, @@ -104,15 +110,31 @@ export function validateCreateHarnessOptions(options: CreateHarnessCliOptions, c } if (options.apiFormat) { - const validFormats = BedrockApiFormatSchema.options; - if (!validFormats.includes(options.apiFormat as (typeof validFormats)[number])) { + const allFormats = HarnessApiFormatSchema.options; + if (!allFormats.includes(options.apiFormat as (typeof allFormats)[number])) { return { valid: false, - error: `Invalid API format: ${options.apiFormat}. Use ${validFormats.join(', ')}`, + error: `Invalid API format: ${options.apiFormat}. Use ${allFormats.join(', ')}`, }; } - if (options.modelProvider !== 'bedrock') { - return { valid: false, error: '--api-format is only supported for the bedrock provider' }; + if (options.modelProvider === 'bedrock') { + const bedrockFormats = BedrockApiFormatSchema.options; + if (!bedrockFormats.includes(options.apiFormat as (typeof bedrockFormats)[number])) { + return { + valid: false, + error: `Invalid API format for bedrock: ${options.apiFormat}. Use ${bedrockFormats.join(', ')}`, + }; + } + } else if (options.modelProvider === 'open_ai') { + const openAiFormats = OpenAiApiFormatSchema.options; + if (!openAiFormats.includes(options.apiFormat as (typeof openAiFormats)[number])) { + return { + valid: false, + error: `Invalid API format for open_ai: ${options.apiFormat}. Use ${openAiFormats.join(', ')}`, + }; + } + } else { + return { valid: false, error: '--api-format is only supported for bedrock and open_ai providers' }; } } diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts index 5ffaaaea0..bd9ec876f 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts @@ -132,6 +132,50 @@ describe('mapHarnessSpecToCreateOptions', () => { }); }); + 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: { diff --git a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts index 2788bb686..165cc5d19 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts @@ -171,6 +171,7 @@ function mapModel(model: HarnessSpec['model']): HarnessModelConfiguration { openAiModelConfig: { modelId, ...(apiKeyArn && { apiKeyArn }), + ...(apiFormat && apiFormat !== 'responses' && { apiFormat: apiFormat as 'responses' | 'chat_completions' }), ...(temperature !== undefined && { temperature }), ...(topP !== undefined && { topP }), ...(maxTokens !== undefined && { maxTokens }), diff --git a/src/cli/primitives/HarnessPrimitive.ts b/src/cli/primitives/HarnessPrimitive.ts index 3851eca49..cf5acd083 100644 --- a/src/cli/primitives/HarnessPrimitive.ts +++ b/src/cli/primitives/HarnessPrimitive.ts @@ -1,6 +1,6 @@ import { APP_DIR, ConfigIO, type Result, findConfigRoot } from '../../lib'; import type { - BedrockApiFormat, + HarnessApiFormat, HarnessGatewayOutboundAuth, HarnessModelProvider, HarnessSpec, @@ -28,7 +28,7 @@ export interface AddHarnessOptions { name: string; modelProvider: HarnessModelProvider; modelId: string; - apiFormat?: BedrockApiFormat; + apiFormat?: HarnessApiFormat; apiKeyArn?: string; systemPrompt?: string; skipMemory?: boolean; @@ -352,7 +352,10 @@ export class HarnessPrimitive extends BasePrimitive', 'Harness name (start with letter, alphanumeric + underscores, max 48 chars)') .option('--model-provider ', 'Model provider: bedrock, open_ai, gemini') .option('--model-id ', 'Model ID (e.g., anthropic.claude-3-5-sonnet-20240620-v1:0)') - .option('--api-format ', 'API format for Bedrock: converse_stream, responses, chat_completions') + .option( + '--api-format ', + 'API format: converse_stream, responses, chat_completions (bedrock); responses, chat_completions (open_ai)' + ) .option('--api-key-arn ', 'API key ARN for non-Bedrock providers') .option('--container ', 'Container image URI or path to a Dockerfile') .option('--no-memory', 'Skip auto-creating memory') @@ -466,10 +469,11 @@ export class HarnessPrimitive extends BasePrimitive API_FORMAT_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })), - [] + () => + (wizard.config.modelProvider === 'open_ai' ? OPENAI_API_FORMAT_OPTIONS : BEDROCK_API_FORMAT_OPTIONS).map(opt => ({ + id: opt.id, + title: opt.title, + description: opt.description, + })), + [wizard.config.modelProvider] ); const containerModeItems: SelectableItem[] = useMemo( @@ -151,7 +157,7 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A const apiFormatNav = useListNavigation({ items: apiFormatItems, - onSelect: item => wizard.setApiFormat(BedrockApiFormatSchema.parse(item.id)), + onSelect: item => wizard.setApiFormat(HarnessApiFormatSchema.parse(item.id)), onExit: () => wizard.goBack(), isActive: isApiFormatStep, }); diff --git a/src/cli/tui/screens/harness/types.ts b/src/cli/tui/screens/harness/types.ts index 15bf0fc9c..35a3bdc9b 100644 --- a/src/cli/tui/screens/harness/types.ts +++ b/src/cli/tui/screens/harness/types.ts @@ -1,4 +1,4 @@ -import type { BedrockApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; +import type { HarnessApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; import type { JwtConfig } from '../../components/jwt-config'; export type ContainerMode = 'none' | 'uri' | 'dockerfile'; @@ -44,7 +44,7 @@ export interface AddHarnessConfig { name: string; modelProvider: HarnessModelProvider; modelId: string; - apiFormat?: BedrockApiFormat; + apiFormat?: HarnessApiFormat; apiKeyArn?: string; skipMemory?: boolean; containerMode?: ContainerMode; @@ -133,7 +133,7 @@ export const MODEL_PROVIDER_OPTIONS = [ }, ] as const; -export const API_FORMAT_OPTIONS = [ +export const BEDROCK_API_FORMAT_OPTIONS = [ { id: 'converse_stream' as const, title: 'Converse Stream', @@ -151,6 +151,21 @@ export const API_FORMAT_OPTIONS = [ }, ] as const; +export const OPENAI_API_FORMAT_OPTIONS = [ + { + id: 'responses' as const, + title: 'Responses', + description: 'OpenAI Responses API (default)', + }, + { + id: 'chat_completions' as const, + title: 'Chat Completions', + description: 'OpenAI Chat Completions API', + }, +] as const; + +export const API_FORMAT_OPTIONS = BEDROCK_API_FORMAT_OPTIONS; + export const TRUNCATION_STRATEGY_OPTIONS = [ { id: 'sliding_window' as const, title: 'Sliding window', description: 'Keep most recent messages' }, { id: 'summarization' as const, title: 'Summarization', description: 'Compress older context' }, diff --git a/src/cli/tui/screens/harness/useAddHarnessWizard.ts b/src/cli/tui/screens/harness/useAddHarnessWizard.ts index f3a275dcc..f181ec8b6 100644 --- a/src/cli/tui/screens/harness/useAddHarnessWizard.ts +++ b/src/cli/tui/screens/harness/useAddHarnessWizard.ts @@ -1,4 +1,4 @@ -import type { BedrockApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; +import type { HarnessApiFormat, HarnessModelProvider, NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; import { isPreviewEnabled } from '../../../feature-flags'; import type { JwtConfig } from '../../components/jwt-config'; import { HARNESS_FILESYSTEM_STEP_NAMES, useFilesystemMountState } from '../../hooks/useFilesystemMountState'; @@ -58,7 +58,7 @@ export function useAddHarnessWizard() { const allSteps = useMemo(() => { const steps: AddHarnessStep[] = ['name', 'model-provider']; - if (config.modelProvider === 'bedrock' && isPreviewEnabled()) { + if ((config.modelProvider === 'bedrock' || config.modelProvider === 'open_ai') && isPreviewEnabled()) { steps.push('api-format'); } @@ -264,13 +264,18 @@ export function useAddHarnessWizard() { } }, []); - const setApiFormat = useCallback((apiFormat: BedrockApiFormat) => { - const isMantle = apiFormat !== 'converse_stream'; - setConfig(c => ({ - ...c, - apiFormat: isMantle ? apiFormat : undefined, - modelId: isMantle ? DEFAULT_BEDROCK_MANTLE_MODEL_ID : DEFAULT_MODEL_IDS.bedrock, - })); + const setApiFormat = useCallback((apiFormat: HarnessApiFormat) => { + setConfig(c => { + if (c.modelProvider === 'bedrock') { + const isMantle = apiFormat !== 'converse_stream'; + return { + ...c, + apiFormat: isMantle ? apiFormat : undefined, + modelId: isMantle ? DEFAULT_BEDROCK_MANTLE_MODEL_ID : DEFAULT_MODEL_IDS.bedrock, + }; + } + return { ...c, apiFormat }; + }); setStep('container'); }, []); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 95732ef40..fb664e8dd 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -80,14 +80,18 @@ export type { HttpGatewayTarget } from './primitives/http-gateway'; export { HttpGatewayTargetSchema } from './primitives/http-gateway'; export type { BedrockApiFormat, + HarnessApiFormat, HarnessGatewayOutboundAuth, HarnessMemoryRef, HarnessModel, HarnessModelProvider, HarnessSpec, + OpenAiApiFormat, } from './primitives/harness'; export { BedrockApiFormatSchema, + HarnessApiFormatSchema, + OpenAiApiFormatSchema, GatewayOAuthGrantTypeSchema, HarnessGatewayOutboundAuthSchema, HarnessModelProviderSchema, diff --git a/src/schema/schemas/primitives/harness.ts b/src/schema/schemas/primitives/harness.ts index ddd6d1e07..c7307c479 100644 --- a/src/schema/schemas/primitives/harness.ts +++ b/src/schema/schemas/primitives/harness.ts @@ -33,12 +33,18 @@ export type HarnessModelProvider = z.infer; export const BedrockApiFormatSchema = z.enum(['converse_stream', 'responses', 'chat_completions']); export type BedrockApiFormat = z.infer; +export const OpenAiApiFormatSchema = z.enum(['responses', 'chat_completions']); +export type OpenAiApiFormat = z.infer; + +export const HarnessApiFormatSchema = z.enum(['converse_stream', 'responses', 'chat_completions']); +export type HarnessApiFormat = z.infer; + export const HarnessModelSchema = z .object({ provider: HarnessModelProviderSchema, modelId: z.string().min(1, 'Model ID is required'), apiKeyArn: z.string().optional(), - apiFormat: BedrockApiFormatSchema.optional(), + apiFormat: HarnessApiFormatSchema.optional(), temperature: z.number().min(0).max(2).optional(), topP: z.number().min(0).max(1).optional(), topK: z.number().min(0).max(1).optional(), @@ -52,12 +58,21 @@ export const HarnessModelSchema = z path: ['topK'], }); } - if (model.apiFormat !== undefined && model.provider !== 'bedrock') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'apiFormat is only supported for the "bedrock" provider', - path: ['apiFormat'], - }); + if (model.apiFormat !== undefined) { + if (model.provider === 'gemini') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'apiFormat is not supported for the "gemini" provider', + path: ['apiFormat'], + }); + } else if (model.provider === 'open_ai' && model.apiFormat === 'converse_stream') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'converse_stream is not a valid API format for the "open_ai" provider. Use responses or chat_completions', + path: ['apiFormat'], + }); + } } }); diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index 41687b973..d67c6285b 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -73,6 +73,7 @@ export { export type { BedrockApiFormat, + HarnessApiFormat, HarnessGatewayOutboundAuth, HarnessMemoryRef, HarnessModel, @@ -81,10 +82,13 @@ export type { HarnessTool, HarnessToolType, HarnessTruncationConfig, + OpenAiApiFormat, } from './harness'; export { AllowedToolSchema, BedrockApiFormatSchema, + HarnessApiFormatSchema, + OpenAiApiFormatSchema, GatewayOAuthGrantTypeSchema, HarnessGatewayOutboundAuthSchema, HarnessMemoryRefSchema, From 2ecfa6f1a5ed9c442dba2ca545905914e37a94b9 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 8 Jun 2026 16:08:58 -0400 Subject: [PATCH 5/7] test: update schema tests for OpenAI apiFormat support Update HarnessSpecSchema tests to reflect that apiFormat is now valid for the open_ai provider (responses, chat_completions) and only rejected for gemini or when using converse_stream with open_ai. --- .../primitives/__tests__/harness.test.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/schema/schemas/primitives/__tests__/harness.test.ts b/src/schema/schemas/primitives/__tests__/harness.test.ts index 0faacbabf..545f15866 100644 --- a/src/schema/schemas/primitives/__tests__/harness.test.ts +++ b/src/schema/schemas/primitives/__tests__/harness.test.ts @@ -716,7 +716,7 @@ describe('HarnessSpecSchema', () => { expect(result.success).toBe(true); }); - it('rejects apiFormat for non-bedrock providers', () => { + it('accepts apiFormat responses for open_ai provider', () => { const result = HarnessSpecSchema.safeParse({ ...minimalHarness, model: { @@ -726,9 +726,51 @@ describe('HarnessSpecSchema', () => { apiFormat: 'responses', }, }); + expect(result.success).toBe(true); + }); + + it('accepts apiFormat chat_completions for open_ai provider', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { + provider: 'open_ai', + modelId: 'gpt-4o', + apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key', + apiFormat: 'chat_completions', + }, + }); + expect(result.success).toBe(true); + }); + + it('rejects converse_stream for open_ai provider', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { + provider: 'open_ai', + modelId: 'gpt-4o', + apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key', + apiFormat: 'converse_stream', + }, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some(i => i.message.includes('converse_stream is not a valid API format'))).toBe(true); + } + }); + + it('rejects apiFormat for gemini provider', () => { + const result = HarnessSpecSchema.safeParse({ + ...minimalHarness, + model: { + provider: 'gemini', + modelId: 'gemini-2.5-flash', + apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret:key', + apiFormat: 'responses', + }, + }); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('apiFormat is only supported'))).toBe(true); + expect(result.error.issues.some(i => i.message.includes('not supported for the "gemini" provider'))).toBe(true); } }); From 80a2480b8a654518f1d5a338df704b704151bdd8 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 8 Jun 2026 19:29:25 -0400 Subject: [PATCH 6/7] refactor: move apiFormat validation logic to schema Add validateApiFormat() helper in the schema that delegates to HarnessModelSchema.safeParse(), eliminating duplicated provider/format checking in both validateAddHarnessOptions and validateCreateHarnessOptions. The schema's superRefine remains the single source of truth for which providers support which formats. --- src/cli/commands/add/validate.ts | 32 +++--------------- src/cli/commands/create/harness-validate.ts | 36 +++------------------ src/schema/schemas/agentcore-project.ts | 1 + src/schema/schemas/primitives/harness.ts | 22 ++++++++++--- 4 files changed, 27 insertions(+), 64 deletions(-) diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 16bdb10c4..8b509b6e5 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -1,16 +1,13 @@ import { ConfigIO, findConfigRoot } from '../../../lib'; import { AgentNameSchema, - BedrockApiFormatSchema, BuildTypeSchema, DatasetNameSchema, DatasetSchemaTypeSchema, GatewayAuthorizerTypeSchema, GatewayExceptionLevelSchema, GatewayNameSchema, - HarnessApiFormatSchema, ModelProviderSchema, - OpenAiApiFormatSchema, ProtocolModeSchema, RuntimeAuthorizerTypeSchema, SDKFrameworkSchema, @@ -22,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'; @@ -896,32 +894,10 @@ const VALID_GATEWAY_OUTBOUND_AUTH = ['awsIam', 'none', 'oauth'] as const; export function validateAddHarnessOptions(options: AddHarnessCliOptions): ValidationResult { if (options.apiFormat) { - const allFormats = HarnessApiFormatSchema.options; - if (!allFormats.includes(options.apiFormat as (typeof allFormats)[number])) { - return { - valid: false, - error: `Invalid API format: ${options.apiFormat}. Use ${allFormats.join(', ')}`, - }; - } const provider = options.modelProvider ?? 'bedrock'; - if (provider === 'bedrock') { - const bedrockFormats = BedrockApiFormatSchema.options; - if (!bedrockFormats.includes(options.apiFormat as (typeof bedrockFormats)[number])) { - return { - valid: false, - error: `Invalid API format for bedrock: ${options.apiFormat}. Use ${bedrockFormats.join(', ')}`, - }; - } - } else if (provider === 'open_ai') { - const openAiFormats = OpenAiApiFormatSchema.options; - if (!openAiFormats.includes(options.apiFormat as (typeof openAiFormats)[number])) { - return { - valid: false, - error: `Invalid API format for open_ai: ${options.apiFormat}. Use ${openAiFormats.join(', ')}`, - }; - } - } else { - return { valid: false, error: '--api-format is only supported for bedrock and open_ai providers' }; + const formatResult = validateApiFormat(options.apiFormat, provider); + if (!formatResult.valid) { + return { valid: false, error: formatResult.error }; } } diff --git a/src/cli/commands/create/harness-validate.ts b/src/cli/commands/create/harness-validate.ts index d0968f303..feedf0784 100644 --- a/src/cli/commands/create/harness-validate.ts +++ b/src/cli/commands/create/harness-validate.ts @@ -1,10 +1,4 @@ -import { - BedrockApiFormatSchema, - HarnessApiFormatSchema, - MAX_EFS_MOUNTS, - MAX_S3_MOUNTS, - OpenAiApiFormatSchema, -} from '../../../schema'; +import { MAX_EFS_MOUNTS, MAX_S3_MOUNTS, validateApiFormat } from '../../../schema'; import { HarnessNameSchema, ProjectNameSchema } from '../../../schema'; import { validateAccessPointMounts, @@ -110,31 +104,9 @@ export function validateCreateHarnessOptions(options: CreateHarnessCliOptions, c } if (options.apiFormat) { - const allFormats = HarnessApiFormatSchema.options; - if (!allFormats.includes(options.apiFormat as (typeof allFormats)[number])) { - return { - valid: false, - error: `Invalid API format: ${options.apiFormat}. Use ${allFormats.join(', ')}`, - }; - } - if (options.modelProvider === 'bedrock') { - const bedrockFormats = BedrockApiFormatSchema.options; - if (!bedrockFormats.includes(options.apiFormat as (typeof bedrockFormats)[number])) { - return { - valid: false, - error: `Invalid API format for bedrock: ${options.apiFormat}. Use ${bedrockFormats.join(', ')}`, - }; - } - } else if (options.modelProvider === 'open_ai') { - const openAiFormats = OpenAiApiFormatSchema.options; - if (!openAiFormats.includes(options.apiFormat as (typeof openAiFormats)[number])) { - return { - valid: false, - error: `Invalid API format for open_ai: ${options.apiFormat}. Use ${openAiFormats.join(', ')}`, - }; - } - } else { - return { valid: false, error: '--api-format is only supported for bedrock and open_ai providers' }; + const formatResult = validateApiFormat(options.apiFormat, options.modelProvider ?? 'bedrock'); + if (!formatResult.valid) { + return { valid: false, error: formatResult.error }; } } diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index fb664e8dd..648513824 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -98,6 +98,7 @@ export { HarnessNameSchema, HarnessSpecSchema, HarnessToolTypeSchema, + validateApiFormat, } from './primitives/harness'; // ============================================================================ diff --git a/src/schema/schemas/primitives/harness.ts b/src/schema/schemas/primitives/harness.ts index c7307c479..7ebf4c555 100644 --- a/src/schema/schemas/primitives/harness.ts +++ b/src/schema/schemas/primitives/harness.ts @@ -59,17 +59,16 @@ export const HarnessModelSchema = z }); } if (model.apiFormat !== undefined) { - if (model.provider === 'gemini') { + if (model.provider !== 'bedrock' && model.provider !== 'open_ai') { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'apiFormat is not supported for the "gemini" provider', + message: '--api-format is only supported for bedrock and open_ai providers', path: ['apiFormat'], }); } else if (model.provider === 'open_ai' && model.apiFormat === 'converse_stream') { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: - 'converse_stream is not a valid API format for the "open_ai" provider. Use responses or chat_completions', + message: `Invalid API format for open_ai: ${model.apiFormat}. Use ${OpenAiApiFormatSchema.options.join(', ')}`, path: ['apiFormat'], }); } @@ -78,6 +77,21 @@ export const HarnessModelSchema = z export type HarnessModel = z.infer; +export function validateApiFormat( + apiFormat: string, + provider: string +): { valid: true } | { valid: false; error: string } { + const allFormats = HarnessApiFormatSchema.options as readonly string[]; + if (!allFormats.includes(apiFormat)) { + return { valid: false, error: `Invalid API format: ${apiFormat}. Use ${allFormats.join(', ')}` }; + } + const result = HarnessModelSchema.safeParse({ provider, modelId: 'placeholder', apiFormat }); + if (result.success) return { valid: true }; + const apiFormatIssue = result.error.issues.find(i => i.path.includes('apiFormat')); + if (apiFormatIssue) return { valid: false, error: apiFormatIssue.message }; + return { valid: true }; +} + // ============================================================================ // Tool Configuration // ============================================================================ From 8143692a4333e4b1dd17f59a966e5c87bcee7587 Mon Sep 17 00:00:00 2001 From: notgitika Date: Mon, 8 Jun 2026 19:52:28 -0400 Subject: [PATCH 7/7] fix unit test --- .../schemas/primitives/__tests__/harness.test.ts | 4 ++-- src/schema/schemas/primitives/harness.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/schema/schemas/primitives/__tests__/harness.test.ts b/src/schema/schemas/primitives/__tests__/harness.test.ts index 545f15866..4b63fd774 100644 --- a/src/schema/schemas/primitives/__tests__/harness.test.ts +++ b/src/schema/schemas/primitives/__tests__/harness.test.ts @@ -754,7 +754,7 @@ describe('HarnessSpecSchema', () => { }); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('converse_stream is not a valid API format'))).toBe(true); + expect(result.error.issues.some(i => i.message.includes('Invalid API format for open_ai'))).toBe(true); } }); @@ -770,7 +770,7 @@ describe('HarnessSpecSchema', () => { }); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('not supported for the "gemini" provider'))).toBe(true); + expect(result.error.issues.some(i => i.message.includes('only supported for bedrock and open_ai'))).toBe(true); } }); diff --git a/src/schema/schemas/primitives/harness.ts b/src/schema/schemas/primitives/harness.ts index 7ebf4c555..4531774c2 100644 --- a/src/schema/schemas/primitives/harness.ts +++ b/src/schema/schemas/primitives/harness.ts @@ -85,10 +85,18 @@ export function validateApiFormat( if (!allFormats.includes(apiFormat)) { return { valid: false, error: `Invalid API format: ${apiFormat}. Use ${allFormats.join(', ')}` }; } + if (provider !== 'bedrock' && provider !== 'open_ai') { + return { valid: false, error: '--api-format is only supported for bedrock and open_ai providers' }; + } const result = HarnessModelSchema.safeParse({ provider, modelId: 'placeholder', apiFormat }); if (result.success) return { valid: true }; const apiFormatIssue = result.error.issues.find(i => i.path.includes('apiFormat')); - if (apiFormatIssue) return { valid: false, error: apiFormatIssue.message }; + if (apiFormatIssue) { + return { + valid: false, + error: `Invalid API format for ${provider}: ${apiFormat}. Use ${(provider === 'open_ai' ? OpenAiApiFormatSchema : BedrockApiFormatSchema).options.join(', ')}`, + }; + } return { valid: true }; }