ai-zero-token 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -70
- package/dist/api.js +0 -1
- package/dist/cli/commands/ask.js +131 -5
- package/dist/cli/commands/clear.js +0 -1
- package/dist/cli/commands/help.js +15 -10
- package/dist/cli/commands/login.js +0 -1
- package/dist/cli/commands/models.js +0 -1
- package/dist/cli/commands/serve.js +41 -4
- package/dist/cli/commands/start.js +10 -0
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/shared.js +57 -6
- package/dist/cli.js +0 -1
- package/dist/core/context.js +7 -2
- package/dist/core/models/openai-codex-models.js +0 -1
- package/dist/core/providers/http-client.js +97 -9
- package/dist/core/providers/openai-codex/chat.js +217 -24
- package/dist/core/providers/openai-codex/oauth.js +15 -4
- package/dist/core/providers/openai-codex/pkce.js +0 -1
- package/dist/core/services/auth-service.js +89 -16
- package/dist/core/services/chat-service.js +24 -14
- package/dist/core/services/config-service.js +0 -1
- package/dist/core/services/image-service.js +360 -0
- package/dist/core/services/model-service.js +4 -2
- package/dist/core/store/profile-store.js +79 -6
- package/dist/core/store/settings-store.js +1 -2
- package/dist/core/types.js +0 -1
- package/dist/http.js +0 -1
- package/dist/models.js +0 -1
- package/dist/oauth.js +0 -1
- package/dist/pkce.js +0 -1
- package/dist/server/admin-page.js +2615 -0
- package/dist/server/app.js +561 -39
- package/dist/server/index.js +0 -1
- package/dist/store.js +0 -1
- package/package.json +12 -3
- package/dist/api.js.map +0 -1
- package/dist/cli/commands/ask.js.map +0 -1
- package/dist/cli/commands/clear.js.map +0 -1
- package/dist/cli/commands/help.js.map +0 -1
- package/dist/cli/commands/login.js.map +0 -1
- package/dist/cli/commands/models.js.map +0 -1
- package/dist/cli/commands/serve.js.map +0 -1
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/shared.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/core/context.js.map +0 -1
- package/dist/core/models/openai-codex-models.js.map +0 -1
- package/dist/core/providers/http-client.js.map +0 -1
- package/dist/core/providers/openai-codex/chat.js.map +0 -1
- package/dist/core/providers/openai-codex/oauth.js.map +0 -1
- package/dist/core/providers/openai-codex/pkce.js.map +0 -1
- package/dist/core/services/auth-service.js.map +0 -1
- package/dist/core/services/chat-service.js.map +0 -1
- package/dist/core/services/config-service.js.map +0 -1
- package/dist/core/services/model-service.js.map +0 -1
- package/dist/core/store/profile-store.js.map +0 -1
- package/dist/core/store/settings-store.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/models.js.map +0 -1
- package/dist/oauth.js.map +0 -1
- package/dist/pkce.js.map +0 -1
- package/dist/server/app.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/store.js.map +0 -1
package/dist/server/app.js
CHANGED
|
@@ -1,28 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import Fastify from "fastify";
|
|
3
4
|
import cors from "@fastify/cors";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { createGatewayContext } from "../core/context.js";
|
|
7
|
+
import { renderAdminPage } from "./admin-page.js";
|
|
8
|
+
const inputPartSchema = z.object({
|
|
9
|
+
type: z.string().optional(),
|
|
10
|
+
text: z.string().optional()
|
|
11
|
+
}).passthrough();
|
|
12
|
+
const inputMessageSchema = z.object({
|
|
13
|
+
role: z.string().optional(),
|
|
14
|
+
content: z.array(inputPartSchema).optional()
|
|
15
|
+
}).passthrough();
|
|
6
16
|
const responsesBodySchema = z.object({
|
|
7
17
|
model: z.string().optional(),
|
|
8
|
-
input: z.union([
|
|
9
|
-
z.string(),
|
|
10
|
-
z.array(
|
|
11
|
-
z.object({
|
|
12
|
-
role: z.string().optional(),
|
|
13
|
-
content: z.array(
|
|
14
|
-
z.object({
|
|
15
|
-
type: z.string().optional(),
|
|
16
|
-
text: z.string().optional()
|
|
17
|
-
})
|
|
18
|
-
).optional()
|
|
19
|
-
})
|
|
20
|
-
)
|
|
21
|
-
]),
|
|
18
|
+
input: z.union([z.string(), z.array(inputMessageSchema)]).optional(),
|
|
22
19
|
instructions: z.string().optional(),
|
|
23
|
-
stream: z.boolean().optional()
|
|
20
|
+
stream: z.boolean().optional(),
|
|
21
|
+
tools: z.array(z.unknown()).optional(),
|
|
22
|
+
tool_choice: z.unknown().optional(),
|
|
23
|
+
include: z.array(z.string()).optional(),
|
|
24
|
+
text: z.record(z.string(), z.unknown()).optional(),
|
|
25
|
+
store: z.boolean().optional(),
|
|
26
|
+
parallel_tool_calls: z.boolean().optional(),
|
|
27
|
+
experimental_codex: z.object({
|
|
28
|
+
body: z.record(z.string(), z.unknown()).optional(),
|
|
29
|
+
allow_unknown_model: z.boolean().optional(),
|
|
30
|
+
include_raw: z.boolean().optional()
|
|
31
|
+
}).passthrough().optional()
|
|
32
|
+
});
|
|
33
|
+
const chatCompletionContentPartSchema = z.object({
|
|
34
|
+
type: z.string().optional(),
|
|
35
|
+
text: z.string().optional(),
|
|
36
|
+
image_url: z.union([
|
|
37
|
+
z.string(),
|
|
38
|
+
z.object({
|
|
39
|
+
url: z.string().optional()
|
|
40
|
+
}).passthrough()
|
|
41
|
+
]).optional()
|
|
42
|
+
}).passthrough();
|
|
43
|
+
const chatCompletionMessageSchema = z.object({
|
|
44
|
+
role: z.string().optional(),
|
|
45
|
+
content: z.union([z.string(), z.array(chatCompletionContentPartSchema)]).optional(),
|
|
46
|
+
name: z.string().optional(),
|
|
47
|
+
tool_call_id: z.string().optional()
|
|
48
|
+
}).passthrough();
|
|
49
|
+
const chatCompletionsBodySchema = z.object({
|
|
50
|
+
model: z.string().optional(),
|
|
51
|
+
messages: z.array(chatCompletionMessageSchema).min(1),
|
|
52
|
+
n: z.number().int().positive().optional(),
|
|
53
|
+
stream: z.boolean().optional(),
|
|
54
|
+
tools: z.array(z.unknown()).optional(),
|
|
55
|
+
tool_choice: z.unknown().optional(),
|
|
56
|
+
response_format: z.unknown().optional(),
|
|
57
|
+
parallel_tool_calls: z.boolean().optional(),
|
|
58
|
+
store: z.boolean().optional(),
|
|
59
|
+
temperature: z.number().optional(),
|
|
60
|
+
top_p: z.number().optional(),
|
|
61
|
+
max_tokens: z.number().optional(),
|
|
62
|
+
max_completion_tokens: z.number().optional(),
|
|
63
|
+
presence_penalty: z.number().optional(),
|
|
64
|
+
frequency_penalty: z.number().optional(),
|
|
65
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
66
|
+
stop: z.union([z.string(), z.array(z.string())]).optional(),
|
|
67
|
+
user: z.string().optional()
|
|
68
|
+
}).passthrough();
|
|
69
|
+
const settingsUpdateSchema = z.object({
|
|
70
|
+
defaultModel: z.string().min(1)
|
|
71
|
+
});
|
|
72
|
+
const profileActionSchema = z.object({
|
|
73
|
+
profileId: z.string().min(1)
|
|
24
74
|
});
|
|
75
|
+
const imageGenerationsBodySchema = z.object({
|
|
76
|
+
prompt: z.string().min(1),
|
|
77
|
+
model: z.string().optional(),
|
|
78
|
+
n: z.number().int().positive().optional(),
|
|
79
|
+
quality: z.enum(["low", "medium", "high", "auto"]).optional(),
|
|
80
|
+
size: z.enum(["1024x1024", "1024x1536", "1536x1024", "auto"]).optional(),
|
|
81
|
+
background: z.enum(["transparent", "opaque", "auto"]).optional(),
|
|
82
|
+
output_format: z.enum(["png", "webp", "jpeg"]).optional(),
|
|
83
|
+
output_compression: z.number().int().min(0).max(100).optional(),
|
|
84
|
+
moderation: z.enum(["auto", "low"]).optional(),
|
|
85
|
+
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
86
|
+
user: z.string().optional()
|
|
87
|
+
}).passthrough();
|
|
88
|
+
const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
|
|
89
|
+
"messages",
|
|
90
|
+
"n",
|
|
91
|
+
"stream",
|
|
92
|
+
"max_tokens",
|
|
93
|
+
"max_completion_tokens"
|
|
94
|
+
]);
|
|
25
95
|
function extractTextInput(input) {
|
|
96
|
+
if (typeof input === "undefined") {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
26
99
|
if (typeof input === "string") {
|
|
27
100
|
return input;
|
|
28
101
|
}
|
|
@@ -36,6 +109,204 @@ function extractTextInput(input) {
|
|
|
36
109
|
}
|
|
37
110
|
return chunks.join("\n").trim();
|
|
38
111
|
}
|
|
112
|
+
function normalizeResponseInput(input) {
|
|
113
|
+
if (typeof input === "undefined") {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
if (typeof input === "string") {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
role: "user",
|
|
120
|
+
content: [{ type: "input_text", text: input }]
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
return input;
|
|
125
|
+
}
|
|
126
|
+
function normalizeChatRole(role) {
|
|
127
|
+
if (role === "developer") {
|
|
128
|
+
return "system";
|
|
129
|
+
}
|
|
130
|
+
return role ?? "user";
|
|
131
|
+
}
|
|
132
|
+
function normalizeChatContentPart(part) {
|
|
133
|
+
if (part.type === "image_url") {
|
|
134
|
+
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
135
|
+
if (!url) {
|
|
136
|
+
throw new Error("chat.completions \u6D88\u606F\u91CC\u7684 image_url \u7F3A\u5C11 url\u3002");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
type: "input_image",
|
|
140
|
+
image_url: url
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (part.type === "input_image") {
|
|
144
|
+
return part;
|
|
145
|
+
}
|
|
146
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
147
|
+
return {
|
|
148
|
+
type: "input_text",
|
|
149
|
+
text
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function normalizeChatContent(content) {
|
|
153
|
+
if (typeof content === "string") {
|
|
154
|
+
return [{ type: "input_text", text: content }];
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
157
|
+
return [{ type: "input_text", text: "" }];
|
|
158
|
+
}
|
|
159
|
+
return content.map((part) => normalizeChatContentPart(part));
|
|
160
|
+
}
|
|
161
|
+
function normalizeChatMessages(messages) {
|
|
162
|
+
return messages.map((message) => ({
|
|
163
|
+
role: normalizeChatRole(message.role),
|
|
164
|
+
content: normalizeChatContent(message.content),
|
|
165
|
+
...message.name ? { name: message.name } : {},
|
|
166
|
+
...message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
function createChatCompletionsCodexBody(data) {
|
|
170
|
+
const body = Object.fromEntries(
|
|
171
|
+
Object.entries(data).filter(([key]) => !chatCompletionExcludedKeys.has(key))
|
|
172
|
+
);
|
|
173
|
+
if (typeof data.max_completion_tokens === "number") {
|
|
174
|
+
body.max_output_tokens = data.max_completion_tokens;
|
|
175
|
+
} else if (typeof data.max_tokens === "number") {
|
|
176
|
+
body.max_output_tokens = data.max_tokens;
|
|
177
|
+
}
|
|
178
|
+
body.input = normalizeChatMessages(data.messages);
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
181
|
+
function summarizeImageRequestForLog(body) {
|
|
182
|
+
return {
|
|
183
|
+
model: body.model ?? "default",
|
|
184
|
+
promptLength: body.prompt.length,
|
|
185
|
+
size: body.size ?? "default",
|
|
186
|
+
quality: body.quality ?? "default",
|
|
187
|
+
background: body.background ?? "default",
|
|
188
|
+
output_format: body.output_format ?? "default",
|
|
189
|
+
output_compression: typeof body.output_compression === "number" ? body.output_compression : void 0,
|
|
190
|
+
moderation: body.moderation ?? "default",
|
|
191
|
+
response_format: body.response_format ?? "default",
|
|
192
|
+
user: body.user ?? void 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function buildResponseApiBody(result, includeRaw) {
|
|
196
|
+
const responseBody = {
|
|
197
|
+
object: "response",
|
|
198
|
+
provider: result.provider,
|
|
199
|
+
model: result.model,
|
|
200
|
+
output_text: result.text,
|
|
201
|
+
output: [
|
|
202
|
+
{
|
|
203
|
+
type: "message",
|
|
204
|
+
role: "assistant",
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "output_text",
|
|
208
|
+
text: result.text
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
};
|
|
214
|
+
if (result.artifacts.length > 0) {
|
|
215
|
+
responseBody.artifacts = result.artifacts;
|
|
216
|
+
}
|
|
217
|
+
if (includeRaw) {
|
|
218
|
+
responseBody.raw = result.raw;
|
|
219
|
+
}
|
|
220
|
+
return responseBody;
|
|
221
|
+
}
|
|
222
|
+
function buildChatCompletionsBody(result) {
|
|
223
|
+
const body = {
|
|
224
|
+
id: `chatcmpl_${randomUUID().replace(/-/g, "")}`,
|
|
225
|
+
object: "chat.completion",
|
|
226
|
+
created: Math.floor(Date.now() / 1e3),
|
|
227
|
+
model: result.model,
|
|
228
|
+
choices: [
|
|
229
|
+
{
|
|
230
|
+
index: 0,
|
|
231
|
+
finish_reason: "stop",
|
|
232
|
+
message: {
|
|
233
|
+
role: "assistant",
|
|
234
|
+
content: result.text
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
if (result.artifacts.length > 0) {
|
|
240
|
+
body.artifacts = result.artifacts;
|
|
241
|
+
}
|
|
242
|
+
return body;
|
|
243
|
+
}
|
|
244
|
+
function validateImageRequest(data) {
|
|
245
|
+
if (data.response_format === "url") {
|
|
246
|
+
return "\u5F53\u524D\u7F51\u5173\u4EC5\u652F\u6301 response_format=b64_json\uFF0C\u6682\u4E0D\u652F\u6301\u8FD4\u56DE\u6258\u7BA1\u56FE\u7247 URL\u3002";
|
|
247
|
+
}
|
|
248
|
+
if (data.background === "transparent" && typeof data.output_format === "string" && !["png", "webp"].includes(data.output_format)) {
|
|
249
|
+
return "transparent \u80CC\u666F\u4EC5\u652F\u6301 output_format=png \u6216 webp\u3002";
|
|
250
|
+
}
|
|
251
|
+
if (typeof data.output_compression === "number" && data.output_format === "png") {
|
|
252
|
+
return "output_compression \u4EC5\u652F\u6301 jpeg \u6216 webp \u8F93\u51FA\u3002";
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function maskSecret(value) {
|
|
257
|
+
if (value.length <= 12) {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
return `${value.slice(0, 8)}...${value.slice(-6)}`;
|
|
261
|
+
}
|
|
262
|
+
function serializeProfile(profile) {
|
|
263
|
+
if (!profile) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
provider: profile.provider,
|
|
268
|
+
profileId: profile.profileId,
|
|
269
|
+
accountId: profile.accountId,
|
|
270
|
+
email: profile.email,
|
|
271
|
+
quota: profile.quota,
|
|
272
|
+
expiresAt: profile.expires,
|
|
273
|
+
accessTokenPreview: maskSecret(profile.access),
|
|
274
|
+
refreshTokenPreview: maskSecret(profile.refresh)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function serializeManagedProfile(profile) {
|
|
278
|
+
return {
|
|
279
|
+
provider: profile.provider,
|
|
280
|
+
profileId: profile.profileId,
|
|
281
|
+
accountId: profile.accountId,
|
|
282
|
+
email: profile.email,
|
|
283
|
+
quota: profile.quota,
|
|
284
|
+
expiresAt: profile.expiresAt,
|
|
285
|
+
accessTokenPreview: profile.accessTokenPreview,
|
|
286
|
+
refreshTokenPreview: profile.refreshTokenPreview,
|
|
287
|
+
isActive: profile.isActive
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function resolveOrigin(request) {
|
|
291
|
+
const host = request.headers.host;
|
|
292
|
+
if (host) {
|
|
293
|
+
return `${request.protocol}://${host}`;
|
|
294
|
+
}
|
|
295
|
+
return "http://127.0.0.1:8787";
|
|
296
|
+
}
|
|
297
|
+
function normalizeError(error) {
|
|
298
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
299
|
+
}
|
|
300
|
+
function getErrorStatusCode(error) {
|
|
301
|
+
const message = normalizeError(error).message;
|
|
302
|
+
if (message.includes("\u7F3A\u5C11") || message.includes("\u683C\u5F0F\u9519\u8BEF") || message.includes("\u672A\u5185\u7F6E\u6A21\u578B") || message.includes("\u4E0D\u652F\u6301") || message.includes("\u6CA1\u6709\u63D0\u4F9B")) {
|
|
303
|
+
return 400;
|
|
304
|
+
}
|
|
305
|
+
if (message.includes("\u8FD8\u6CA1\u6709\u767B\u5F55")) {
|
|
306
|
+
return 401;
|
|
307
|
+
}
|
|
308
|
+
return 500;
|
|
309
|
+
}
|
|
39
310
|
function createApp(params) {
|
|
40
311
|
const app = Fastify({
|
|
41
312
|
logger: false
|
|
@@ -43,13 +314,131 @@ function createApp(params) {
|
|
|
43
314
|
const ctx = createGatewayContext();
|
|
44
315
|
void app.register(cors, {
|
|
45
316
|
origin: params?.corsOrigin ?? true,
|
|
46
|
-
methods: ["GET", "POST", "OPTIONS"]
|
|
317
|
+
methods: ["GET", "POST", "PUT", "OPTIONS"]
|
|
318
|
+
});
|
|
319
|
+
app.setErrorHandler((error, request, reply) => {
|
|
320
|
+
const normalized = normalizeError(error);
|
|
321
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
322
|
+
console.error("[gateway:error]", {
|
|
323
|
+
method: request.method,
|
|
324
|
+
url: request.url,
|
|
325
|
+
statusCode,
|
|
326
|
+
message: normalized.message,
|
|
327
|
+
stack: normalized.stack
|
|
328
|
+
});
|
|
329
|
+
reply.code(statusCode);
|
|
330
|
+
return {
|
|
331
|
+
error: {
|
|
332
|
+
type: "gateway_error",
|
|
333
|
+
message: normalized.message
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
async function buildAdminConfig(request) {
|
|
338
|
+
const [status, models, settings, profile, profiles] = await Promise.all([
|
|
339
|
+
ctx.authService.getStatus(),
|
|
340
|
+
ctx.modelService.listModels(),
|
|
341
|
+
ctx.configService.getSettings(),
|
|
342
|
+
ctx.authService.getActiveProfile(),
|
|
343
|
+
ctx.authService.listProfiles()
|
|
344
|
+
]);
|
|
345
|
+
const origin = resolveOrigin(request);
|
|
346
|
+
return {
|
|
347
|
+
status,
|
|
348
|
+
settings,
|
|
349
|
+
models,
|
|
350
|
+
profile: serializeProfile(profile),
|
|
351
|
+
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
352
|
+
adminUrl: `${origin}/`,
|
|
353
|
+
baseUrl: `${origin}/v1`,
|
|
354
|
+
supportedEndpoints: [
|
|
355
|
+
{
|
|
356
|
+
method: "GET",
|
|
357
|
+
path: "/v1/models",
|
|
358
|
+
description: "OpenAI models \u5217\u8868\u517C\u5BB9\u63A5\u53E3\u3002"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
path: "/v1/responses",
|
|
363
|
+
description: "OpenAI responses \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
method: "POST",
|
|
367
|
+
path: "/v1/chat/completions",
|
|
368
|
+
description: "OpenAI chat.completions \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
method: "POST",
|
|
372
|
+
path: "/v1/images/generations",
|
|
373
|
+
description: "OpenAI images.generations \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
app.get("/", async (_request, reply) => {
|
|
379
|
+
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
380
|
+
return renderAdminPage();
|
|
381
|
+
});
|
|
382
|
+
app.get("/favicon.ico", async (_request, reply) => {
|
|
383
|
+
reply.code(204);
|
|
384
|
+
return "";
|
|
47
385
|
});
|
|
48
386
|
app.get("/_gateway/health", async () => ({ ok: true }));
|
|
49
387
|
app.get("/_gateway/status", async () => ctx.authService.getStatus());
|
|
50
388
|
app.get("/_gateway/models", async () => ({
|
|
51
389
|
data: await ctx.modelService.listModels()
|
|
52
390
|
}));
|
|
391
|
+
app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
|
|
392
|
+
app.post("/_gateway/admin/login", async (request) => {
|
|
393
|
+
await ctx.authService.login("openai-codex");
|
|
394
|
+
return buildAdminConfig(request);
|
|
395
|
+
});
|
|
396
|
+
app.post("/_gateway/admin/logout", async (request) => {
|
|
397
|
+
await ctx.authService.logoutAll();
|
|
398
|
+
return buildAdminConfig(request);
|
|
399
|
+
});
|
|
400
|
+
app.post("/_gateway/admin/profiles/activate", async (request, reply) => {
|
|
401
|
+
const parsed = profileActionSchema.safeParse(request.body);
|
|
402
|
+
if (!parsed.success) {
|
|
403
|
+
reply.code(400);
|
|
404
|
+
return {
|
|
405
|
+
error: {
|
|
406
|
+
type: "validation_error",
|
|
407
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
await ctx.authService.activateProfile(parsed.data.profileId);
|
|
412
|
+
return buildAdminConfig(request);
|
|
413
|
+
});
|
|
414
|
+
app.post("/_gateway/admin/profiles/remove", async (request, reply) => {
|
|
415
|
+
const parsed = profileActionSchema.safeParse(request.body);
|
|
416
|
+
if (!parsed.success) {
|
|
417
|
+
reply.code(400);
|
|
418
|
+
return {
|
|
419
|
+
error: {
|
|
420
|
+
type: "validation_error",
|
|
421
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
await ctx.authService.removeProfile(parsed.data.profileId);
|
|
426
|
+
return buildAdminConfig(request);
|
|
427
|
+
});
|
|
428
|
+
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
429
|
+
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
430
|
+
if (!parsed.success) {
|
|
431
|
+
reply.code(400);
|
|
432
|
+
return {
|
|
433
|
+
error: {
|
|
434
|
+
type: "validation_error",
|
|
435
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
await ctx.configService.setDefaultModel(parsed.data.defaultModel);
|
|
440
|
+
return buildAdminConfig(request);
|
|
441
|
+
});
|
|
53
442
|
app.get("/v1/models", async () => ({
|
|
54
443
|
object: "list",
|
|
55
444
|
data: (await ctx.modelService.listModels()).map((model) => ({
|
|
@@ -74,47 +463,180 @@ function createApp(params) {
|
|
|
74
463
|
return {
|
|
75
464
|
error: {
|
|
76
465
|
type: "not_supported",
|
|
77
|
-
message: "\
|
|
466
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 stream=true"
|
|
78
467
|
}
|
|
79
468
|
};
|
|
80
469
|
}
|
|
81
470
|
const input = extractTextInput(parsed.data.input);
|
|
82
|
-
|
|
471
|
+
const hasInput = typeof parsed.data.input !== "undefined" || typeof parsed.data.experimental_codex?.body?.input !== "undefined";
|
|
472
|
+
if (!hasInput) {
|
|
83
473
|
reply.code(400);
|
|
84
474
|
return {
|
|
85
475
|
error: {
|
|
86
476
|
type: "validation_error",
|
|
87
|
-
message: "\u6CA1\u6709\
|
|
477
|
+
message: "\u6CA1\u6709\u63D0\u4F9B input\uFF0C\u4E5F\u6CA1\u6709\u5728 experimental_codex.body \u91CC\u900F\u4F20 input"
|
|
88
478
|
}
|
|
89
479
|
};
|
|
90
480
|
}
|
|
481
|
+
const codexBody = {
|
|
482
|
+
...parsed.data.experimental_codex?.body ?? {}
|
|
483
|
+
};
|
|
484
|
+
const normalizedInput = normalizeResponseInput(parsed.data.input);
|
|
485
|
+
if (typeof normalizedInput !== "undefined") {
|
|
486
|
+
codexBody.input = normalizedInput;
|
|
487
|
+
}
|
|
488
|
+
if (typeof parsed.data.instructions === "string") {
|
|
489
|
+
codexBody.instructions = parsed.data.instructions;
|
|
490
|
+
}
|
|
491
|
+
if (parsed.data.tools) {
|
|
492
|
+
codexBody.tools = parsed.data.tools;
|
|
493
|
+
}
|
|
494
|
+
if (typeof parsed.data.tool_choice !== "undefined") {
|
|
495
|
+
codexBody.tool_choice = parsed.data.tool_choice;
|
|
496
|
+
}
|
|
497
|
+
if (parsed.data.include) {
|
|
498
|
+
codexBody.include = parsed.data.include;
|
|
499
|
+
}
|
|
500
|
+
if (parsed.data.text) {
|
|
501
|
+
codexBody.text = parsed.data.text;
|
|
502
|
+
}
|
|
503
|
+
if (typeof parsed.data.store === "boolean") {
|
|
504
|
+
codexBody.store = parsed.data.store;
|
|
505
|
+
}
|
|
506
|
+
if (typeof parsed.data.parallel_tool_calls === "boolean") {
|
|
507
|
+
codexBody.parallel_tool_calls = parsed.data.parallel_tool_calls;
|
|
508
|
+
}
|
|
91
509
|
const result = await ctx.chatService.chat({
|
|
92
510
|
model: parsed.data.model,
|
|
93
|
-
input,
|
|
94
|
-
system: parsed.data.instructions
|
|
511
|
+
input: input || void 0,
|
|
512
|
+
system: parsed.data.instructions,
|
|
513
|
+
experimental: {
|
|
514
|
+
codexBody,
|
|
515
|
+
allowUnknownModel: parsed.data.experimental_codex?.allow_unknown_model
|
|
516
|
+
}
|
|
95
517
|
});
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
type: "output_text",
|
|
108
|
-
text: result.text
|
|
109
|
-
}
|
|
110
|
-
]
|
|
518
|
+
return buildResponseApiBody(result, parsed.data.experimental_codex?.include_raw);
|
|
519
|
+
});
|
|
520
|
+
app.post("/v1/chat/completions", async (request, reply) => {
|
|
521
|
+
const parsed = chatCompletionsBodySchema.safeParse(request.body);
|
|
522
|
+
if (!parsed.success) {
|
|
523
|
+
reply.code(400);
|
|
524
|
+
return {
|
|
525
|
+
error: {
|
|
526
|
+
type: "validation_error",
|
|
527
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
111
528
|
}
|
|
112
|
-
|
|
113
|
-
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (parsed.data.stream) {
|
|
532
|
+
reply.code(501);
|
|
533
|
+
return {
|
|
534
|
+
error: {
|
|
535
|
+
type: "not_supported",
|
|
536
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 chat.completions \u7684 stream=true"
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
|
|
541
|
+
reply.code(501);
|
|
542
|
+
return {
|
|
543
|
+
error: {
|
|
544
|
+
type: "not_supported",
|
|
545
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301\u4E00\u6B21\u8FD4\u56DE\u591A\u4E2A choices\uFF08n > 1\uFF09"
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const codexBody = createChatCompletionsCodexBody(parsed.data);
|
|
550
|
+
const fallbackInput = parsed.data.messages.map(
|
|
551
|
+
(message) => typeof message.content === "string" ? message.content : (message.content ?? []).map((part) => typeof part.text === "string" ? part.text : "").filter(Boolean).join("\n")
|
|
552
|
+
).filter(Boolean).join("\n").trim();
|
|
553
|
+
const result = await ctx.chatService.chat({
|
|
554
|
+
model: parsed.data.model,
|
|
555
|
+
input: fallbackInput || void 0,
|
|
556
|
+
experimental: {
|
|
557
|
+
codexBody
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
return buildChatCompletionsBody(result);
|
|
561
|
+
});
|
|
562
|
+
app.post("/v1/images/generations", async (request, reply) => {
|
|
563
|
+
const parsed = imageGenerationsBodySchema.safeParse(request.body);
|
|
564
|
+
if (!parsed.success) {
|
|
565
|
+
console.error("[gateway:image] validation failure", {
|
|
566
|
+
method: request.method,
|
|
567
|
+
url: request.url,
|
|
568
|
+
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
569
|
+
});
|
|
570
|
+
reply.code(400);
|
|
571
|
+
return {
|
|
572
|
+
error: {
|
|
573
|
+
type: "validation_error",
|
|
574
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const validationError = validateImageRequest(parsed.data);
|
|
579
|
+
if (validationError) {
|
|
580
|
+
console.error("[gateway:image] validation failure", {
|
|
581
|
+
method: request.method,
|
|
582
|
+
url: request.url,
|
|
583
|
+
summary: summarizeImageRequestForLog(parsed.data),
|
|
584
|
+
issue: validationError
|
|
585
|
+
});
|
|
586
|
+
reply.code(400);
|
|
587
|
+
return {
|
|
588
|
+
error: {
|
|
589
|
+
type: "validation_error",
|
|
590
|
+
message: validationError
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
|
|
595
|
+
console.error("[gateway:image] not supported", {
|
|
596
|
+
method: request.method,
|
|
597
|
+
url: request.url,
|
|
598
|
+
summary: summarizeImageRequestForLog(parsed.data),
|
|
599
|
+
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
600
|
+
});
|
|
601
|
+
reply.code(501);
|
|
602
|
+
return {
|
|
603
|
+
error: {
|
|
604
|
+
type: "not_supported",
|
|
605
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.generations \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const requestSummary = summarizeImageRequestForLog(parsed.data);
|
|
610
|
+
console.info("[gateway:image] request accepted", {
|
|
611
|
+
method: request.method,
|
|
612
|
+
url: request.url,
|
|
613
|
+
summary: requestSummary
|
|
614
|
+
});
|
|
615
|
+
const response = await ctx.imageService.generate({
|
|
616
|
+
prompt: parsed.data.prompt,
|
|
617
|
+
model: parsed.data.model,
|
|
618
|
+
n: parsed.data.n,
|
|
619
|
+
size: parsed.data.size,
|
|
620
|
+
quality: parsed.data.quality,
|
|
621
|
+
background: parsed.data.background,
|
|
622
|
+
outputFormat: parsed.data.output_format,
|
|
623
|
+
outputCompression: parsed.data.output_compression,
|
|
624
|
+
moderation: parsed.data.moderation
|
|
625
|
+
});
|
|
626
|
+
console.info("[gateway:image] response ready", {
|
|
627
|
+
method: request.method,
|
|
628
|
+
url: request.url,
|
|
629
|
+
summary: requestSummary,
|
|
630
|
+
created: response.created,
|
|
631
|
+
imageCount: response.data.length,
|
|
632
|
+
output_format: response.output_format,
|
|
633
|
+
quality: response.quality,
|
|
634
|
+
size: response.size
|
|
635
|
+
});
|
|
636
|
+
return response;
|
|
114
637
|
});
|
|
115
638
|
return app;
|
|
116
639
|
}
|
|
117
640
|
export {
|
|
118
641
|
createApp
|
|
119
642
|
};
|
|
120
|
-
//# sourceMappingURL=app.js.map
|
package/dist/server/index.js
CHANGED
package/dist/store.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-zero-token",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -22,7 +22,14 @@
|
|
|
22
22
|
"gateway",
|
|
23
23
|
"oauth",
|
|
24
24
|
"openai",
|
|
25
|
-
"codex"
|
|
25
|
+
"codex",
|
|
26
|
+
"openai-compatible",
|
|
27
|
+
"chatgpt",
|
|
28
|
+
"gpt-image-2",
|
|
29
|
+
"image-generation",
|
|
30
|
+
"images-generations",
|
|
31
|
+
"local-gateway",
|
|
32
|
+
"multi-account"
|
|
26
33
|
],
|
|
27
34
|
"engines": {
|
|
28
35
|
"node": ">=22"
|
|
@@ -31,9 +38,11 @@
|
|
|
31
38
|
"login": "bun src/cli.ts login",
|
|
32
39
|
"status": "bun src/cli.ts status",
|
|
33
40
|
"ask": "bun src/cli.ts ask",
|
|
41
|
+
"start": "bun src/cli.ts start",
|
|
34
42
|
"serve": "bun src/cli.ts serve",
|
|
35
43
|
"build": "tsup",
|
|
36
44
|
"typecheck": "bunx tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"prepack": "npm run build",
|
|
37
46
|
"pack:dry": "npm pack --dry-run"
|
|
38
47
|
},
|
|
39
48
|
"dependencies": {
|