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
51 changes: 40 additions & 11 deletions .github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 43 additions & 43 deletions .github/workflows/smoke-copilot-byok-aoai-apikey.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,46 +56,48 @@ sandbox:
agent:
id: awf
strict: true
steps:
- name: Pre-compute BYOK smoke test data
id: smoke-data
run: |
echo "::group::Verify BYOK configuration"
echo "COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}"
echo "::endgroup::"

echo "::group::Fetching last 2 merged PRs"
PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \
--json number,title,author,mergedAt \
--jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \
|| echo "(PR fetch failed)")
echo "$PR_DATA"
echo "::endgroup::"

echo "::group::GitHub.com connectivity check"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000")
echo "github.com returned HTTP $HTTP_CODE"
echo "::endgroup::"

echo "::group::File write/read test"
TEST_DIR="/tmp/gh-aw/agent"
TEST_FILE="$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt"
mkdir -p "$TEST_DIR"
echo "BYOK AOAI api-key smoke test passed at $(date)" > "$TEST_FILE"
FILE_CONTENT=$(cat "$TEST_FILE")
echo "Wrote and read back: $FILE_CONTENT"
echo "::endgroup::"

{
echo "SMOKE_PR_DATA<<SMOKE_EOF"
echo "$PR_DATA"
echo "SMOKE_EOF"
echo "SMOKE_HTTP_CODE=$HTTP_CODE"
echo "SMOKE_FILE_CONTENT=$FILE_CONTENT"
echo "SMOKE_FILE_PATH=$TEST_FILE"
} >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
jobs:
activation:
pre-steps:
- name: Pre-compute BYOK smoke test data
id: smoke-data
run: |
echo "::group::Verify BYOK configuration"
echo "COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}"
echo "::endgroup::"

echo "::group::Fetching last 2 merged PRs"
PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \
--json number,title,author,mergedAt \
--jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \
|| echo "(PR fetch failed)")
echo "$PR_DATA"
echo "::endgroup::"

echo "::group::GitHub.com connectivity check"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000")
echo "github.com returned HTTP $HTTP_CODE"
echo "::endgroup::"

echo "::group::File write/read test"
TEST_DIR="/tmp/gh-aw/agent"
TEST_FILE="$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt"
mkdir -p "$TEST_DIR"
echo "BYOK AOAI api-key smoke test passed at $(date)" > "$TEST_FILE"
FILE_CONTENT=$(cat "$TEST_FILE")
echo "Wrote and read back: $FILE_CONTENT"
echo "::endgroup::"

{
echo "SMOKE_PR_DATA<<SMOKE_EOF"
echo "$PR_DATA"
echo "SMOKE_EOF"
echo "SMOKE_HTTP_CODE=$HTTP_CODE"
echo "SMOKE_FILE_CONTENT=$FILE_CONTENT"
echo "SMOKE_FILE_PATH=$TEST_FILE"
} >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
post-steps:
- name: Validate safe outputs were invoked
run: |
Expand Down Expand Up @@ -151,9 +153,7 @@ Pre-step result: HTTP ${{ steps.smoke-data.outputs.SMOKE_HTTP_CODE }} from githu
✅ if HTTP 200 or 301, ❌ otherwise.

### 3. File Write/Read Test
Pre-step wrote and read back: "${{ steps.smoke-data.outputs.SMOKE_FILE_CONTENT }}"
File path: ${{ steps.smoke-data.outputs.SMOKE_FILE_PATH }}
Verify by running `cat` on the file path using bash to confirm it exists.
The activation pre-step wrote and read back: "${{ steps.smoke-data.outputs.SMOKE_FILE_CONTENT }}" (that file lives on the activation runner, not here). To exercise agent-side file I/O in the sandbox, write a short string to `/tmp/gh-aw/agent/agent-write-test.txt` and `cat` it back with bash. ✅ if the read-back matches.

### 4. BYOK Inference Test
You are running in direct BYOK mode against Azure OpenAI (Foundry) right now, using `o4-mini-aw` via an api-key. The fact that you can read this prompt and respond means the BYOK inference path (agent → api-proxy sidecar → Foundry endpoint) is working. Confirm ✅.
Expand Down
47 changes: 47 additions & 0 deletions src/commands/validators/config-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,53 @@ describe('config-assembly', () => {
);
});

