Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,486 changes: 1,380 additions & 2,106 deletions bun.lock

Large diffs are not rendered by default.

299 changes: 299 additions & 0 deletions docs/superpowers/plans/2026-08-02-openai-responses-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# OpenAI Responses API Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.

**Goal:** 通过 `UPSTREAM_API_MODEL=responses` 让 API-key OpenAI Provider 调用通用 `{OPENAI_BASE_URL}/responses` SSE API,同时保留 Chat Completions 和 ChatGPT Subscription 的既有行为。

**Architecture:** 在 OpenAI client 层新增无 ChatGPT 专用认证头的 Responses 流请求函数,复用 `responsesAdapter.ts` 的请求构造、SSE 解析和流适配。`queryModelOpenAI()` 依照 ChatGPT OAuth → 通用 Responses → Chat Completions 的优先级选择传输路径;配置解析集中为可测试的纯函数,未知配置值显式报错。

**Tech Stack:** Bun、TypeScript strict、`bun:test`、OpenAI SDK、Fetch/SSE、Anthropic 内部流事件。

## Global Constraints

- `UPSTREAM_API_MODEL` 的有效值仅为 `chat_completions`(默认)和 `responses`。
- `OPENAI_MODEL` 仅表示上游模型名称。
- `OPENAI_AUTH_MODE=chatgpt` 必须优先于 `UPSTREAM_API_MODEL`,保持现有 ChatGPT OAuth 路径。
- 通用 Responses 模式使用 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL`,不得发送 ChatGPT 专用 headers。
- 生产代码不得使用 `as any`;项目类型检查必须保持零错误。
- 不创建 commit;按仓库约定运行相关 Bun 测试、typecheck、lint。

---

### Task 1: 定义并测试上游协议模式解析

**Files:**
- Create: `src/services/api/openai/upstreamApiMode.ts`
- Create: `src/services/api/openai/__tests__/upstreamApiMode.test.ts`

**Interfaces:**
- Consumes: `process.env.UPSTREAM_API_MODEL`(可选)。
- Produces: `export type UpstreamApiMode = 'chat_completions' | 'responses'` 与 `export function getUpstreamApiMode(value = process.env.UPSTREAM_API_MODEL): UpstreamApiMode`。
- Error contract: 非空未知值抛出 `Error('Invalid UPSTREAM_API_MODEL: <value>. Expected "chat_completions" or "responses".')`。

- [x] **Step 1: Write the failing test**

```ts
import { describe, expect, test } from 'bun:test'
import { getUpstreamApiMode } from '../upstreamApiMode.js'

describe('getUpstreamApiMode', () => {
test('defaults to chat_completions when unset', () => {
expect(getUpstreamApiMode(undefined)).toBe('chat_completions')
})

test('selects responses when explicitly configured', () => {
expect(getUpstreamApiMode('responses')).toBe('responses')
})

test('rejects an unsupported protocol mode', () => {
expect(() => getUpstreamApiMode('completion')).toThrow(
'Invalid UPSTREAM_API_MODEL: completion. Expected "chat_completions" or "responses".',
)
})
})
```

- [x] **Step 2: Run test to verify it fails**

Run:

```bash
bun test src/services/api/openai/__tests__/upstreamApiMode.test.ts
```

Expected: module-not-found failure because `upstreamApiMode.ts` does not exist.

- [x] **Step 3: Write minimal implementation**

```ts
export type UpstreamApiMode = 'chat_completions' | 'responses'

export function getUpstreamApiMode(
value = process.env.UPSTREAM_API_MODEL,
): UpstreamApiMode {
if (value === undefined || value === '' || value === 'chat_completions') {
return 'chat_completions'
}
if (value === 'responses') return 'responses'
throw new Error(
`Invalid UPSTREAM_API_MODEL: ${value}. Expected "chat_completions" or "responses".`,
)
}
```

- [x] **Step 4: Run test to verify it passes**

Run:

```bash
bun test src/services/api/openai/__tests__/upstreamApiMode.test.ts
```

Expected: 3 passing tests.

### Task 2: 新增 API-key Responses SSE transport

**Files:**
- Modify: `src/services/api/openai/responsesAdapter.ts`
- Modify: `src/services/api/openai/__tests__/responsesAdapter.test.ts`

**Interfaces:**
- Consumes: `OPENAI_BASE_URL`、`OPENAI_API_KEY`、`getProxyFetchOptions({ forAnthropicAPI: false })`、`ResponsesRequest`。
- Produces: `export async function createOpenAIResponsesStream(params: { request: ResponsesRequest; signal: AbortSignal; fetchOverride?: typeof fetch }): Promise<AsyncIterable<Record<string, unknown>>>`。
- URL contract: base URL 的尾随 `/` 被移除后拼接 `/responses`;无 base URL 时使用 `https://api.openai.com/v1/responses`。
- Header contract: API Key Bearer、JSON Content-Type 和 SSE Accept;不得发送 `Origin`、`Referer`、`OpenAI-Beta`、`ChatGPT-Account-Id`。

