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
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ AI agents are the primary users of AgentV—not humans reading docs. Design for

## Working Style

### Worktree Setup
- For any feature, bug fix, or non-trivial repo change, work from a dedicated git worktree based on the latest `origin/main`.
- Before starting implementation, run `git fetch origin` and verify your worktree `HEAD` is based on the current `origin/main` commit.
- Do not implement from the primary checkout, from a stale local `main`, or from a branch created off an outdated base.
- Default setup:
```bash
git fetch origin
git worktree add ../agentv.worktrees/<type>-<short-desc> -b <type>/<issue-or-topic>-<short-desc> origin/main
cd ../agentv.worktrees/<type>-<short-desc>
```
- If you discover you are not on a fresh worktree from the latest `origin/main`, stop and fix that first before changing code.

### Planning
- Use plan mode for any non-trivial task (5+ steps or architectural decisions).
- If something goes sideways, STOP and re-plan immediately — don't keep pushing a broken approach.
Expand Down
30 changes: 24 additions & 6 deletions packages/core/src/evaluation/providers/pi-coding-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { fileURLToPath } from 'node:url';

import { recordPiLogEntry } from './pi-log-tracker.js';
import {
normalizeAzureSdkBaseUrl,
resolveEnvBaseUrlName,
resolveEnvKeyName,
resolveSubprovider,
Expand Down Expand Up @@ -177,17 +178,21 @@ export class PiCodingAgentProvider implements Provider {
try {
const cwd = this.resolveCwd(request.cwd);
const rawProvider = this.config.subprovider ?? 'google';
const hasBaseUrl = !!this.config.baseUrl;
const normalizedBaseUrl = this.normalizeSdkBaseUrl(rawProvider, this.config.baseUrl);
const hasBaseUrl = !!normalizedBaseUrl;
const providerName = resolveSubprovider(rawProvider, hasBaseUrl);
const modelId = this.config.model ?? 'gemini-2.5-flash';

// Set provider-specific env vars so the SDK can find them
this.setApiKeyEnv(rawProvider, hasBaseUrl);
this.setBaseUrlEnv(rawProvider, hasBaseUrl);
this.setBaseUrlEnv(rawProvider, normalizedBaseUrl, hasBaseUrl);

// Build model using pi-ai's getModel (requires type assertion for runtime strings).
// biome-ignore lint/suspicious/noExplicitAny: runtime string config requires any cast
let model = (sdk.getModel as any)(providerName, modelId);
if (model && normalizedBaseUrl) {
model = { ...model, baseUrl: normalizedBaseUrl };
}
if (!model) {
// Model not in the pi-ai registry — construct a minimal model descriptor.
// This is common for Azure deployments whose names don't match standard model IDs.
Expand All @@ -199,7 +204,7 @@ export class PiCodingAgentProvider implements Provider {
name: modelId,
api: providerName,
provider: envProvider,
baseUrl: this.config.baseUrl ?? '',
baseUrl: normalizedBaseUrl ?? '',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
Expand Down Expand Up @@ -416,12 +421,25 @@ export class PiCodingAgentProvider implements Provider {
}

/** Maps config baseUrl to the provider-specific env var the SDK reads. */
private setBaseUrlEnv(providerName: string, hasBaseUrl = false): void {
if (!this.config.baseUrl) return;
private setBaseUrlEnv(
providerName: string,
baseUrl: string | undefined = this.config.baseUrl,
hasBaseUrl = false,
): void {
const normalizedBaseUrl = this.normalizeSdkBaseUrl(providerName, baseUrl);
if (!normalizedBaseUrl) return;
const envKey = resolveEnvBaseUrlName(providerName, hasBaseUrl);
if (envKey) {
process.env[envKey] = this.config.baseUrl;
process.env[envKey] = normalizedBaseUrl;
}
}

private normalizeSdkBaseUrl(providerName: string, baseUrl?: string): string | undefined {
if (!baseUrl) return undefined;
if (providerName.toLowerCase() === 'azure') {
return normalizeAzureSdkBaseUrl(baseUrl);
}
return baseUrl;
}

private resolveCwd(cwdOverride?: string): string {
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/evaluation/providers/pi-provider-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,25 @@ export function extractAzureResourceName(baseUrl: string): string {
// Already a resource name
return baseUrl;
}

/**
* For pi-coding-agent SDK azure, normalize either a bare resource name or an
* Azure endpoint URL into the OpenAI-compatible v1 base URL expected by the
* SDK's openai-responses path.
*/
export function normalizeAzureSdkBaseUrl(baseUrl: string): string {
const trimmed = baseUrl.trim().replace(/\/+$/, '');
if (!trimmed) {
return trimmed;
}
if (!/^https?:\/\//i.test(trimmed)) {
return `https://${trimmed}.openai.azure.com/openai/v1`;
}
if (/\/openai\/v1$/i.test(trimmed)) {
return trimmed;
}
if (/\/openai$/i.test(trimmed)) {
return `${trimmed}/v1`;
}
return `${trimmed}/openai/v1`;
}
24 changes: 24 additions & 0 deletions packages/core/test/evaluation/providers/pi-coding-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,28 @@ describe('PiCodingAgentProvider', () => {
'aborted before execution',
);
});

it('normalizes a bare Azure resource name before setting OPENAI_BASE_URL for the SDK path', () => {
const original = process.env.OPENAI_BASE_URL;
const provider = new PiCodingAgentProvider('test-target', {
subprovider: 'azure',
baseUrl: 'leos-m6pmw8kz-eastus2',
});

(
provider as unknown as {
setBaseUrlEnv(providerName: string, baseUrl?: string, hasBaseUrl?: boolean): void;
}
).setBaseUrlEnv('azure', 'leos-m6pmw8kz-eastus2', true);

expect(process.env.OPENAI_BASE_URL).toBe(
'https://leos-m6pmw8kz-eastus2.openai.azure.com/openai/v1',
);

if (original === undefined) {
process.env.OPENAI_BASE_URL = undefined;
} else {
process.env.OPENAI_BASE_URL = original;
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ENV_BASE_URL_MAP,
ENV_KEY_MAP,
extractAzureResourceName,
normalizeAzureSdkBaseUrl,
resolveCliProvider,
resolveEnvBaseUrlName,
resolveEnvKeyName,
Expand Down Expand Up @@ -89,6 +90,28 @@ describe('extractAzureResourceName', () => {
});
});

describe('normalizeAzureSdkBaseUrl', () => {
it('converts a bare resource name to an OpenAI-compatible Azure v1 URL', () => {
expect(normalizeAzureSdkBaseUrl('my-resource')).toBe(
'https://my-resource.openai.azure.com/openai/v1',
);
});

it('appends /openai/v1 to a standard Azure endpoint URL', () => {
expect(normalizeAzureSdkBaseUrl('https://my-resource.openai.azure.com')).toBe(
'https://my-resource.openai.azure.com/openai/v1',
);
});

it('preserves an Azure v1 URL that is already normalized', () => {
expect(
normalizeAzureSdkBaseUrl(
'https://my-resource.services.ai.azure.com/api/projects/foo/openai/v1',
),
).toBe('https://my-resource.services.ai.azure.com/api/projects/foo/openai/v1');
});
});

describe('ENV_KEY_MAP', () => {
it('maps azure to AZURE_OPENAI_API_KEY', () => {
expect(ENV_KEY_MAP.azure).toBe('AZURE_OPENAI_API_KEY');
Expand Down
Loading