Browse Source

ci: run pi-ai OpenAI e2e through Azure

Yichen Jiang 3 months ago
parent
commit
1fdcd5a773

+ 11 - 9
.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: >-

+ 19 - 0
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'],

+ 15 - 10
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<Context> {
     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')