Spaces:
Paused
Paused
icebear0828 Claude Opus 4.6 commited on
Commit ·
53d3b3b
1
Parent(s): 55594d3
feat: add Structured Outputs support (response_format → text.format)
Browse filesOpenAI response_format (json_object/json_schema) and Gemini
responseMimeType/responseSchema now translate to Codex text.format.
/v1/responses endpoint passes through text field as-is.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CHANGELOG.md +1 -0
- README.md +1 -0
- README_EN.md +1 -0
- src/proxy/codex-api.ts +9 -0
- src/routes/responses.ts +22 -0
- src/translation/gemini-to-codex.ts +20 -0
- src/translation/openai-to-codex.ts +21 -0
- src/types/gemini.ts +2 -0
- src/types/openai.ts +9 -0
CHANGELOG.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
|
| 11 |
- 更新弹窗 + 自动重启:点击"有可用更新"弹出 Modal 显示 changelog,一键更新后服务器自动重启、前端自动刷新,零人工干预(git 模式 spawn 新进程、Docker/Electron 显示对应操作指引)
|
| 12 |
- Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
|
|
|
|
| 13 |
|
| 14 |
### Changed
|
| 15 |
|
|
|
|
| 10 |
|
| 11 |
- 更新弹窗 + 自动重启:点击"有可用更新"弹出 Modal 显示 changelog,一键更新后服务器自动重启、前端自动刷新,零人工干预(git 模式 spawn 新进程、Docker/Electron 显示对应操作指引)
|
| 12 |
- Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
|
| 13 |
+
- Structured Outputs 支持:`/v1/chat/completions` 支持 `response_format`(`json_object` / `json_schema`),Gemini 端点支持 `responseMimeType` + `responseSchema`,自动翻译为 Codex Responses API 的 `text.format`;`/v1/responses` 直通 `text` 字段
|
| 14 |
|
| 15 |
### Changed
|
| 16 |
|
README.md
CHANGED
|
@@ -107,6 +107,7 @@ curl http://localhost:8080/v1/chat/completions \
|
|
| 107 |
- 完全兼容 `/v1/chat/completions`(OpenAI)、`/v1/messages`(Anthropic)和 Gemini 格式
|
| 108 |
- 支持 SSE 流式输出,可直接对接所有 OpenAI SDK 和客户端
|
| 109 |
- 自动完成 Chat Completions ↔ Codex Responses API 双向协议转换
|
|
|
|
| 110 |
|
| 111 |
### 2. 🔐 账号管理与智能轮换 (Auth & Multi-Account)
|
| 112 |
- **OAuth PKCE 登录** — 浏览器一键授权,无需手动复制 Token
|
|
|
|
| 107 |
- 完全兼容 `/v1/chat/completions`(OpenAI)、`/v1/messages`(Anthropic)和 Gemini 格式
|
| 108 |
- 支持 SSE 流式输出,可直接对接所有 OpenAI SDK 和客户端
|
| 109 |
- 自动完成 Chat Completions ↔ Codex Responses API 双向协议转换
|
| 110 |
+
- **Structured Outputs** — 支持 `response_format`(OpenAI `json_object` / `json_schema`)和 Gemini `responseMimeType`,强制 JSON 结构化输出无需提示词
|
| 111 |
|
| 112 |
### 2. 🔐 账号管理与智能轮换 (Auth & Multi-Account)
|
| 113 |
- **OAuth PKCE 登录** — 浏览器一键授权,无需手动复制 Token
|
README_EN.md
CHANGED
|
@@ -103,6 +103,7 @@ curl http://localhost:8080/v1/chat/completions \
|
|
| 103 |
- Compatible with `/v1/chat/completions` (OpenAI), `/v1/messages` (Anthropic), and Gemini formats
|
| 104 |
- SSE streaming output, works with all OpenAI SDKs and clients
|
| 105 |
- Automatic bidirectional translation between Chat Completions and Codex Responses API
|
|
|
|
| 106 |
|
| 107 |
### 2. 🔐 Account Management & Smart Rotation
|
| 108 |
- **OAuth PKCE login** — one-click browser auth, no manual token copying
|
|
|
|
| 103 |
- Compatible with `/v1/chat/completions` (OpenAI), `/v1/messages` (Anthropic), and Gemini formats
|
| 104 |
- SSE streaming output, works with all OpenAI SDKs and clients
|
| 105 |
- Automatic bidirectional translation between Chat Completions and Codex Responses API
|
| 106 |
+
- **Structured Outputs** — supports `response_format` (OpenAI `json_object` / `json_schema`) and Gemini `responseMimeType` for enforcing JSON output without prompt engineering
|
| 107 |
|
| 108 |
### 2. 🔐 Account Management & Smart Rotation
|
| 109 |
- **OAuth PKCE login** — one-click browser auth, no manual token copying
|
src/proxy/codex-api.ts
CHANGED
|
@@ -34,6 +34,15 @@ export interface CodexResponsesRequest {
|
|
| 34 |
tools?: unknown[];
|
| 35 |
/** Optional: tool choice strategy */
|
| 36 |
tool_choice?: string | { type: string; name: string };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
/** Structured content part for multimodal Codex input. */
|
|
|
|
| 34 |
tools?: unknown[];
|
| 35 |
/** Optional: tool choice strategy */
|
| 36 |
tool_choice?: string | { type: string; name: string };
|
| 37 |
+
/** Optional: text output format (JSON mode / structured outputs) */
|
| 38 |
+
text?: {
|
| 39 |
+
format: {
|
| 40 |
+
type: "text" | "json_object" | "json_schema";
|
| 41 |
+
name?: string;
|
| 42 |
+
schema?: Record<string, unknown>;
|
| 43 |
+
strict?: boolean;
|
| 44 |
+
};
|
| 45 |
+
};
|
| 46 |
}
|
| 47 |
|
| 48 |
/** Structured content part for multimodal Codex input. */
|
src/routes/responses.ts
CHANGED
|
@@ -261,6 +261,28 @@ export function createResponsesRoutes(
|
|
| 261 |
codexRequest.tool_choice = body.tool_choice as CodexResponsesRequest["tool_choice"];
|
| 262 |
}
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
// Client can request non-streaming (collect mode), but upstream is always stream
|
| 265 |
const clientWantsStream = body.stream !== false;
|
| 266 |
|
|
|
|
| 261 |
codexRequest.tool_choice = body.tool_choice as CodexResponsesRequest["tool_choice"];
|
| 262 |
}
|
| 263 |
|
| 264 |
+
// Pass through text format (JSON mode / structured outputs) as-is
|
| 265 |
+
if (
|
| 266 |
+
isRecord(body.text) &&
|
| 267 |
+
isRecord(body.text.format) &&
|
| 268 |
+
typeof body.text.format.type === "string"
|
| 269 |
+
) {
|
| 270 |
+
codexRequest.text = {
|
| 271 |
+
format: {
|
| 272 |
+
type: body.text.format.type as "text" | "json_object" | "json_schema",
|
| 273 |
+
...(typeof body.text.format.name === "string"
|
| 274 |
+
? { name: body.text.format.name }
|
| 275 |
+
: {}),
|
| 276 |
+
...(isRecord(body.text.format.schema)
|
| 277 |
+
? { schema: body.text.format.schema as Record<string, unknown> }
|
| 278 |
+
: {}),
|
| 279 |
+
...(typeof body.text.format.strict === "boolean"
|
| 280 |
+
? { strict: body.text.format.strict }
|
| 281 |
+
: {}),
|
| 282 |
+
},
|
| 283 |
+
};
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
// Client can request non-streaming (collect mode), but upstream is always stream
|
| 287 |
const clientWantsStream = body.stream !== false;
|
| 288 |
|
src/translation/gemini-to-codex.ts
CHANGED
|
@@ -232,5 +232,25 @@ export function translateGeminiToCodexRequest(
|
|
| 232 |
request.service_tier = serviceTier;
|
| 233 |
}
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
return request;
|
| 236 |
}
|
|
|
|
| 232 |
request.service_tier = serviceTier;
|
| 233 |
}
|
| 234 |
|
| 235 |
+
// Response format: translate responseMimeType + responseSchema → text.format
|
| 236 |
+
const mimeType = req.generationConfig?.responseMimeType;
|
| 237 |
+
if (mimeType === "application/json") {
|
| 238 |
+
const schema = req.generationConfig?.responseSchema;
|
| 239 |
+
if (schema && Object.keys(schema).length > 0) {
|
| 240 |
+
// Codex strict mode requires additionalProperties: false at root level
|
| 241 |
+
const strictSchema = { additionalProperties: false, ...schema };
|
| 242 |
+
request.text = {
|
| 243 |
+
format: {
|
| 244 |
+
type: "json_schema",
|
| 245 |
+
name: "gemini_schema",
|
| 246 |
+
schema: strictSchema,
|
| 247 |
+
strict: true,
|
| 248 |
+
},
|
| 249 |
+
};
|
| 250 |
+
} else {
|
| 251 |
+
request.text = { format: { type: "json_object" } };
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
return request;
|
| 256 |
}
|
src/translation/openai-to-codex.ts
CHANGED
|
@@ -192,5 +192,26 @@ export function translateToCodexRequest(
|
|
| 192 |
request.service_tier = serviceTier;
|
| 193 |
}
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
return request;
|
| 196 |
}
|
|
|
|
| 192 |
request.service_tier = serviceTier;
|
| 193 |
}
|
| 194 |
|
| 195 |
+
// Response format: translate response_format → text.format
|
| 196 |
+
if (req.response_format && req.response_format.type !== "text") {
|
| 197 |
+
if (req.response_format.type === "json_object") {
|
| 198 |
+
request.text = { format: { type: "json_object" } };
|
| 199 |
+
} else if (
|
| 200 |
+
req.response_format.type === "json_schema" &&
|
| 201 |
+
req.response_format.json_schema
|
| 202 |
+
) {
|
| 203 |
+
request.text = {
|
| 204 |
+
format: {
|
| 205 |
+
type: "json_schema",
|
| 206 |
+
name: req.response_format.json_schema.name,
|
| 207 |
+
schema: req.response_format.json_schema.schema,
|
| 208 |
+
...(req.response_format.json_schema.strict !== undefined
|
| 209 |
+
? { strict: req.response_format.json_schema.strict }
|
| 210 |
+
: {}),
|
| 211 |
+
},
|
| 212 |
+
};
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
return request;
|
| 217 |
}
|
src/types/gemini.ts
CHANGED
|
@@ -40,6 +40,8 @@ const GeminiGenerationConfigSchema = z.object({
|
|
| 40 |
maxOutputTokens: z.number().optional(),
|
| 41 |
stopSequences: z.array(z.string()).optional(),
|
| 42 |
thinkingConfig: GeminiThinkingConfigSchema.optional(),
|
|
|
|
|
|
|
| 43 |
});
|
| 44 |
|
| 45 |
export const GeminiGenerateContentRequestSchema = z.object({
|
|
|
|
| 40 |
maxOutputTokens: z.number().optional(),
|
| 41 |
stopSequences: z.array(z.string()).optional(),
|
| 42 |
thinkingConfig: GeminiThinkingConfigSchema.optional(),
|
| 43 |
+
responseMimeType: z.string().optional(),
|
| 44 |
+
responseSchema: z.record(z.unknown()).optional(),
|
| 45 |
});
|
| 46 |
|
| 47 |
export const GeminiGenerateContentRequestSchema = z.object({
|
src/types/openai.ts
CHANGED
|
@@ -60,6 +60,15 @@ export const ChatCompletionRequestSchema = z.object({
|
|
| 60 |
z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) }),
|
| 61 |
]).optional(),
|
| 62 |
parallel_tool_calls: z.boolean().optional(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
// Legacy function format (accepted for compatibility, not forwarded to Codex)
|
| 64 |
functions: z.array(z.object({
|
| 65 |
name: z.string(),
|
|
|
|
| 60 |
z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) }),
|
| 61 |
]).optional(),
|
| 62 |
parallel_tool_calls: z.boolean().optional(),
|
| 63 |
+
// Structured output format (JSON mode / JSON Schema)
|
| 64 |
+
response_format: z.object({
|
| 65 |
+
type: z.enum(["text", "json_object", "json_schema"]),
|
| 66 |
+
json_schema: z.object({
|
| 67 |
+
name: z.string(),
|
| 68 |
+
schema: z.record(z.unknown()),
|
| 69 |
+
strict: z.boolean().optional(),
|
| 70 |
+
}).optional(),
|
| 71 |
+
}).optional(),
|
| 72 |
// Legacy function format (accepted for compatibility, not forwarded to Codex)
|
| 73 |
functions: z.array(z.object({
|
| 74 |
name: z.string(),
|