@cartanova/qgrid-cli 2.10.3 → 2.10.5
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/bundle/dist/application/qgrid/qgrid-image-generation.js +6 -2
- package/bundle/dist/application/qgrid/qgrid-response-format.js +4 -4
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +63 -140
- package/bundle/dist/application/qgrid/qgrid.frame.js +3 -6
- package/bundle/dist/application/qgrid/qgrid.types.js +4 -8
- package/bundle/dist/application/request-log/request-log.model.js +21 -15
- package/bundle/dist/application/setting/setting.model.js +2 -64
- package/bundle/dist/application/setting/setting.store.js +3 -4
- package/bundle/dist/application/setting/setting.types.js +2 -12
- package/bundle/dist/i18n/en.js +1 -3
- package/bundle/dist/i18n/ko.js +1 -3
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +19 -8
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +18 -0
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +14 -3
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +9 -0
- package/bundle/src/application/qgrid/qgrid-response-format.ts +10 -7
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +0 -111
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +82 -178
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -6
- package/bundle/src/application/qgrid/qgrid.types.ts +14 -4
- package/bundle/src/application/request-log/request-log.model.test.ts +17 -12
- package/bundle/src/application/request-log/request-log.model.ts +8 -2
- package/bundle/src/application/setting/setting.model.test.ts +1 -101
- package/bundle/src/application/setting/setting.model.ts +0 -73
- package/bundle/src/application/setting/setting.store.ts +2 -3
- package/bundle/src/application/setting/setting.types.ts +0 -12
- package/bundle/src/application/sonamu.generated.http +2 -7
- package/bundle/src/i18n/en.ts +0 -3
- package/bundle/src/i18n/ko.ts +0 -3
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +19 -1
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +22 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +12 -0
- package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +1 -0
- package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
- package/bundle/web-dist/client/assets/{index-BIC1sQxw.js → index-Dxw7DiXF.js} +4 -4
- package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +1 -0
- package/bundle/web-dist/client/assets/{monit-CraVV3MQ.js → monit-E8eGW1Sj.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-B3ZBoJ60.js → routes-7spLfIGZ.js} +2 -2
- package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
- package/bundle/web-dist/client/assets/settings-BdosZFNC.js +1 -0
- package/bundle/web-dist/client/assets/{show-ItTExdAh.js → show-b2PFNI3D.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-BynKx8Vn.js → tokens-CBdHb2_U.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-8xw6on5P.js → use-oauth-login-flow-Cyn3YPqW.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-D-Y8OySK.js → use-update-token-mutation-DQCwCYfR.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{cost-CSH_t17I.js → cost-59jBdx-I.js} +8 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CaaWO9-Z.js} +9 -7
- package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-CBil92fJ.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-CmvbWuDf.js → plus-Bvdj8Pwx.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BjR1Iaz8.js → react-dom-CyuShs9K.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-B8U9BL9-.js → refresh-cw-3QAiSWBd.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-C7lB2Q85.js → routes-DtVf9yqo.js} +7 -7
- package/bundle/web-dist/server/assets/{services.generated-BM4X3Ecj.js → services.generated-FPwsG9p3.js} +116 -131
- package/bundle/web-dist/server/assets/{settings-BFVuldch.js → settings-DeHNwoMv.js} +40 -251
- package/bundle/web-dist/server/assets/{show-Cswh_tqs.js → show-8NNdRxhM.js} +3 -3
- package/bundle/web-dist/server/assets/{tokens-DiTUwUwr.js → tokens-odgS2-9q.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DYKnG4A2.js → use-oauth-login-flow-DTQKdYBO.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-DqGMopII.js → use-update-token-mutation-xvXbPrCI.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +3 -2
- package/skills/qgrid/SKILL.md +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
- package/skills/qgrid/references/decision-rationale.md +2 -2
- package/skills/qgrid/references/openai-codex-runtime.md +5 -1
- package/bundle/dist/utils/process-supervisor.js +0 -22
- package/bundle/dist/utils/server-restart.js +0 -61
- package/bundle/src/utils/process-supervisor.test.ts +0 -24
- package/bundle/src/utils/process-supervisor.ts +0 -17
- package/bundle/src/utils/server-restart.test.ts +0 -76
- package/bundle/src/utils/server-restart.ts +0 -68
- package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +0 -1
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +0 -1
- package/bundle/web-dist/client/assets/logs-zvdujZWn.js +0 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
- package/bundle/web-dist/client/assets/settings-DwbzT5jo.js +0 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
1
|
|
|
3
|
-
import {
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
3
|
|
|
6
|
-
import { resetNativeRunRegistryForTests } from "../qgrid/qgrid-run-lifecycle";
|
|
7
|
-
import { resetServerRestartForTests } from "../../utils/server-restart";
|
|
8
4
|
import { SettingModel } from "./setting.model";
|
|
9
5
|
import * as reminders from "../qgrid/expired-token-reminder";
|
|
10
6
|
import { SlackNotificationError } from "../../utils/slack-notify";
|
|
@@ -26,99 +22,3 @@ describe("SettingModel.triggerExpiryReminder", () => {
|
|
|
26
22
|
});
|
|
27
23
|
});
|
|
28
24
|
});
|
|
29
|
-
|
|
30
|
-
function installContext(headers: Record<string, string>) {
|
|
31
|
-
const raw = Object.assign(new EventEmitter(), {
|
|
32
|
-
destroyed: false,
|
|
33
|
-
writableEnded: false,
|
|
34
|
-
});
|
|
35
|
-
vi.spyOn(Sonamu, "getContext").mockReturnValue({
|
|
36
|
-
transport: "http",
|
|
37
|
-
headers,
|
|
38
|
-
request: { protocol: "http" },
|
|
39
|
-
reply: { raw },
|
|
40
|
-
} as never);
|
|
41
|
-
return raw;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe("SettingModel.restartServer", () => {
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
resetServerRestartForTests();
|
|
51
|
-
resetNativeRunRegistryForTests();
|
|
52
|
-
vi.unstubAllEnvs();
|
|
53
|
-
vi.restoreAllMocks();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("rejects restart when pm2 will not respawn the process", async () => {
|
|
57
|
-
const raw = installContext({ host: "localhost:44900" });
|
|
58
|
-
vi.stubEnv("pm_id", undefined);
|
|
59
|
-
|
|
60
|
-
await expect(SettingModel.restartServer()).rejects.toMatchObject({ statusCode: 400 });
|
|
61
|
-
expect(raw.listenerCount("finish")).toBe(0);
|
|
62
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("rejects a browser request whose Origin host does not match Host", async () => {
|
|
66
|
-
installContext({ origin: "https://evil.example", host: "qgrid.example.com" });
|
|
67
|
-
vi.stubEnv("pm_id", "0");
|
|
68
|
-
|
|
69
|
-
await expect(SettingModel.restartServer()).rejects.toMatchObject({ statusCode: 403 });
|
|
70
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("rejects a malformed present Origin instead of treating it as an operational client", async () => {
|
|
74
|
-
installContext({ origin: "null", host: "qgrid.example.com" });
|
|
75
|
-
vi.stubEnv("pm_id", "0");
|
|
76
|
-
|
|
77
|
-
await expect(SettingModel.restartServer()).rejects.toMatchObject({ statusCode: 403 });
|
|
78
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("accepts a matching Origin using the Caddy-forwarded host", async () => {
|
|
82
|
-
const raw = installContext({
|
|
83
|
-
origin: "https://qgrid.example.com",
|
|
84
|
-
host: "127.0.0.1:44900",
|
|
85
|
-
"x-forwarded-host": "qgrid.example.com",
|
|
86
|
-
});
|
|
87
|
-
vi.stubEnv("pm_id", "0");
|
|
88
|
-
|
|
89
|
-
await expect(SettingModel.restartServer()).resolves.toEqual({ supervisor: "pm2" });
|
|
90
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
91
|
-
|
|
92
|
-
raw.emit("finish");
|
|
93
|
-
expect(process.exit).toHaveBeenCalledOnce();
|
|
94
|
-
expect(process.exit).toHaveBeenCalledWith(0);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("accepts an Origin-less operational request", async () => {
|
|
98
|
-
const raw = installContext({ host: "qgrid.example.com" });
|
|
99
|
-
vi.stubEnv("pm_id", "0");
|
|
100
|
-
|
|
101
|
-
await expect(SettingModel.restartServer()).resolves.toEqual({ supervisor: "pm2" });
|
|
102
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
103
|
-
|
|
104
|
-
raw.emit("finish");
|
|
105
|
-
expect(process.exit).toHaveBeenCalledWith(0);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("coordinates simultaneous calls through one process exit", async () => {
|
|
109
|
-
const firstRaw = installContext({ host: "qgrid.example.com" });
|
|
110
|
-
vi.stubEnv("pm_id", "0");
|
|
111
|
-
|
|
112
|
-
const first = SettingModel.restartServer();
|
|
113
|
-
const second = SettingModel.restartServer();
|
|
114
|
-
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
115
|
-
{ supervisor: "pm2" },
|
|
116
|
-
{ supervisor: "pm2" },
|
|
117
|
-
]);
|
|
118
|
-
|
|
119
|
-
firstRaw.emit("finish");
|
|
120
|
-
firstRaw.emit("close");
|
|
121
|
-
expect(process.exit).toHaveBeenCalledOnce();
|
|
122
|
-
expect(process.exit).toHaveBeenCalledWith(0);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { getLogger } from "@logtape/logtape";
|
|
2
1
|
import {
|
|
3
2
|
BaseModelClass,
|
|
4
3
|
type ListResult,
|
|
5
4
|
asArray,
|
|
6
5
|
NotFoundException,
|
|
7
6
|
BadRequestException,
|
|
8
|
-
SoException,
|
|
9
|
-
Sonamu,
|
|
10
7
|
api,
|
|
11
8
|
exhaustive,
|
|
12
9
|
} from "sonamu";
|
|
13
10
|
|
|
14
11
|
import { SD, type LocalizedString } from "../../i18n/sd.generated";
|
|
15
|
-
import { detectSupervisor } from "../../utils/process-supervisor";
|
|
16
|
-
import { armServerRestartExit, beginServerRestart } from "../../utils/server-restart";
|
|
17
12
|
import { SlackNotificationError } from "../../utils/slack-notify";
|
|
18
13
|
import { type SettingSubsetKey, type SettingSubsetMapping } from "../sonamu.generated";
|
|
19
14
|
import { settingSubsetQueries, settingLoaderQueries } from "../sonamu.generated.sso";
|
|
@@ -37,37 +32,8 @@ import {
|
|
|
37
32
|
type SettingListParams,
|
|
38
33
|
type SettingSaveParams,
|
|
39
34
|
type SettingsResponse,
|
|
40
|
-
type SupervisorKind,
|
|
41
35
|
} from "./setting.types";
|
|
42
36
|
|
|
43
|
-
const logger = getLogger(["qgrid", "setting"]);
|
|
44
|
-
|
|
45
|
-
class ForbiddenException extends SoException {
|
|
46
|
-
constructor() {
|
|
47
|
-
super(403, SD("error.forbidden"));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function firstHeaderValue(value: string | string[] | undefined): string | undefined {
|
|
52
|
-
return (Array.isArray(value) ? value[0] : value)?.split(",")[0]?.trim();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Origin 없는 운영 CLI 요청은 통과시키되, 브라우저 교차 출처 POST 는 막는다. */
|
|
56
|
-
function assertRestartOrigin(): void {
|
|
57
|
-
const { headers } = Sonamu.getContext();
|
|
58
|
-
const origin = firstHeaderValue(headers.origin);
|
|
59
|
-
if (headers.origin === undefined) return;
|
|
60
|
-
if (!origin) throw new ForbiddenException();
|
|
61
|
-
|
|
62
|
-
const host = firstHeaderValue(headers["x-forwarded-host"]) ?? firstHeaderValue(headers.host);
|
|
63
|
-
try {
|
|
64
|
-
if (!host || new URL(origin).host !== host) throw new ForbiddenException();
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (error instanceof ForbiddenException) throw error;
|
|
67
|
-
throw new ForbiddenException();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
37
|
/*
|
|
72
38
|
Setting Model
|
|
73
39
|
*/
|
|
@@ -246,7 +212,6 @@ class SettingModelClass extends BaseModelClass<
|
|
|
246
212
|
return {
|
|
247
213
|
settings,
|
|
248
214
|
runtime: this.runtimeInfo(),
|
|
249
|
-
supervisor: detectSupervisor(),
|
|
250
215
|
keepaliveRunnerEnabled: process.env[TOKEN_WINDOW_KEEPALIVE_RUNNER_ENV_KEY] === "true",
|
|
251
216
|
};
|
|
252
217
|
}
|
|
@@ -309,44 +274,6 @@ class SettingModelClass extends BaseModelClass<
|
|
|
309
274
|
throw error;
|
|
310
275
|
}
|
|
311
276
|
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* 서버를 재시작한다 — 정확히는 스스로 종료하고, pm2 가 다시 띄우는 것에 맡긴다.
|
|
315
|
-
*
|
|
316
|
-
* `restart` 로 표시된 워커 설정은 dispatcher 생성자에서 한 번만 읽히므로 이 경로 말고는
|
|
317
|
-
* 반영할 방법이 없었다. 지금까지는 SSH 로 `pm2 restart` 를 쳐야 했다.
|
|
318
|
-
*
|
|
319
|
-
* 부수 효과가 하나 더 있다: pm2 는 `ecosystem.config.cjs` 의 원래 명령으로 다시 띄우고,
|
|
320
|
-
* 그 명령에 `--skip-update` 가 없으면 CLI 가 npm 최신 버전을 받아 올린다. 즉 재시작은
|
|
321
|
-
* 배포이기도 하다 — 화면 문구가 이 사실을 함께 알린다.
|
|
322
|
-
*/
|
|
323
|
-
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
324
|
-
async restartServer(): Promise<{ supervisor: SupervisorKind }> {
|
|
325
|
-
assertRestartOrigin();
|
|
326
|
-
const supervisor = detectSupervisor();
|
|
327
|
-
if (!supervisor) {
|
|
328
|
-
// 다시 띄워줄 주체가 없으면 종료가 곧 정지다. 살릴 수 없는데 죽이지 않는다.
|
|
329
|
-
throw new BadRequestException(SD("setting.noSupervisor")());
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const { reply } = Sonamu.getContext();
|
|
333
|
-
const started = beginServerRestart();
|
|
334
|
-
if (started) {
|
|
335
|
-
logger.warn(`restart requested via dashboard, exiting for ${supervisor} to respawn`);
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
// 정적 import로 qgrid.frame/token 그래프를 당겨오면 모듈 목 테스트가 오염된다.
|
|
339
|
-
// 재시작이 실제로 확정된 시점에 active native run 정합성 처리만 로드한다.
|
|
340
|
-
const { finishActiveNativeRunsForRestart } = await import("../qgrid/qgrid-run-lifecycle");
|
|
341
|
-
await finishActiveNativeRunsForRestart();
|
|
342
|
-
} finally {
|
|
343
|
-
// 정합성 처리가 실패해도 응답이 flush 된 뒤에는 재시작을 끝까지 진행한다.
|
|
344
|
-
armServerRestartExit(reply.raw);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return { supervisor };
|
|
349
|
-
}
|
|
350
277
|
}
|
|
351
278
|
|
|
352
279
|
export const SettingModel = new SettingModelClass();
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 런타임 설정 조회 캐시.
|
|
3
3
|
*
|
|
4
|
-
* 값의 출처는 DB(`settings`) → env 순이다.
|
|
5
|
-
*
|
|
6
|
-
* 기본값으로 떨어진다.
|
|
4
|
+
* 값의 출처는 DB(`settings`) → env 순이다. 저장된 설정이 없으면 기동 시 전달된
|
|
5
|
+
* 환경변수를 사용하므로, 설정을 DB에 저장하기 전에도 기존 값이 유지된다.
|
|
7
6
|
*
|
|
8
7
|
* 설정은 요청 경로(permit 해석, 알림 전송)에서 읽히므로 매번 DB 를 때리지 않고 메모리에
|
|
9
8
|
* 둔다. `immediate` 변경도 현재 API 프로세스에만 즉시 반영된다. 인스턴스가 여러 개면 다른
|
|
@@ -21,16 +21,6 @@ export type SettingApplies = z.infer<typeof SettingApplies>;
|
|
|
21
21
|
export const SettingSource = z.enum(["db", "env", "default"]);
|
|
22
22
|
export type SettingSource = z.infer<typeof SettingSource>;
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* 프로세스를 다시 띄워주는 도구. 재시작은 스스로 종료하고 이 도구에 맡기므로,
|
|
26
|
-
* 없으면(null) 화면에서 재시작을 시도조차 하지 않는다.
|
|
27
|
-
*
|
|
28
|
-
* API 반환 타입에 쓰이므로 zod 로 둔다 — sonamu syncer 가 클라이언트 코드를 만들 때
|
|
29
|
-
* 타입만 있는 선언은 모듈을 찾지 못한다.
|
|
30
|
-
*/
|
|
31
|
-
export const SupervisorKind = z.enum(["pm2"]);
|
|
32
|
-
export type SupervisorKind = z.infer<typeof SupervisorKind>;
|
|
33
|
-
|
|
34
24
|
export const SettingItem = z.object({
|
|
35
25
|
key: z.string(),
|
|
36
26
|
group: z.string(),
|
|
@@ -59,8 +49,6 @@ export type RuntimeInfoItem = z.infer<typeof RuntimeInfoItem>;
|
|
|
59
49
|
export const SettingsResponse = z.object({
|
|
60
50
|
settings: SettingItem.array(),
|
|
61
51
|
runtime: RuntimeInfoItem.array(),
|
|
62
|
-
/** null 이면 재시작 버튼을 쓸 수 없다 — 종료해도 되살릴 주체가 없다. */
|
|
63
|
-
supervisor: SupervisorKind.nullable(),
|
|
64
52
|
/** 이 인스턴스가 keepalive 스케줄러를 소유하도록 명시적으로 허용됐는지. */
|
|
65
53
|
keepaliveRunnerEnabled: z.boolean(),
|
|
66
54
|
});
|
|
@@ -98,13 +98,6 @@ Content-Type: application/json
|
|
|
98
98
|
|
|
99
99
|
###
|
|
100
100
|
|
|
101
|
-
POST {{baseUrl}}/api/setting/restartServer
|
|
102
|
-
Content-Type: application/json
|
|
103
|
-
|
|
104
|
-
{}
|
|
105
|
-
|
|
106
|
-
###
|
|
107
|
-
|
|
108
101
|
GET {{baseUrl}}/api/requestLogStep/findById
|
|
109
102
|
?subset=A
|
|
110
103
|
&id=0
|
|
@@ -297,6 +290,7 @@ Content-Type: application/json
|
|
|
297
290
|
"inputSchema": "unknown"
|
|
298
291
|
}
|
|
299
292
|
],
|
|
293
|
+
"thinking": true,
|
|
300
294
|
"effort": "EFFORT",
|
|
301
295
|
"verbosity": "low",
|
|
302
296
|
"reasoningSummary": "auto",
|
|
@@ -361,6 +355,7 @@ Content-Type: application/json
|
|
|
361
355
|
"inputSchema": "unknown"
|
|
362
356
|
}
|
|
363
357
|
],
|
|
358
|
+
"thinking": true,
|
|
364
359
|
"effort": "EFFORT",
|
|
365
360
|
"verbosity": "low",
|
|
366
361
|
"reasoningSummary": "auto",
|
package/bundle/src/i18n/en.ts
CHANGED
|
@@ -49,12 +49,9 @@ export default {
|
|
|
49
49
|
"error.serviceUnavailable": "Service unavailable",
|
|
50
50
|
"setting.invalidValue": (reason: string) => `Invalid setting value: ${reason}`,
|
|
51
51
|
"setting.unknownKey": (key: string) => `Unknown setting key ${key}`,
|
|
52
|
-
"setting.noSupervisor": () =>
|
|
53
|
-
"Restart is unavailable when running locally — nothing would bring the server back up",
|
|
54
52
|
"qgrid.dispatcherFailed": (provider: string) => `${provider} dispatcher failed to initialize`,
|
|
55
53
|
"qgrid.dispatcherStarting": (provider: string) =>
|
|
56
54
|
`${provider} dispatcher is starting up. Please retry shortly`,
|
|
57
|
-
"qgrid.restartPending": () => "Server restart is in progress. Please retry shortly",
|
|
58
55
|
"error.unauthorized": "Unauthorized",
|
|
59
56
|
notFound: (name: string, id: number) => `${name} ID ${id} not found`,
|
|
60
57
|
"validation.email": "Invalid email format",
|
package/bundle/src/i18n/ko.ts
CHANGED
|
@@ -48,12 +48,9 @@ export default {
|
|
|
48
48
|
"error.serviceUnavailable": "서비스를 사용할 수 없습니다",
|
|
49
49
|
"setting.invalidValue": (reason: string) => `설정 값이 올바르지 않습니다: ${reason}`,
|
|
50
50
|
"setting.unknownKey": (key: string) => `알 수 없는 설정 키 ${key}`,
|
|
51
|
-
"setting.noSupervisor": () =>
|
|
52
|
-
"로컬 실행 환경에서는 재시작할 수 없습니다 — 종료하면 다시 켜줄 모니터링 도구가 없습니다",
|
|
53
51
|
"qgrid.dispatcherFailed": (provider: string) => `${provider} dispatcher 초기화에 실패했습니다`,
|
|
54
52
|
"qgrid.dispatcherStarting": (provider: string) =>
|
|
55
53
|
`${provider} dispatcher 기동 중입니다. 잠시 후 다시 시도해주세요`,
|
|
56
|
-
"qgrid.restartPending": () => "서버 재시작 중입니다. 잠시 후 다시 시도해주세요",
|
|
57
54
|
"error.unauthorized": "인증이 필요합니다",
|
|
58
55
|
"error.unknownSearchField": (field: string) => `구현되지 않은 검색 필드 ${field}`,
|
|
59
56
|
notFound: (name: string, id: number) => `존재하지 않는 ${name} ID ${id}`,
|
|
@@ -8,6 +8,24 @@ import {
|
|
|
8
8
|
} from "./openai-backend-protocol";
|
|
9
9
|
|
|
10
10
|
describe("OpenAI Codex backend protocol", () => {
|
|
11
|
+
it.each(["png", "jpeg", "webp"])("preserves reported hosted %s generation settings", (output_format) => {
|
|
12
|
+
expect(normalizeOpenAIEvent({ type: "response.output_item.done", item: {
|
|
13
|
+
type: "image_generation_call", result: "aGVsbG8=", output_format,
|
|
14
|
+
size: "841x1870", quality: "low", background: "opaque",
|
|
15
|
+
} })).toMatchObject({ generation: {
|
|
16
|
+
route: "codex-responses", size: "841x1870", quality: "low", background: "opaque",
|
|
17
|
+
} });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("reports PNG byte dimensions instead of a conflicting backend size", () => {
|
|
21
|
+
const bytes = Buffer.alloc(24);
|
|
22
|
+
Buffer.from("89504e470d0a1a0a", "hex").copy(bytes);
|
|
23
|
+
bytes.writeUInt32BE(841, 16);
|
|
24
|
+
bytes.writeUInt32BE(1870, 20);
|
|
25
|
+
expect(normalizeOpenAIEvent({ type: "response.output_item.done", item: {
|
|
26
|
+
type: "image_generation_call", result: bytes.toString("base64"), size: "1152x2560",
|
|
27
|
+
} })).toMatchObject({ generation: { route: "codex-responses", size: "841x1870" } });
|
|
28
|
+
});
|
|
11
29
|
it("distinguishes partial function items from completed arguments", () => {
|
|
12
30
|
const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: "{}" };
|
|
13
31
|
expect(normalizeOpenAIEvent({ type: "response.output_item.added", item })).toEqual({ type: "output-item", item, completed: false });
|
|
@@ -129,6 +147,6 @@ describe("OpenAI Codex backend protocol", () => {
|
|
|
129
147
|
type: "response.output_item.done",
|
|
130
148
|
item: { type: "image_generation_call", id: "img", result: "aGVsbG8=", revised_prompt: "better" },
|
|
131
149
|
}),
|
|
132
|
-
).toEqual({ type: "image", id: "img", base64: "aGVsbG8=", mimeType: "image/png", revisedPrompt: "better" });
|
|
150
|
+
).toEqual({ type: "image", id: "img", base64: "aGVsbG8=", mimeType: "image/png", revisedPrompt: "better", generation: { route: "codex-responses" } });
|
|
133
151
|
});
|
|
134
152
|
});
|
|
@@ -248,11 +248,33 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
|
|
|
248
248
|
) {
|
|
249
249
|
const item = event.item as OpenAIResponseItem;
|
|
250
250
|
if (item.type === "image_generation_call" && typeof item.result === "string") {
|
|
251
|
+
// PNG exposes dimensions in its fixed header. Other formats retain the
|
|
252
|
+
// backend's observed size; never substitute requested image controls.
|
|
253
|
+
const header = Buffer.from(item.result.slice(0, 32), "base64");
|
|
254
|
+
const pngSize =
|
|
255
|
+
header.length >= 24 &&
|
|
256
|
+
header.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")) &&
|
|
257
|
+
header.readUInt32BE(16) > 0 &&
|
|
258
|
+
header.readUInt32BE(20) > 0
|
|
259
|
+
? `${header.readUInt32BE(16)}x${header.readUInt32BE(20)}`
|
|
260
|
+
: undefined;
|
|
261
|
+
const size =
|
|
262
|
+
pngSize ??
|
|
263
|
+
(typeof item.size === "string" && /^[1-9]\d*x[1-9]\d*$/.test(item.size)
|
|
264
|
+
? item.size
|
|
265
|
+
: undefined);
|
|
251
266
|
return {
|
|
252
267
|
type: "image",
|
|
253
268
|
...(typeof item.id === "string" ? { id: item.id } : {}),
|
|
254
269
|
base64: item.result,
|
|
255
270
|
mimeType: imageMimeType(item.result, item.output_format),
|
|
271
|
+
generation: {
|
|
272
|
+
route: "codex-responses",
|
|
273
|
+
...(size ? { size } : {}),
|
|
274
|
+
...(typeof item.model === "string" ? { model: item.model } : {}),
|
|
275
|
+
...(typeof item.quality === "string" ? { quality: item.quality } : {}),
|
|
276
|
+
...(typeof item.background === "string" ? { background: item.background } : {}),
|
|
277
|
+
},
|
|
256
278
|
...(typeof item.revised_prompt === "string"
|
|
257
279
|
? { revisedPrompt: item.revised_prompt }
|
|
258
280
|
: typeof item.revisedPrompt === "string"
|
|
@@ -144,6 +144,18 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
it.each(["auto", "1152x2560"])("forwards %s size hints and preserves hosted observations", async (size) => {
|
|
148
|
+
const args = QueryInput.parse({ prompt: "draw in 9:20", imageGeneration: true, imageGenerationOptions: { size } });
|
|
149
|
+
const generation = { route: "codex-responses" as const, size: "841x1870", quality: "low" };
|
|
150
|
+
const d = dispatcher((options) => {
|
|
151
|
+
expect(buildOpenAIResponsesRequest(options).tools).toContainEqual({ type: "image_generation", size });
|
|
152
|
+
return events({ type: "image", base64: "data", mimeType: "image/png", generation }, { type: "completed", responseId: "r" });
|
|
153
|
+
});
|
|
154
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
155
|
+
const result = await d.generate(request({ imageGeneration: args.imageGeneration, imageGenerationOptions: args.imageGenerationOptions }));
|
|
156
|
+
expect(result.images).toMatchObject([{ generation }]);
|
|
157
|
+
});
|
|
158
|
+
|
|
147
159
|
beforeEach(() => { refreshMock.mockReset(); });
|
|
148
160
|
|
|
149
161
|
it("refreshes a quota 401 once, shares the recovery, and uses the new credentials", async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=1e6,t=4;function n(t){return t/e}function r(e,n=!1){return`${n?`Known `:``}$${e.toFixed(t)}`}function i(e){return r(n(e))}function a(e){let t=e.is_image_generation&&e.image_cost_usd===null;return e.cost_usd===null&&e.image_cost_usd===null?t?`Unknown`:`—`:r(n((e.cost_usd??0)+(e.image_cost_usd??0)),t)}function o(e){return e.input_tokens<=0?`—`:`${Math.round(e.cache_read_tokens/e.input_tokens*100)}%`}export{r as i,i as n,a as r,o as t};
|