Skip to content

Commit 3240eec

Browse files
committed
feat(web): API Key 页「OpenAI 客户端接入」补 Responses 入口
API Key 页此前只列 chat/completions、user/balance、models 三个端点, Responses API(/v1/responses)只在文档里有——用户在该页照着接入 Codex CLI 时看不到入口。 - 端点列表新增 `POST /v1/responses`(Responses 子集,供 Codex CLI 接入), 可展开出可直接复制的 Codex CLI 配置;示例复用同一 baseUrl + 真实 Key (未创建 Key 时用 sk-… 占位) - 补一条说明:只实现 Codex CLI 用到的子集,与 chat/completions 共用选号 / 冷却 / 轮换 / 会话粘性;store=true、previous_response_id 与服务端私有工具 显式 400,不静默降级 - 测试:新增 Responses 端点展开与复制用例,并把「未创建 Key 时示例用占位符」 「创建后示例带入真实 Key」两条扩展到覆盖 responses 浏览器验证(ego-browser + 临时实例):端点行默认折叠,展开显示 Codex CLI 配置,复制按钮给出示例 URL;说明段落行内 code 正常不拆行。
1 parent 9670788 commit 3240eec

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎web/src/pages/ApiKeysPage.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function openaiExamples(baseUrl: string, apiKey: string): {
4343
curl: string;
4444
python: string;
4545
balance: string;
46+
responses: string;
4647
} {
4748
const key = apiKey || "sk-…";
4849
return {
@@ -64,6 +65,12 @@ resp = client.chat.completions.create(
6465
print(resp.choices[0].message.content)`,
6566
balance: `curl ${baseUrl}/user/balance \\
6667
-H "Authorization: Bearer ${key}"`,
68+
// Codex CLI 用 env_key 引用环境变量名(不是 Key 本身),故先 export 再引用
69+
responses: `export CODING2API_KEY=${key}
70+
codex -c "model_providers.coding2api={ name='coding2api', base_url='${baseUrl}', wire_api='responses', env_key='CODING2API_KEY' }" \\
71+
-c model_provider=coding2api \\
72+
-c model='glm-5.2' \\
73+
'你的任务'`,
6774
};
6875
}
6976

@@ -186,6 +193,11 @@ function OpenAIEntry({ apiKey }: { apiKey: string }) {
186193
<CodeExample label="curl 示例" text={examples.balance} copied={copied === "balance"} onCopy={() => void copy("balance", examples.balance)} testid="example-balance-curl" copyTestid="copy-balance-curl" />
187194
</EndpointRow>
188195
</li>
196+
<li>
197+
<EndpointRow method="POST" path={`${baseUrl}/responses`} description="Responses 子集(供 Codex CLI 接入)" testid="example-details-responses">
198+
<CodeExample label="Codex CLI 配置" text={examples.responses} copied={copied === "responses"} onCopy={() => void copy("responses", examples.responses)} testid="example-responses" copyTestid="copy-responses" />
199+
</EndpointRow>
200+
</li>
189201
<li>
190202
<Badge>GET</Badge> <code>{baseUrl}/models</code> 模型列表
191203
</li>
@@ -194,6 +206,15 @@ function OpenAIEntry({ apiKey }: { apiKey: string }) {
194206
任何兼容 OpenAI 协议的客户端(ChatGPT Next Web、LobeChat、Cursor 等)
195207
都可以按上面的 Base URL + API Key 接入。模型名从 /v1/models 获取。
196208
</Notice>
209+
<Notice tone="muted">
210+
{/* 整段包一层 span:ShadAlert 是 grid 容器,直接放多个行内元素会被拆成多行 */}
211+
<span>
212+
<code>POST /v1/responses</code> 只实现 Codex CLI 用到的子集,与{" "}
213+
<code>/v1/chat/completions</code> 共用同一套选号、冷却、轮换与会话粘性。
214+
<code>store=true</code>、<code>previous_response_id</code> 与{" "}
215+
<code>web_search</code> 等服务端私有工具会被显式拒绝(400),不静默降级。
216+
</span>
217+
</Notice>
197218
</div>
198219
</Panel>
199220
);

‎web/src/pages/__tests__/ApiKeysPage.test.tsx‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,10 @@ describe("OpenAI 客户端接入面板", () => {
209209
`curl ${window.location.origin}/v1/user/balance`,
210210
);
211211
expect(screen.getByTestId("example-balance-curl")).toHaveTextContent("sk-…");
212+
// Responses 端点在此列出,示例默认折叠
213+
expect(screen.getByTestId("example-details-responses")).toHaveTextContent("/responses");
214+
expect(screen.getByTestId("openai-entry")).toHaveTextContent("Codex CLI");
212215
});
213-
214216
it("创建 Key 后示例自动带入真实 Key", async () => {
215217
const fetchSpy = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
216218
const url = String(input);
@@ -231,6 +233,9 @@ describe("OpenAI 客户端接入面板", () => {
231233
});
232234
expect(screen.getByTestId("example-python")).toHaveTextContent('api_key="sk-real-key"');
233235
expect(screen.getByTestId("example-balance-curl")).toHaveTextContent("sk-real-key");
236+
expect(screen.getByTestId("example-responses")).toHaveTextContent(
237+
"export CODING2API_KEY=sk-real-key",
238+
);
234239
});
235240
});
236241

@@ -265,3 +270,29 @@ it("余额端点示例同样默认折叠,展开后可复制 curl", async () =>
265270
-H "Authorization: Bearer sk-…"`,
266271
);
267272
});
273+
274+
it("Responses 端点示例默认折叠,展开后可复制 Codex CLI 配置", async () => {
275+
const writeText = vi.fn(async () => undefined);
276+
vi.stubGlobal("navigator", { clipboard: { writeText } });
277+
mockFetch({ "/api/api-keys": { api_keys: [] } });
278+
renderPage(<ApiKeysPage />);
279+
await settle();
280+
281+
const details = screen.getByTestId("example-details-responses") as HTMLDetailsElement;
282+
expect(details.open).toBe(false);
283+
// <details> 关闭时子节点仍在 DOM 里,只是不可见
284+
expect(screen.getByTestId("example-responses")).not.toBeVisible();
285+
286+
await userEvent.click(details.querySelector("summary")!);
287+
expect(details.open).toBe(true);
288+
expect(screen.getByTestId("example-responses")).toBeVisible();
289+
expect(screen.getByTestId("example-responses")).toHaveTextContent("wire_api='responses'");
290+
expect(screen.getByTestId("example-responses")).toHaveTextContent(
291+
`base_url='${window.location.origin}/v1'`,
292+
);
293+
294+
await userEvent.click(screen.getByTestId("copy-responses"));
295+
expect(writeText).toHaveBeenCalledWith(
296+
expect.stringContaining("export CODING2API_KEY=sk-…"),
297+
);
298+
});

0 commit comments

Comments
 (0)