- [x] **Step 1: Write the failing tests**

```ts
test('posts API-key Responses requests to the configured responses endpoint', async () => {
const requests: Request[] = []
const stream = await createOpenAIResponsesStream({
request: { model: 'gpt-5.6-sol', input: 'hello', stream: true },
signal: new AbortController().signal,
fetchOverride: async input => {
requests.push(new Request(input))
return new Response('data: {"type":"response.completed","response":{"status":"completed"}}\n\n', {
headers: { 'Content-Type': 'text/event-stream' },
})
},
})
await Array.fromAsync(stream)
expect(requests[0]?.url).toBe('https://gateway.example/v1/responses')
expect(requests[0]?.headers.get('Authorization')).toBe('Bearer test-key')
expect(requests[0]?.headers.get('Origin')).toBeNull()
})

test('throws an actionable error for a non-success Responses response', async () => {
await expect(
createOpenAIResponsesStream({
request: { model: 'gpt-5.6-sol', input: 'hello', stream: true },
signal: new AbortController().signal,
fetchOverride: async () => new Response('bad model', { status: 400 }),
}),
).rejects.toThrow('OpenAI Responses API request failed (400): bad model')
})
```

Test setup sets `OPENAI_BASE_URL=https://gateway.example/v1/` and `OPENAI_API_KEY=test-key` before each test, then restores both variables after each test.

- [x] **Step 2: Run tests to verify they fail**

Run:

```bash
bun test src/services/api/openai/__tests__/responsesAdapter.test.ts
```

Expected: import failure for `createOpenAIResponsesStream`.

- [x] **Step 3: Write minimal implementation**

Add imports for `getProxyFetchOptions` and `getOpenAIClient`-compatible API configuration helpers only if needed. Implement `createOpenAIResponsesStream` with `fetch`, `process.env.OPENAI_API_KEY`, `process.env.OPENAI_BASE_URL`, `getProxyFetchOptions({ forAnthropicAPI: false })`, and existing `parseSSE(response)`. Use the exact error message asserted above, truncating error text to 500 characters as the existing ChatGPT Responses function does.

- [x] **Step 4: Run tests to verify they pass**

Run:

```bash
bun test src/services/api/openai/__tests__/responsesAdapter.test.ts
```

Expected: all existing adapter tests plus the two Responses transport tests pass.

### Task 3: 依配置路由 queryModelOpenAI 并覆盖优先级

**Files:**
- Modify: `src/services/api/openai/index.ts`
- Modify: `src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts`

**Interfaces:**
- Consumes: `isChatGPTAuthEnabled()`、`getUpstreamApiMode()`、`createChatGPTResponsesStream()`、`createOpenAIResponsesStream()`。
- Produces: 以下优先级的 transport selection:ChatGPT auth → API-key Responses → Chat Completions。
- Error contract: 无效 `UPSTREAM_API_MODEL` 被 `queryModelOpenAI()` 捕获并作为现有 `createAssistantAPIErrorMessage` 输出。

- [x] **Step 1: Write the failing routing tests**

Add mock spies for `createOpenAIResponsesStream` and `getOpenAIClient().chat.completions.create`, then add tests asserting:

