Add Codex DeepSeek credentialed e2e

This commit is contained in:
pku-xht
2026-08-05 01:30:42 +08:00
parent 46db608843
commit 01704b9a3a
9 changed files with 362 additions and 16 deletions

View File

@@ -0,0 +1,190 @@
import { createServer } from 'node:http'
import type {
IncomingMessage,
Server,
ServerResponse,
} from 'node:http'
import { completeResponsesEvents } from './responses-fixture.ts'
const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
const MAX_REQUEST_BYTES = 1_048_576
/** One running test-only Responses-to-DeepSeek bridge. */
export interface DeepSeekResponsesBridge {
readonly baseUrl: string
readonly completedRequests: number
close(): Promise<void>
}
function readRequest(request: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk: string) => {
body += chunk
if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) {
request.destroy(new Error('DeepSeek bridge request exceeded its byte limit'))
}
})
request.on('end', () => { resolve(body) })
request.on('error', reject)
})
}
function responseInputTexts(body: Record<string, unknown>): string[] {
if (!Array.isArray(body.input)) return []
return body.input.flatMap((item): string[] => {
if (item === null || typeof item !== 'object') return []
const content = (item as Record<string, unknown>).content
if (!Array.isArray(content)) return []
return content.flatMap((part): string[] => (
part !== null
&& typeof part === 'object'
&& typeof (part as Record<string, unknown>).text === 'string'
? [(part as Record<string, unknown>).text as string]
: []
))
})
}
function taskText(body: Record<string, unknown>): string {
const input = responseInputTexts(body).join('\n')
if (input.trim().length > 0) return input
return typeof body.instructions === 'string' ? body.instructions : ''
}
function deepSeekBaseUrl(): string {
const configured = (process.env.DEEPSEEK_BASE_URL ?? OFFICIAL_DEEPSEEK_BASE_URL)
.replace(/\/+$/, '')
if (configured !== OFFICIAL_DEEPSEEK_BASE_URL) {
throw new Error('Codex DeepSeek e2e requires the official DeepSeek base URL')
}
return configured
}
async function completeWithDeepSeek(
authorization: string,
task: string,
): Promise<string> {
const response = await fetch(`${deepSeekBaseUrl()}/chat/completions`, {
method: 'POST',
headers: {
authorization,
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
messages: [
{
role: 'system',
content: 'Follow the user instruction and return only the requested nonce.',
},
{ role: 'user', content: task },
],
temperature: 0,
max_tokens: 64,
stream: false,
}),
})
if (!response.ok) {
void response.body?.cancel()
throw new Error(`DeepSeek bridge upstream returned HTTP ${response.status}`)
}
const payload = await response.json() as {
choices?: Array<{ message?: { content?: unknown } }>
}
const content = payload.choices?.[0]?.message?.content
if (typeof content !== 'string' || content.trim().length === 0) {
throw new Error('DeepSeek bridge upstream returned no text')
}
return content
}
function closeServer(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error !== undefined) reject(error)
else resolve()
})
server.closeAllConnections()
})
}
/**
* Start the single-purpose loopback bridge used by the Codex credentialed e2e.
* @param nonce - unique answer the incoming Responses task must request.
* @returns loopback endpoint, completion count, and close operation.
*/
export async function startDeepSeekResponsesBridge(
nonce: string,
): Promise<DeepSeekResponsesBridge> {
let seenRequests = 0
let completedRequests = 0
const openResponses = new Set<ServerResponse>()
const server = createServer((request, response) => {
openResponses.add(response)
response.on('close', () => { openResponses.delete(response) })
void (async () => {
if (request.method !== 'POST' || request.url !== '/v1/responses') {
response.writeHead(404)
response.end()
return
}
if (seenRequests !== 0) {
response.writeHead(409)
response.end()
return
}
seenRequests += 1
const authorization = request.headers.authorization
if (
typeof authorization !== 'string'
|| !authorization.startsWith('Bearer ')
|| authorization.length === 'Bearer '.length
) {
throw new Error('Codex DeepSeek bridge received no bearer credential')
}
const body = JSON.parse(await readRequest(request)) as Record<string, unknown>
const task = taskText(body)
if (!task.includes(nonce)) {
throw new Error('Codex DeepSeek bridge request omitted the expected nonce')
}
const text = await completeWithDeepSeek(authorization, task)
completedRequests += 1
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
'x-request-id': 'req_deepseek_e2e',
})
for (const event of completeResponsesEvents(text)) {
response.write(`data: ${JSON.stringify(event)}\n\n`)
}
response.end('data: [DONE]\n\n')
})().catch(() => {
if (!response.headersSent) {
response.writeHead(502, { 'content-type': 'application/json' })
}
response.end(JSON.stringify({ error: { message: 'DeepSeek bridge request failed' } }))
})
})
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
resolve()
})
})
const address = server.address()
if (address === null || typeof address === 'string') {
throw new Error('DeepSeek bridge did not acquire a TCP port')
}
return {
baseUrl: `http://127.0.0.1:${address.port}/v1`,
get completedRequests(): number { return completedRequests },
async close(): Promise<void> {
for (const response of openResponses) response.destroy()
await closeServer(server)
},
}
}

