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
50 changes: 48 additions & 2 deletions src/cli/operations/dev/web-ui/__tests__/harness-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,60 @@ describe('buildInvokeOptions — model resolution', () => {
expect(opts.skills).toEqual([{ path: './skills' }]);
});

it('sends no default override for non-bedrock providers', () => {
it('defaults an OpenAI model with maxTokens + apiKeyArn (dropping converse_stream apiFormat)', () => {
const openAiModel: HarnessModel = {
provider: 'open_ai',
modelId: 'gpt-4.1',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret/openai',
maxTokens: 2048,
temperature: 0.5,
};
const opts = buildInvokeOptions(ARN, 'us-west-2', 's-1', [], undefined, openAiModel);
expect(opts.model).toEqual({
openAiModelConfig: {
modelId: 'gpt-4.1',
temperature: 0.5,
maxTokens: 2048,
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret/openai',
},
});
});

it('defaults a Gemini model with maxTokens + topK', () => {
const geminiModel: HarnessModel = {
provider: 'gemini',
modelId: 'gemini-2.5-flash',
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret/gemini',
maxTokens: 512,
topK: 40,
};
const opts = buildInvokeOptions(ARN, 'us-west-2', 's-1', [], undefined, geminiModel);
expect(opts.model).toBeUndefined();
expect(opts.model).toEqual({
geminiModelConfig: {
modelId: 'gemini-2.5-flash',
maxTokens: 512,
apiKeyArn: 'arn:aws:secretsmanager:us-east-1:123:secret/gemini',
topK: 40,
},
});
});

it('defaults a LiteLLM model with maxTokens + apiBase + additionalParams', () => {
const liteLlmModel: HarnessModel = {
provider: 'lite_llm',
modelId: 'bedrock/us.anthropic.claude-sonnet-4-5',
maxTokens: 8192,
apiBase: 'https://proxy.example.com',
additionalParams: { foo: 'bar' },
};
const opts = buildInvokeOptions(ARN, 'us-west-2', 's-1', [], undefined, liteLlmModel);
expect(opts.model).toEqual({
liteLlmModelConfig: {
modelId: 'bedrock/us.anthropic.claude-sonnet-4-5',
maxTokens: 8192,
apiBase: 'https://proxy.example.com',
additionalParams: { foo: 'bar' },
},
});
});
});
72 changes: 58 additions & 14 deletions src/cli/operations/dev/web-ui/handlers/harness-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
import { ConfigIO } from '../../../../../lib';
import type { HarnessModel } from '../../../../../schema';
import type { HarnessSystemPrompt, InvokeHarnessOptions } from '../../../../aws/agentcore-harness';
import type {
HarnessModelConfiguration,
HarnessSystemPrompt,
InvokeHarnessOptions,
} from '../../../../aws/agentcore-harness';
import type { HarnessInvocationOverrides } from '../api-types';

const DEFAULT_MAX_ITERATIONS = 75;

/**
* Map the local harness spec's model onto the invoke API's model-configuration shape, so dev
* invocations honor agentcore.json model settings (notably maxTokens) without a redeploy. The web
* UI only sends a model override when the user edits the model panel — without this default the
* deployed model config silently wins. Returns undefined for an unknown provider.
*/
function buildModelOverride(specModel: HarnessModel): HarnessModelConfiguration | undefined {
// Fields common to every provider config.
const common = {
modelId: specModel.modelId,
...(specModel.temperature !== undefined && { temperature: specModel.temperature }),
...(specModel.topP !== undefined && { topP: specModel.topP }),
...(specModel.maxTokens !== undefined && { maxTokens: specModel.maxTokens }),
};

switch (specModel.provider) {
case 'bedrock':
return { bedrockModelConfig: { ...common, ...(specModel.apiFormat && { apiFormat: specModel.apiFormat }) } };
case 'open_ai':
return {
openAiModelConfig: {
...common,
...(specModel.apiKeyArn && { apiKeyArn: specModel.apiKeyArn }),
// OpenAI accepts only 'responses' | 'chat_completions'; the schema rejects converse_stream here.
...(specModel.apiFormat && specModel.apiFormat !== 'converse_stream' && { apiFormat: specModel.apiFormat }),
},
};
case 'gemini':
return {
geminiModelConfig: {
...common,
...(specModel.apiKeyArn && { apiKeyArn: specModel.apiKeyArn }),
...(specModel.topK !== undefined && { topK: specModel.topK }),
},
};
case 'lite_llm':
return {
liteLlmModelConfig: {
...common,
...(specModel.apiKeyArn && { apiKeyArn: specModel.apiKeyArn }),
...(specModel.apiBase && { apiBase: specModel.apiBase }),
...(specModel.additionalParams && { additionalParams: specModel.additionalParams }),
},
};
default:
return undefined;
}
}

/**
* Read the model config from the local harness spec, best-effort. Returns undefined when there is
* no config root or the spec is unreadable — the invocation then uses the deployed model config.
Expand Down Expand Up @@ -40,19 +93,10 @@ export function buildInvokeOptions(

if (overrides?.model) {
opts.model = overrides.model;
} else if (specModel?.provider === 'bedrock') {
// The web UI only sends a model override when the user edits the model panel, so without this
// default the deployed model config silently wins during dev. Send the local spec's Bedrock
// model config so agentcore.json settings (notably maxTokens) apply without a redeploy.
opts.model = {
bedrockModelConfig: {
modelId: specModel.modelId,
...(specModel.apiFormat && { apiFormat: specModel.apiFormat }),
...(specModel.temperature !== undefined && { temperature: specModel.temperature }),
...(specModel.topP !== undefined && { topP: specModel.topP }),
...(specModel.maxTokens !== undefined && { maxTokens: specModel.maxTokens }),
},
};
} else if (specModel) {
// Default the model from the local spec so agentcore.json settings (notably maxTokens) apply in
// dev without a redeploy. See buildModelOverride for the per-provider mapping.
opts.model = buildModelOverride(specModel);
}
if (overrides?.systemPrompt) opts.systemPrompt = [{ text: overrides.systemPrompt }] as HarnessSystemPrompt;
if (overrides?.skills) opts.skills = overrides.skills;
Expand Down
Loading