```ts
test('uses API-key Responses transport when UPSTREAM_API_MODEL is responses', async () => {
process.env.UPSTREAM_API_MODEL = 'responses'
mockIsChatGPTAuthEnabled(false)
await collectQueryModelOpenAIResult()
expect(createOpenAIResponsesStreamMock).toHaveBeenCalledTimes(1)
expect(chatCompletionsCreateMock).not.toHaveBeenCalled()
})

test('prefers ChatGPT Responses transport over UPSTREAM_API_MODEL', async () => {
process.env.UPSTREAM_API_MODEL = 'responses'
mockIsChatGPTAuthEnabled(true)
await collectQueryModelOpenAIResult()
expect(createChatGPTResponsesStreamMock).toHaveBeenCalledTimes(1)
expect(createOpenAIResponsesStreamMock).not.toHaveBeenCalled()
})

test('keeps Chat Completions as the default transport', async () => {
delete process.env.UPSTREAM_API_MODEL
mockIsChatGPTAuthEnabled(false)
await collectQueryModelOpenAIResult()
expect(chatCompletionsCreateMock).toHaveBeenCalledTimes(1)
expect(createOpenAIResponsesStreamMock).not.toHaveBeenCalled()
})
```

- [x] **Step 2: Run tests to verify they fail**

Run:

```bash
bun test src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts
```

Expected: API-key Responses routing test fails because the current code always reaches Chat Completions when ChatGPT OAuth is disabled.

- [x] **Step 3: Write minimal routing implementation**

In `queryModelOpenAI()`:

```ts
const useChatGPTResponses = isChatGPTAuthEnabled()
const upstreamApiMode = getUpstreamApiMode()
const useApiKeyResponses =
!useChatGPTResponses && upstreamApiMode === 'responses'
```

Select the existing ChatGPT Responses branch first, then call `createOpenAIResponsesStream({ request: buildResponsesRequest(...), signal, fetchOverride })` for `useApiKeyResponses`, adapting both Results streams through `adaptResponsesStreamToAnthropic()`. Keep the existing `adaptOpenAIStreamToAnthropic(getOpenAIClient().chat.completions.create(...))` branch otherwise.

- [x] **Step 4: Run tests to verify they pass**

Run:

```bash
bun test src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts
bun test src/services/api/openai/__tests__/responsesAdapter.test.ts
bun test src/services/api/openai/__tests__/upstreamApiMode.test.ts
```

Expected: all selected tests pass.

### Task 4: 验证无空流静默失败并完成仓库检查

**Files:**
- Modify: `src/services/api/openai/index.ts`
- Modify: `src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts`
- Modify: `docs/superpowers/specs/2026-08-02-openai-responses-api-design.md`

**Interfaces:**
- Consumes: adapted Responses/Chat Completions event stream completion state.
- Produces: 在没有 `message_stop` 且没有有效 assistant 内容时,`queryModelOpenAI()` 产出含说明的 `createAssistantAPIErrorMessage`,不再导致无上下文的 `Execution error`。

- [x] **Step 1: Write the failing no-event test**

```ts
test('emits an API error when an OpenAI stream ends without a terminal message', async () => {
mockAdaptedStream([])
const messages = await collectQueryModelOpenAIResult()
expect(messages).toContainEqual(
expect.objectContaining({
type: 'assistant',
apiError: 'api_error',
message: expect.objectContaining({
content: expect.stringContaining('ended without a terminal message'),
}),
}),
)
})
```

- [x] **Step 2: Run test to verify it fails**

Run:

```bash
bun test src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts
```

Expected: no matching API error because the current OpenAI path completes without yielding a diagnostic message.

- [x] **Step 3: Write minimal implementation**

Track whether `message_stop` was received while consuming `adaptedStream`. After iteration, if no terminal event occurred and no `partialMessage` can be safely assembled, throw `Error('OpenAI stream ended without a terminal message or assistant content')` so the existing catch emits `createAssistantAPIErrorMessage`.

- [x] **Step 4: Run focused checks**

Run:

```bash
bun test src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts
bun test src/services/api/openai/__tests__/responsesAdapter.test.ts
bun test src/services/api/openai/__tests__/upstreamApiMode.test.ts
bun run typecheck
bun run lint
```

Expected: all commands exit 0.

- [x] **Step 5: Update design documentation verification status**

Append a concise verification section to `docs/superpowers/specs/2026-08-02-openai-responses-api-design.md` recording the focused test commands and their result.
71 changes: 71 additions & 0 deletions docs/superpowers/specs/2026-08-02-openai-responses-api-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 通用 OpenAI Responses API 支持设计

## 目标

为 API Key 驱动的 OpenAI 兼容 Provider 增加原生 Responses API 支持,使上游端点可按配置调用 `/responses`,并将其 SSE 事件适配为现有 Anthropic 内部流事件。

