From 2f921a65518999416abfedf2cbd13db9ebaf3acc Mon Sep 17 00:00:00 2001 From: notgitika Date: Thu, 28 May 2026 11:11:42 -0400 Subject: [PATCH 1/2] 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 bc6e49330..00b39e696 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -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); @@ -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( diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 7969869c3..f5ebdaa6b 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -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); @@ -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( 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 28fdda684..d9903c9be 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; @@ -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, diff --git a/src/cli/commands/create/harness-validate.ts b/src/cli/commands/create/harness-validate.ts index 52f84d6e2..3151f9c72 100644 --- a/src/cli/commands/create/harness-validate.ts +++ b/src/cli/commands/create/harness-validate.ts @@ -6,6 +6,7 @@ export interface CreateHarnessCliOptions { projectName?: string; modelProvider?: string; modelId?: string; + apiFormat?: string; apiKeyArn?: string; container?: string; noMemory?: boolean; @@ -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 }; } diff --git a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts index 273951a91..2f7cb1ad4 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts @@ -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 }), diff --git a/src/cli/primitives/HarnessPrimitive.ts b/src/cli/primitives/HarnessPrimitive.ts index 0c78a0612..b1f6681eb 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; @@ -149,6 +151,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') @@ -390,6 +394,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 })), [] @@ -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'; @@ -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), @@ -206,6 +220,7 @@ export function AddHarnessScreen({ existingHarnessNames, onComplete, onExit }: A : isAdvancedStep || isToolsSelectStep ? 'Space toggle · Enter confirm · Esc back' : isModelProviderStep || + isApiFormatStep || isMemoryStep || isContainerStep || isNetworkModeStep || @@ -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 }); } @@ -372,6 +391,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', @@ -100,6 +103,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}` }, { @@ -114,6 +119,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 13325fe35..cf9f2d2f4 100644 --- a/src/cli/tui/screens/harness/useAddHarnessWizard.ts +++ b/src/cli/tui/screens/harness/useAddHarnessWizard.ts @@ -1,7 +1,8 @@ -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 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[] = [ @@ -56,6 +57,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'); } @@ -154,14 +159,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 })); @@ -424,6 +441,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 accf8055b..27a2d5bb8 100644 --- a/src/schema/schemas/primitives/harness.ts +++ b/src/schema/schemas/primitives/harness.ts @@ -25,11 +25,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(), @@ -43,6 +47,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 e9896c79279802b3651022766e3f8e5c600b0f30 Mon Sep 17 00:00:00 2001 From: notgitika Date: Thu, 28 May 2026 15:29:59 -0400 Subject: [PATCH 2/2] 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 | 49 +++++++++++++++++++ .../__tests__/harness-mapper.test.ts | 45 +++++++++++++++++ .../primitives/__tests__/harness.test.ts | 48 ++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/cli/commands/create/__tests__/harness-validate.test.ts diff --git a/src/cli/commands/create/__tests__/harness-validate.test.ts b/src/cli/commands/create/__tests__/harness-validate.test.ts new file mode 100644 index 000000000..cb9280962 --- /dev/null +++ b/src/cli/commands/create/__tests__/harness-validate.test.ts @@ -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); + }); + }); +}); 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 b3e4faf4d..14648de63 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); + }); });