ci: run pi-ai OpenAI e2e through Azure
This commit is contained in:
20
.github/workflows/pi-ai-provider-e2e.yml
vendored
20
.github/workflows/pi-ai-provider-e2e.yml
vendored
@@ -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
|
# This suite spends tokens against two external providers and is intentionally
|
||||||
# opt-in. It has no push, pull_request, schedule, or workflow_call trigger.
|
# opt-in. It has no push, pull_request, schedule, or workflow_call trigger.
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
openai_model:
|
azure_openai_model:
|
||||||
description: OpenAI model from pi-ai's installed catalog
|
description: Azure OpenAI model from pi-ai's installed catalog
|
||||||
required: true
|
required: true
|
||||||
default: gpt-5.5
|
default: gpt-5.5
|
||||||
type: string
|
type: string
|
||||||
@@ -22,7 +22,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: OpenAI Responses + Anthropic Messages
|
name: Azure OpenAI Responses + Anthropic Messages
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -52,12 +52,12 @@ jobs:
|
|||||||
# dispatched CI run must fail instead of reporting an all-skipped green.
|
# dispatched CI run must fail instead of reporting an all-skipped green.
|
||||||
- name: Preflight (require provider API keys)
|
- name: Preflight (require provider API keys)
|
||||||
env:
|
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 }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
missing=0
|
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
|
if [ -z "${!name:-}" ]; then
|
||||||
echo "::error::${name} is empty. Configure the corresponding *_EXTERNAL repository secret."
|
echo "::error::${name} is empty. Configure the corresponding *_EXTERNAL repository secret."
|
||||||
missing=1
|
missing=1
|
||||||
@@ -65,11 +65,13 @@ jobs:
|
|||||||
done
|
done
|
||||||
exit "$missing"
|
exit "$missing"
|
||||||
|
|
||||||
- name: E2E tests (real OpenAI and Anthropic APIs)
|
- name: E2E tests (real Azure OpenAI and Anthropic APIs)
|
||||||
env:
|
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 }}
|
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_PI_AI_ANTHROPIC_MODEL: ${{ inputs.anthropic_model }}
|
||||||
DSH_E2E_MAX_WORKERS: 2
|
DSH_E2E_MAX_WORKERS: 2
|
||||||
run: >-
|
run: >-
|
||||||
|
|||||||
@@ -167,6 +167,25 @@ describe('PiAiAdapter provider routing', () => {
|
|||||||
expect(server.paths).toEqual(['/v1/responses'])
|
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([
|
it.each([
|
||||||
[401, 'AUTH'],
|
[401, 'AUTH'],
|
||||||
[400, 'INVALID_REQUEST'],
|
[400, 'INVALID_REQUEST'],
|
||||||
|
|||||||
@@ -3,22 +3,26 @@ import { Context } from 'cordis'
|
|||||||
import LlmService, { CallId } from '@deepseek-ai/dsh-llm'
|
import LlmService, { CallId } from '@deepseek-ai/dsh-llm'
|
||||||
import type { Message, ToolSchema } 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 * 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'
|
import { assemble, type AssembledResult } from './assemble.ts'
|
||||||
|
|
||||||
interface ProviderCase {
|
interface ProviderCase {
|
||||||
provider: 'openai' | 'anthropic'
|
provider: 'azure-openai-responses' | 'anthropic'
|
||||||
api: 'openai-responses' | 'anthropic-messages'
|
api: 'azure-openai-responses' | 'anthropic-messages'
|
||||||
model: string
|
model: string
|
||||||
apiKey?: 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[] = [
|
const providerCases: ProviderCase[] = [
|
||||||
{
|
{
|
||||||
provider: 'openai',
|
provider: 'azure-openai-responses',
|
||||||
api: 'openai-responses',
|
api: 'azure-openai-responses',
|
||||||
model: process.env.DSH_PI_AI_OPENAI_MODEL ?? 'gpt-5.5',
|
model: process.env.DSH_PI_AI_AZURE_OPENAI_MODEL ?? 'gpt-5.5',
|
||||||
...process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : {},
|
...process.env.AZURE_OPENAI_API_KEY ? { apiKey: process.env.AZURE_OPENAI_API_KEY } : {},
|
||||||
|
...azureOpenAIBaseURL ? { baseURL: azureOpenAIBaseURL } : {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
@@ -38,6 +42,7 @@ async function harness(): Promise<Context> {
|
|||||||
providers: providerCases.map(profile => ({
|
providers: providerCases.map(profile => ({
|
||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey },
|
...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey },
|
||||||
|
...profile.baseURL === undefined ? {} : { baseURL: profile.baseURL },
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
@@ -90,7 +95,7 @@ for (const profile of providerCases) {
|
|||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
model: profile.model,
|
model: profile.model,
|
||||||
messages: ask('Reply with exactly the word: pong'),
|
messages: ask('Reply with exactly the word: pong'),
|
||||||
maxTokens: 64,
|
maxTokens: 1024,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.finish.kind).toBe('stop')
|
expect(result.finish.kind).toBe('stop')
|
||||||
@@ -108,7 +113,7 @@ for (const profile of providerCases) {
|
|||||||
model: profile.model,
|
model: profile.model,
|
||||||
messages: prompt,
|
messages: prompt,
|
||||||
tools: [lookupTool],
|
tools: [lookupTool],
|
||||||
maxTokens: 256,
|
maxTokens: 2048,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first.finish.kind).toBe('tool-calls')
|
expect(first.finish.kind).toBe('tool-calls')
|
||||||
@@ -134,7 +139,7 @@ for (const profile of providerCases) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
tools: [lookupTool],
|
tools: [lookupTool],
|
||||||
maxTokens: 256,
|
maxTokens: 2048,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(second.finish.kind).toBe('stop')
|
expect(second.finish.kind).toBe('stop')
|
||||||
|
|||||||
Reference in New Issue
Block a user