## 配置契约

- `UPSTREAM_API_MODEL=chat_completions`:默认值,沿用 `POST {OPENAI_BASE_URL}/chat/completions`。
- `UPSTREAM_API_MODEL=responses`:新增模式,调用 `POST {OPENAI_BASE_URL}/responses`。
- `OPENAI_MODEL`:始终表示上游模型名称,不承担协议选择职责。
- `OPENAI_BASE_URL`:均为 API 基础地址;Responses 模式下其末尾路径为 `/responses`。
- `OPENAI_API_KEY`:Responses 模式使用 Bearer API key。
- `OPENAI_AUTH_MODE=chatgpt`:保留现有 ChatGPT Subscription 专用路径;其固定请求 ChatGPT Codex Responses endpoint,优先级高于 `UPSTREAM_API_MODEL`。

## 请求选择顺序

1. `OPENAI_AUTH_MODE=chatgpt`:调用既有 ChatGPT OAuth Responses 路径。
2. 否则 `UPSTREAM_API_MODEL=responses`:调用新增的 API-key 通用 Responses 路径。
3. 否则:调用现有 Chat Completions 路径。

未知的 `UPSTREAM_API_MODEL` 值在启动/调用时产生明确错误,不静默回退,避免协议错配重新表现为 `Execution error`。

## 实现边界

### 复用

- `buildResponsesRequest()`:把内部消息和工具转换为 Responses 请求体。
- `parseSSE()`:解析 `text/event-stream`。
- `adaptResponsesStreamToAnthropic()`:将 Responses SSE 事件转换为 Anthropic 内部流事件。
- 既有 `getProxyFetchOptions()`:保留代理、证书和超时行为。

### 新增

新增 API-key Responses 客户端函数:

- URL:`{OPENAI_BASE_URL}/responses`;未配置 base URL 时使用 OpenAI SDK 的默认 API 基础地址对应的 `/responses`。
- Headers:`Authorization: Bearer ${OPENAI_API_KEY}`、`Content-Type: application/json`、`Accept: text/event-stream`。
- 不发送 ChatGPT Subscription 专用 headers(`Origin`、`Referer`、`OpenAI-Beta`、`ChatGPT-Account-Id`)。
- 非 2xx 响应抛出包含 HTTP 状态和有限响应体的错误。

### 可靠性

- Responses stream 在未生成 `response.completed` / `response.incomplete` 时应报出明确的 API 错误。
- OpenAI Chat Completions 流保持原有行为。
- 不修改模型选择、鉴权持久化、工具定义或其他 Provider。

## 测试

1. `UPSTREAM_API_MODEL=responses` 使用 API key 请求 `{baseURL}/responses`。
2. 请求头只包含通用 API-key Responses 所需头,不包含 ChatGPT 专用头。
3. Responses SSE 的 text、tool call、完成事件可生成 assistant 消息。
4. 非 2xx Responses 请求转成可见 API error。
5. `chat_completions` 默认路径仍调用 `chat.completions.create()`。
6. `OPENAI_AUTH_MODE=chatgpt` 仍优先使用 ChatGPT Responses 路径。
7. 未知 `UPSTREAM_API_MODEL` 返回明确配置错误。

## 非目标

- 不支持非 OpenAI Responses 事件规范。
- 不变更 ChatGPT Subscription OAuth 协议。
- 不增加新的模型供应商或修改全局 Provider 优先级。


## 验证记录(2026-08-02)

- `bun test src/services/api/openai/__tests__/upstreamApiMode.test.ts`:3 项通过。
- `bun test src/services/api/openai/__tests__/responsesAdapter.test.ts`:11 项通过。
- `bun test ./src/services/api/openai/__tests__/queryModelOpenAI.isolated.ts`:18 项通过。该文件名不符合 Bun 默认测试文件后缀,须以 `./` 路径形式运行。
- `bun run lint`:通过;仅保留仓库已有的 Biome 配置迁移提示及 `contributors.svg` 大文件警告。
- `bun run typecheck`:未能全绿;当前主工作树仅报告 5 个未改动文件中的既有诊断:`src/services/lsp/passiveFeedback.ts` 1 项、`src/utils/bash/commands.ts` 4 项。本次变更涉及的 `src/services/api/openai` 文件未出现在 TypeScript 诊断中。
Loading
Loading