View File

@@ -0,0 +1,141 @@
import { execFile } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from 'cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import {
startDeepSeekResponsesBridge,
type DeepSeekResponsesBridge,
} from './deepseek-responses-bridge.ts'
const execFileAsync = promisify(execFile)
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
const codexPackage = JSON.parse(readFileSync(
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
'utf8',
)) as { version: string }
const roots: string[] = []
const contexts: Context[] = []
const bridges: DeepSeekResponsesBridge[] = []
afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
await Promise.all(bridges.splice(0).map(bridge => bridge.close()))
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise<void> {
expect(handles.length).toBeGreaterThan(0)
for (const handle of handles) {
await expect(handle.waitForExit()).resolves.toBe(true)
await expect(handle.done).resolves.toHaveProperty('exitCode')
}
}
describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
'Codex provider with real DeepSeek API',
() => {
it('returns one unique nonce through the production provider and real Codex', async () => {
const apiKey = process.env.DEEPSEEK_API_KEY
if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY')
const root = mkdtempSync(join(tmpdir(), 'dsh-codex-deepseek-e2e-'))
roots.push(root)
const workspace = join(root, 'workspace')
const codexHome = join(root, 'codex-home')
mkdirSync(workspace)
mkdirSync(codexHome)
const nonce = `DSH_CODEX_DEEPSEEK_${randomUUID()}`
const bridge = await startDeepSeekResponsesBridge(nonce)
bridges.push(bridge)
writeFileSync(join(codexHome, 'config.toml'), [
'model = "deepseek-v4-flash"',
'model_provider = "deepseek-e2e"',
'approval_policy = "never"',
'sandbox_mode = "read-only"',
'disable_response_storage = true',
'check_for_update_on_startup = false',
'',
'[model_providers.deepseek-e2e]',
'name = "DeepSeek E2E bridge"',
`base_url = "${bridge.baseUrl}"`,
'env_key = "DEEPSEEK_API_KEY"',
'wire_api = "responses"',
'requires_openai_auth = false',
'',
'[analytics]',
'enabled = false',
'',
].join('\n'))
const env = {
DEEPSEEK_API_KEY: apiKey,
CODEX_HOME: codexHome,
HOME: root,
XDG_CONFIG_HOME: join(root, 'xdg-config'),
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
HTTP_PROXY: '',
HTTPS_PROXY: '',
ALL_PROXY: '',
NO_PROXY: '127.0.0.1,localhost',
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
const handle = spawn(spec)
handles.push(handle)
return handle
})
await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], {
env: { ...process.env, ...env },
})
expect(codexPackage.version).toBe('0.146.0')
expect(version.stdout.trim()).toBe('codex-cli 0.146.0')
const parent = {
id: 'deepseek-e2e-parent',
session: { header: { cwd: workspace } },
} as unknown as Agent
const run = await ctx.subagents.start('codex', {
prompt: [{
type: 'text',
text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`,
}],
parent,
signal: new AbortController().signal,
})
const result = await run.result
await run.dispose()
expect(result.stopReason).toBe('completed')
const text = result.output
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
.trim()
expect(text).toBe(nonce)
expect(bridge.completedRequests).toBe(1)
await expectQuiescent(handles)
}, 180_000)
},
)

View File

@@ -85,7 +85,12 @@ function responseObject(text: string): Record<string, unknown> {
}
}
function completeEvents(text: string): Record<string, unknown>[] {
/**
* Build the minimal Responses SSE event sequence consumed by Codex 0.146.0.
* @param text - exact assistant answer.
* @returns ordered response lifecycle events.
*/
export function completeResponsesEvents(text: string): Record<string, unknown>[] {
const completed = responseObject(text)
const message = (completed.output as Record<string, unknown>[])[0]!
const part = (message.content as Record<string, unknown>[])[0]!
@@ -250,7 +255,7 @@ export async function startResponsesFixture(
})
if (behavior.kind === 'hold') return
const events = behavior.kind === 'complete'
? completeEvents(behavior.text)
? completeResponsesEvents(behavior.text)
: functionCallEvents(behavior.name, behavior.arguments)
for (const event of events) {
response.write(`data: ${JSON.stringify(event)}\n\n`)