From b50e02a2b33eae201e83b016b49aae4e50ab68ea Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 17 Jul 2026 23:33:30 +0800 Subject: [PATCH 1/5] ci: add manual pi-ai provider e2e --- .github/workflows/pi-ai-provider-e2e.yml | 77 +++++++++ .../llm/llm-pi-ai/tests/provider-apis.e2e.ts | 146 ++++++++++++++++++ vitest.e2e.config.ts | 5 +- 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pi-ai-provider-e2e.yml create mode 100644 packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts diff --git a/.github/workflows/pi-ai-provider-e2e.yml b/.github/workflows/pi-ai-provider-e2e.yml new file mode 100644 index 0000000000..7a273c52f3 --- /dev/null +++ b/.github/workflows/pi-ai-provider-e2e.yml @@ -0,0 +1,77 @@ +name: E2E (pi-ai OpenAI and Anthropic) + +# This suite spends tokens against two external providers and is intentionally +# opt-in. It has no push, pull_request, schedule, or workflow_call trigger. +on: + workflow_dispatch: + inputs: + openai_model: + description: OpenAI model from pi-ai's installed catalog + required: true + default: gpt-5.5 + type: string + anthropic_model: + description: Anthropic model from pi-ai's installed catalog + required: true + default: claude-opus-4-8 + type: string + +permissions: + contents: read + +jobs: + e2e: + runs-on: ubuntu-latest + name: OpenAI Responses + Anthropic Messages + timeout-minutes: 20 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Enable corepack (pnpm) + run: corepack enable + + - name: Resolve pnpm store path + id: pnpm-store + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node-24-pnpm- + + - name: Install (immutable) + run: pnpm install --frozen-lockfile + + # The tests self-skip locally when a credential is absent. A manually + # dispatched CI run must fail instead of reporting an all-skipped green. + - name: Preflight (require provider API keys) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_EXTERNAL }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }} + run: | + set -euo pipefail + missing=0 + for name in OPENAI_API_KEY ANTHROPIC_API_KEY; do + if [ -z "${!name:-}" ]; then + echo "::error::${name} is empty. Configure the corresponding *_EXTERNAL repository secret." + missing=1 + fi + done + exit "$missing" + + - name: E2E tests (real OpenAI and Anthropic APIs) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_EXTERNAL }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }} + DSH_PI_AI_OPENAI_MODEL: ${{ inputs.openai_model }} + DSH_PI_AI_ANTHROPIC_MODEL: ${{ inputs.anthropic_model }} + DSH_E2E_MAX_WORKERS: 2 + run: >- + pnpm exec vitest run --config vitest.e2e.config.ts + packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts new file mode 100644 index 0000000000..7fffba4618 --- /dev/null +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -0,0 +1,146 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import LlmService, { CallId } from '@deepseek-ai/dsh-llm' +import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' +import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' +import type { PiAiReplayState } from '@deepseek-ai/dsh-llm-pi-ai' +import { assemble, type AssembledResult } from './assemble.ts' + +interface ProviderCase { + provider: 'openai' | 'anthropic' + api: 'openai-responses' | 'anthropic-messages' + model: string + apiKey?: string +} + +const providerCases: ProviderCase[] = [ + { + provider: 'openai', + api: 'openai-responses', + model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5', + ...process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : {}, + }, + { + provider: 'anthropic', + api: 'anthropic-messages', + model: process.env.DSH_PI_AI_ANTHROPIC_MODEL ?? 'claude-opus-4-8', + ...process.env.ANTHROPIC_API_KEY ? { apiKey: process.env.ANTHROPIC_API_KEY } : {}, + }, +] + +const contexts: Context[] = [] + +async function harness(): Promise { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(LlmService) + await ctx.plugin(LlmPiAi, { + providers: providerCases.map(profile => ({ + provider: profile.provider, + ...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey }, + })), + }) + return ctx +} + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) +}) + +function ask(text: string): Message[] { + return [{ role: 'user', content: [{ type: 'text', text }] }] +} + +function textOf(result: AssembledResult): string { + return result.message.content + .filter(block => block.type === 'text') + .map(block => block.text) + .join('') +} + +function expectNativeReplay(result: AssembledResult, profile: ProviderCase): PiAiReplayState { + const replayState = result.message.provenance?.replayState + expect(replayState).toMatchObject({ + kind: 'pi-ai', + version: 1, + api: profile.api, + provider: profile.provider, + model: profile.model, + }) + return replayState as PiAiReplayState +} + +const lookupTool: ToolSchema = { + name: 'lookup_code', + description: 'Look up the word represented by a short code.', + parameters: { + type: 'object', + properties: { code: { type: 'string', description: 'The code to look up.' } }, + required: ['code'], + }, +} + +for (const profile of providerCases) { + describe.skipIf(profile.apiKey === undefined)( + `llm-pi-ai ${profile.provider} e2e (${profile.api})`, + () => { + it('streams text with usage and native replay metadata', async () => { + const ctx = await harness() + const result = await assemble(ctx, { + provider: profile.provider, + model: profile.model, + messages: ask('Reply with exactly the word: pong'), + maxTokens: 64, + }) + + expect(result.finish.kind).toBe('stop') + expect(textOf(result).toLowerCase()).toContain('pong') + expect(result.usage?.inputTokens).toBeGreaterThan(0) + expect(result.usage?.outputTokens).toBeGreaterThan(0) + expect(expectNativeReplay(result, profile).stopReason).toBe('stop') + }) + + it('round-trips a tool call with provider-native replay metadata', async () => { + const ctx = await harness() + const prompt = ask('Use lookup_code with code "blue". Do not answer without calling the tool.') + const first = await assemble(ctx, { + provider: profile.provider, + model: profile.model, + messages: prompt, + tools: [lookupTool], + maxTokens: 256, + }) + + expect(first.finish.kind).toBe('tool-calls') + const call = first.message.content.find(block => block.type === 'tool-call') + expect(call).toBeDefined() + expect(call!.name).toBe('lookup_code') + expect(JSON.parse(call!.arguments)).toMatchObject({ code: 'blue' }) + expect(expectNativeReplay(first, profile).stopReason).toBe('toolUse') + + const second = await assemble(ctx, { + provider: profile.provider, + model: profile.model, + messages: [ + ...prompt, + first.message, + { + role: 'user', + content: [{ + type: 'tool-result', + toolCallId: CallId(call!.id), + content: [{ type: 'text', text: 'The code blue means ocean.' }], + }], + }, + ], + tools: [lookupTool], + maxTokens: 256, + }) + + expect(second.finish.kind).toBe('stop') + expect(textOf(second).toLowerCase()).toContain('ocean') + expect(expectNativeReplay(second, profile).stopReason).toBe('stop') + }) + }, + ) +} diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index adc8b3f3b2..6a84b13daf 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -2,8 +2,9 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' // Real-API suite, separate because it spends tokens. Each test self-skips without -// DEEPSEEK_API_KEY for keyless CI; the credentialed workflow preflights the secret. Values may come -// from the environment or gitignored root `.env`, with optional DEEPSEEK_BASE_URL. +// its provider credential for keyless CI; credentialed workflows preflight the +// secrets they require. Values may come from the environment or gitignored root +// `.env`, with provider-specific endpoint overrides where supported. try { // Node >= 21.7 native; throws when the file does not exist. process.loadEnvFile(new URL('.env', import.meta.url).pathname) From 1fdcd5a77314717391462d377e6991622db4eb31 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 13:42:21 +0800 Subject: [PATCH 2/5] ci: run pi-ai OpenAI e2e through Azure --- .github/workflows/pi-ai-provider-e2e.yml | 20 ++++++++------- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 19 ++++++++++++++ .../llm/llm-pi-ai/tests/provider-apis.e2e.ts | 25 +++++++++++-------- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pi-ai-provider-e2e.yml b/.github/workflows/pi-ai-provider-e2e.yml index 7a273c52f3..4be5f0c13a 100644 --- a/.github/workflows/pi-ai-provider-e2e.yml +++ b/.github/workflows/pi-ai-provider-e2e.yml @@ -1,12 +1,12 @@ -name: E2E (pi-ai OpenAI and Anthropic) +name: E2E (pi-ai Azure OpenAI and Anthropic) # This suite spends tokens against two external providers and is intentionally # opt-in. It has no push, pull_request, schedule, or workflow_call trigger. on: workflow_dispatch: inputs: - openai_model: - description: OpenAI model from pi-ai's installed catalog + azure_openai_model: + description: Azure OpenAI model from pi-ai's installed catalog required: true default: gpt-5.5 type: string @@ -22,7 +22,7 @@ permissions: jobs: e2e: runs-on: ubuntu-latest - name: OpenAI Responses + Anthropic Messages + name: Azure OpenAI Responses + Anthropic Messages timeout-minutes: 20 steps: - uses: actions/checkout@v6 @@ -52,12 +52,12 @@ jobs: # dispatched CI run must fail instead of reporting an all-skipped green. - name: Preflight (require provider API keys) env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_EXTERNAL }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY_EXTERNAL }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }} run: | set -euo pipefail missing=0 - for name in OPENAI_API_KEY ANTHROPIC_API_KEY; do + for name in AZURE_OPENAI_API_KEY ANTHROPIC_API_KEY; do if [ -z "${!name:-}" ]; then echo "::error::${name} is empty. Configure the corresponding *_EXTERNAL repository secret." missing=1 @@ -65,11 +65,13 @@ jobs: done exit "$missing" - - name: E2E tests (real OpenAI and Anthropic APIs) + - name: E2E tests (real Azure OpenAI and Anthropic APIs) env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_EXTERNAL }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY_EXTERNAL }} + AZURE_OPENAI_API_VERSION: v1 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }} - DSH_PI_AI_OPENAI_MODEL: ${{ inputs.openai_model }} + DSH_PI_AI_AZURE_OPENAI_MODEL: ${{ inputs.azure_openai_model }} + DSH_PI_AI_AZURE_OPENAI_BASE_URL: https://openai-routerhub-resource.services.ai.azure.com/api/projects/openai/openai/ DSH_PI_AI_ANTHROPIC_MODEL: ${{ inputs.anthropic_model }} DSH_E2E_MAX_WORKERS: 2 run: >- diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index 559c36bf27..3ebb23ee50 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -167,6 +167,25 @@ describe('PiAiAdapter provider routing', () => { expect(server.paths).toEqual(['/v1/responses']) }) + it('uses Azure OpenAI Responses with the configured project base path and API key', async () => { + vi.stubEnv('AZURE_OPENAI_API_VERSION', 'v1') + const server = await mockServer([{ status: 401, body: JSON.stringify({ error: { message: 'expected mock failure' } }) }]) + const ctx = new Context() + await ctx.plugin(LlmService) + await ctx.plugin(LlmPiAi, { + providers: [{ + provider: 'azure-openai-responses', + apiKey: 'test-key', + baseURL: `${server.url}/api/projects/openai/openai/`, + maxRetries: 0, + }], + }) + const result = await assemble(ctx, { provider: 'azure-openai-responses', model: 'gpt-5.5', messages: [] }) + expect(result.finish.kind).toBe('error') + expect(server.paths).toEqual(['/api/projects/openai/openai/responses?api-version=v1']) + expect(server.headers[0]?.['api-key']).toBe('test-key') + }) + it.each([ [401, 'AUTH'], [400, 'INVALID_REQUEST'], diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts index 7fffba4618..1ed0959785 100644 --- a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -3,22 +3,26 @@ import { Context } from 'cordis' import LlmService, { CallId } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' -import type { PiAiReplayState } from '@deepseek-ai/dsh-llm-pi-ai' +import type { PiAiReplayState } from '../src/replay.ts' import { assemble, type AssembledResult } from './assemble.ts' interface ProviderCase { - provider: 'openai' | 'anthropic' - api: 'openai-responses' | 'anthropic-messages' + provider: 'azure-openai-responses' | 'anthropic' + api: 'azure-openai-responses' | 'anthropic-messages' model: string apiKey?: string + baseURL?: string } +const azureOpenAIBaseURL = process.env.DSH_PI_AI_AZURE_OPENAI_BASE_URL ?? process.env.AZURE_OPENAI_BASE_URL + const providerCases: ProviderCase[] = [ { - provider: 'openai', - api: 'openai-responses', - model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5', - ...process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : {}, + provider: 'azure-openai-responses', + api: 'azure-openai-responses', + model: process.env.DSH_PI_AI_AZURE_OPENAI_MODEL ?? 'gpt-5.5', + ...process.env.AZURE_OPENAI_API_KEY ? { apiKey: process.env.AZURE_OPENAI_API_KEY } : {}, + ...azureOpenAIBaseURL ? { baseURL: azureOpenAIBaseURL } : {}, }, { provider: 'anthropic', @@ -38,6 +42,7 @@ async function harness(): Promise { providers: providerCases.map(profile => ({ provider: profile.provider, ...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey }, + ...profile.baseURL === undefined ? {} : { baseURL: profile.baseURL }, })), }) return ctx @@ -90,7 +95,7 @@ for (const profile of providerCases) { provider: profile.provider, model: profile.model, messages: ask('Reply with exactly the word: pong'), - maxTokens: 64, + maxTokens: 1024, }) expect(result.finish.kind).toBe('stop') @@ -108,7 +113,7 @@ for (const profile of providerCases) { model: profile.model, messages: prompt, tools: [lookupTool], - maxTokens: 256, + maxTokens: 2048, }) expect(first.finish.kind).toBe('tool-calls') @@ -134,7 +139,7 @@ for (const profile of providerCases) { }, ], tools: [lookupTool], - maxTokens: 256, + maxTokens: 2048, }) expect(second.finish.kind).toBe('stop') From 51425dd61a481257a72f0351e7a564439e470be0 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 13:51:25 +0800 Subject: [PATCH 3/5] test(llm-pi-ai): surface provider e2e failures --- packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts index 1ed0959785..b830bfe08b 100644 --- a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -63,6 +63,13 @@ function textOf(result: AssembledResult): string { .join('') } +function expectFinish(result: AssembledResult, expected: 'stop' | 'tool-calls'): void { + if (result.finish.kind === 'error') { + throw new Error(`provider request failed (${result.finish.code ?? 'unknown'}): ${result.finish.message}`) + } + expect(result.finish.kind).toBe(expected) +} + function expectNativeReplay(result: AssembledResult, profile: ProviderCase): PiAiReplayState { const replayState = result.message.provenance?.replayState expect(replayState).toMatchObject({ @@ -98,7 +105,7 @@ for (const profile of providerCases) { maxTokens: 1024, }) - expect(result.finish.kind).toBe('stop') + expectFinish(result, 'stop') expect(textOf(result).toLowerCase()).toContain('pong') expect(result.usage?.inputTokens).toBeGreaterThan(0) expect(result.usage?.outputTokens).toBeGreaterThan(0) @@ -116,7 +123,7 @@ for (const profile of providerCases) { maxTokens: 2048, }) - expect(first.finish.kind).toBe('tool-calls') + expectFinish(first, 'tool-calls') const call = first.message.content.find(block => block.type === 'tool-call') expect(call).toBeDefined() expect(call!.name).toBe('lookup_code') @@ -142,7 +149,7 @@ for (const profile of providerCases) { maxTokens: 2048, }) - expect(second.finish.kind).toBe('stop') + expectFinish(second, 'stop') expect(textOf(second).toLowerCase()).toContain('ocean') expect(expectNativeReplay(second, profile).stopReason).toBe('stop') }) From 39dea6107ea7ec00994142b078b04e12506dc7a9 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 14:06:18 +0800 Subject: [PATCH 4/5] fix(ci): target Azure Foundry OpenAI v1 route --- .github/workflows/pi-ai-provider-e2e.yml | 5 ++--- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 12 ++++++------ .../llm/llm-pi-ai/tests/provider-apis.e2e.ts | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pi-ai-provider-e2e.yml b/.github/workflows/pi-ai-provider-e2e.yml index 4be5f0c13a..d198abf5b5 100644 --- a/.github/workflows/pi-ai-provider-e2e.yml +++ b/.github/workflows/pi-ai-provider-e2e.yml @@ -68,10 +68,9 @@ jobs: - name: E2E tests (real Azure OpenAI and Anthropic APIs) env: AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY_EXTERNAL }} - AZURE_OPENAI_API_VERSION: v1 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }} - DSH_PI_AI_AZURE_OPENAI_MODEL: ${{ inputs.azure_openai_model }} - DSH_PI_AI_AZURE_OPENAI_BASE_URL: https://openai-routerhub-resource.services.ai.azure.com/api/projects/openai/openai/ + DSH_PI_AI_OPENAI_MODEL: ${{ inputs.azure_openai_model }} + DSH_PI_AI_OPENAI_BASE_URL: https://openai-routerhub-resource.services.ai.azure.com/api/projects/openai/openai/v1 DSH_PI_AI_ANTHROPIC_MODEL: ${{ inputs.anthropic_model }} DSH_E2E_MAX_WORKERS: 2 run: >- diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index 3ebb23ee50..26fbb392de 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -167,22 +167,22 @@ describe('PiAiAdapter provider routing', () => { expect(server.paths).toEqual(['/v1/responses']) }) - it('uses Azure OpenAI Responses with the configured project base path and API key', async () => { - vi.stubEnv('AZURE_OPENAI_API_VERSION', 'v1') + it('uses OpenAI Responses against an Azure project v1 path with its API key header', async () => { const server = await mockServer([{ status: 401, body: JSON.stringify({ error: { message: 'expected mock failure' } }) }]) const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmPiAi, { providers: [{ - provider: 'azure-openai-responses', + provider: 'openai', apiKey: 'test-key', - baseURL: `${server.url}/api/projects/openai/openai/`, + baseURL: `${server.url}/api/projects/openai/openai/v1`, + headers: { 'api-key': 'test-key' }, maxRetries: 0, }], }) - const result = await assemble(ctx, { provider: 'azure-openai-responses', model: 'gpt-5.5', messages: [] }) + const result = await assemble(ctx, { provider: 'openai', model: 'gpt-5.5', messages: [] }) expect(result.finish.kind).toBe('error') - expect(server.paths).toEqual(['/api/projects/openai/openai/responses?api-version=v1']) + expect(server.paths).toEqual(['/api/projects/openai/openai/v1/responses']) expect(server.headers[0]?.['api-key']).toBe('test-key') }) diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts index b830bfe08b..a2fcc2c143 100644 --- a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -7,22 +7,24 @@ import type { PiAiReplayState } from '../src/replay.ts' import { assemble, type AssembledResult } from './assemble.ts' interface ProviderCase { - provider: 'azure-openai-responses' | 'anthropic' - api: 'azure-openai-responses' | 'anthropic-messages' + provider: 'openai' | 'anthropic' + api: 'openai-responses' | 'anthropic-messages' model: string apiKey?: string baseURL?: string + headers?: Record } -const azureOpenAIBaseURL = process.env.DSH_PI_AI_AZURE_OPENAI_BASE_URL ?? process.env.AZURE_OPENAI_BASE_URL +const openAIBaseURL = process.env.DSH_PI_AI_OPENAI_BASE_URL +const azureOpenAIKey = process.env.AZURE_OPENAI_API_KEY const providerCases: ProviderCase[] = [ { - provider: 'azure-openai-responses', - api: 'azure-openai-responses', - model: process.env.DSH_PI_AI_AZURE_OPENAI_MODEL ?? 'gpt-5.5', - ...process.env.AZURE_OPENAI_API_KEY ? { apiKey: process.env.AZURE_OPENAI_API_KEY } : {}, - ...azureOpenAIBaseURL ? { baseURL: azureOpenAIBaseURL } : {}, + provider: 'openai', + api: 'openai-responses', + model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5', + ...azureOpenAIKey ? { apiKey: azureOpenAIKey, headers: { 'api-key': azureOpenAIKey } } : {}, + ...openAIBaseURL ? { baseURL: openAIBaseURL } : {}, }, { provider: 'anthropic', @@ -43,6 +45,7 @@ async function harness(): Promise { provider: profile.provider, ...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey }, ...profile.baseURL === undefined ? {} : { baseURL: profile.baseURL }, + ...profile.headers === undefined ? {} : { headers: profile.headers }, })), }) return ctx From fc478d675c8ce32bc838f2f56a60ac0e0cdc1511 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 19 Jul 2026 14:10:47 +0800 Subject: [PATCH 5/5] fix(ci): suppress bearer auth for Azure API keys --- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 3 ++- packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index 26fbb392de..af791bd806 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -176,7 +176,7 @@ describe('PiAiAdapter provider routing', () => { provider: 'openai', apiKey: 'test-key', baseURL: `${server.url}/api/projects/openai/openai/v1`, - headers: { 'api-key': 'test-key' }, + headers: { 'api-key': 'test-key', Authorization: '' }, maxRetries: 0, }], }) @@ -184,6 +184,7 @@ describe('PiAiAdapter provider routing', () => { expect(result.finish.kind).toBe('error') expect(server.paths).toEqual(['/api/projects/openai/openai/v1/responses']) expect(server.headers[0]?.['api-key']).toBe('test-key') + expect(server.headers[0]?.authorization).toBe('') }) it.each([ diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts index a2fcc2c143..107c12d264 100644 --- a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -23,7 +23,9 @@ const providerCases: ProviderCase[] = [ provider: 'openai', api: 'openai-responses', model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5', - ...azureOpenAIKey ? { apiKey: azureOpenAIKey, headers: { 'api-key': azureOpenAIKey } } : {}, + ...azureOpenAIKey + ? { apiKey: azureOpenAIKey, headers: { 'api-key': azureOpenAIKey, Authorization: '' } } + : {}, ...openAIBaseURL ? { baseURL: openAIBaseURL } : {}, }, {