it('should allow custom COPILOT_MODEL values in BYOK mode with a provider base URL', () => {
mockBuildConfigOnce({
copilotProviderApiKey: 'byok-api-key-for-azure-foundry',
copilotProviderBaseUrl: 'https://example-resource.openai.azure.com/openai/deployments/o4-mini-aw',
additionalEnv: { COPILOT_MODEL: 'o4-mini-aw' },
});

const agentOptions = createMinimalAgentOptions();
agentOptions.additionalEnv = { COPILOT_MODEL: 'o4-mini-aw' };

const result = assembleAndValidateConfig(
{},
'echo test',
createMinimalLogAndLimits(),
createMinimalNetworkOptions(),
agentOptions,
);

expect(logger.error).not.toHaveBeenCalled();
expect(result.additionalEnv?.COPILOT_MODEL).toBe('o4-mini-aw');
});

it('should allow custom COPILOT_MODEL values when provider base URL is set via env file', () => {
const envFilePath = path.join(testDir, 'byok.env');
fs.writeFileSync(envFilePath, 'COPILOT_PROVIDER_BASE_URL=https://example-resource.openai.azure.com/openai/deployments/o4-mini-aw\n');

mockBuildConfigOnce({
copilotProviderApiKey: 'byok-api-key-for-azure-foundry',
envFile: envFilePath,
additionalEnv: { COPILOT_MODEL: 'o4-mini-aw' },
});

const agentOptions = createMinimalAgentOptions();
agentOptions.additionalEnv = { COPILOT_MODEL: 'o4-mini-aw' };

const result = assembleAndValidateConfig(
{},
'echo test',
createMinimalLogAndLimits(),
createMinimalNetworkOptions(),
agentOptions,
);

expect(logger.error).not.toHaveBeenCalled();
expect(result.additionalEnv?.COPILOT_MODEL).toBe('o4-mini-aw');
});

it('should log normalization when COPILOT_MODEL casing is adjusted', () => {
mockBuildConfigOnce({
copilotGithubToken: 'github_pat_testtoken',
Expand Down
21 changes: 18 additions & 3 deletions src/commands/validators/config-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export function assembleAndValidateConfig(
networkOptions: NetworkOptionsResult,
agentOptions: AgentOptionsResult,
): WrapperConfig {
const readCopilotModelFromEnvFiles = (envFile: unknown): string | undefined => {
const readEnvVarFromEnvFiles = (envFile: unknown, key: string): string | undefined => {
const envFiles = Array.isArray(envFile) ? envFile : envFile ? [envFile] : [];
let lastSeen: string | undefined;
const pattern = new RegExp(`^(?:export\\s+)?${key}\\s*=\\s*(.*)$`);
for (const candidate of envFiles) {
if (typeof candidate !== 'string' || candidate.trim() === '') continue;
try {
Expand All @@ -57,7 +58,7 @@ export function assembleAndValidateConfig(
for (const line of envFileContents.split(/\r?\n/)) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
const match = trimmedLine.match(/^(?:export\s+)?COPILOT_MODEL\s*=\s*(.*)$/);
const match = trimmedLine.match(pattern);
if (match) {
lastSeen = match[1]?.trim() || '';
}
Expand All @@ -69,6 +70,10 @@ export function assembleAndValidateConfig(
return lastSeen;
};

const readCopilotModelFromEnvFiles = (envFile: unknown): string | undefined => {
return readEnvVarFromEnvFiles(envFile, 'COPILOT_MODEL');
};

// --- Config assembly -----------------------------------------------------

const config = buildConfig({
Expand Down Expand Up @@ -276,7 +281,17 @@ export function assembleAndValidateConfig(
logger.warn.bind(logger),
);

if (copilotModel && (config.copilotGithubToken || config.copilotProviderApiKey)) {
const hasCustomCopilotProviderBaseUrl = !!(
config.copilotProviderBaseUrl ||
config.additionalEnv?.COPILOT_PROVIDER_BASE_URL ||
readEnvVarFromEnvFiles((config as { envFile?: unknown }).envFile, 'COPILOT_PROVIDER_BASE_URL') ||
(config.envAll ? process.env.COPILOT_PROVIDER_BASE_URL : undefined)
);
Comment on lines +284 to +289
if (
copilotModel &&
!hasCustomCopilotProviderBaseUrl &&
(config.copilotGithubToken || config.copilotProviderApiKey)
) {
Comment on lines +284 to +294
const validation = validateCopilotModel(copilotModel);
if (!validation.valid) {
logger.error(validation.message);
Expand Down
Loading