From 0e2d306a8a850d31f256dc76d513240aa6232467 Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Sat, 4 Apr 2026 09:12:54 +0000 Subject: [PATCH] fix(core): normalize pi-sdk azure base url --- AGENTS.md | 12 ++++++++ .../evaluation/providers/pi-coding-agent.ts | 30 +++++++++++++++---- .../providers/pi-provider-aliases.ts | 22 ++++++++++++++ .../providers/pi-coding-agent.test.ts | 24 +++++++++++++++ .../providers/pi-provider-aliases.test.ts | 23 ++++++++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dd7529314..2f2a53400 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/- -b /- origin/main +cd ../agentv.worktrees/- +``` +- 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. diff --git a/packages/core/src/evaluation/providers/pi-coding-agent.ts b/packages/core/src/evaluation/providers/pi-coding-agent.ts index 5b21aeace..b900ac016 100644 --- a/packages/core/src/evaluation/providers/pi-coding-agent.ts +++ b/packages/core/src/evaluation/providers/pi-coding-agent.ts @@ -19,6 +19,7 @@ import { fileURLToPath } from 'node:url'; import { recordPiLogEntry } from './pi-log-tracker.js'; import { + normalizeAzureSdkBaseUrl, resolveEnvBaseUrlName, resolveEnvKeyName, resolveSubprovider, @@ -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. @@ -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 }, @@ -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 { diff --git a/packages/core/src/evaluation/providers/pi-provider-aliases.ts b/packages/core/src/evaluation/providers/pi-provider-aliases.ts index ed8f6f404..cabdeb5f8 100644 --- a/packages/core/src/evaluation/providers/pi-provider-aliases.ts +++ b/packages/core/src/evaluation/providers/pi-provider-aliases.ts @@ -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`; +} diff --git a/packages/core/test/evaluation/providers/pi-coding-agent.test.ts b/packages/core/test/evaluation/providers/pi-coding-agent.test.ts index 233dc76d9..0a264f1ff 100644 --- a/packages/core/test/evaluation/providers/pi-coding-agent.test.ts +++ b/packages/core/test/evaluation/providers/pi-coding-agent.test.ts @@ -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; + } + }); }); diff --git a/packages/core/test/evaluation/providers/pi-provider-aliases.test.ts b/packages/core/test/evaluation/providers/pi-provider-aliases.test.ts index b5df85827..b45e8a88d 100644 --- a/packages/core/test/evaluation/providers/pi-provider-aliases.test.ts +++ b/packages/core/test/evaluation/providers/pi-provider-aliases.test.ts @@ -4,6 +4,7 @@ import { ENV_BASE_URL_MAP, ENV_KEY_MAP, extractAzureResourceName, + normalizeAzureSdkBaseUrl, resolveCliProvider, resolveEnvBaseUrlName, resolveEnvKeyName, @@ -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');