@cartanova/qgrid-cli 2.10.2 → 2.10.4
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/monit/log-buffer.js +7 -11
- package/bundle/dist/application/monit/monit.frame.js +6 -11
- package/bundle/dist/application/qgrid/oauth.js +2 -2
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +64 -141
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -36
- package/bundle/dist/application/qgrid/qgrid.frame.js +3 -6
- package/bundle/dist/application/qgrid/qgrid.types.js +2 -1
- package/bundle/dist/application/qgrid/token-subscriber.js +4 -7
- package/bundle/dist/application/qgrid/token-window-keepalive.js +3 -2
- package/bundle/dist/application/qgrid/tool-emulation-schema.js +4 -4
- package/bundle/dist/application/qgrid/tool-emulation.js +14 -2
- package/bundle/dist/application/request-log/request-log.model.js +7 -5
- 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/sonamu.config.js +6 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +53 -84
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +55 -0
- package/bundle/dist/utils/providers/anthropic/claude-session.js +11 -13
- package/bundle/dist/utils/providers/common/effort.js +1 -1
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +3 -3
- package/bundle/dist/utils/providers/common/thinking.js +12 -0
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +6 -16
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +89 -78
- package/bundle/src/application/monit/log-buffer.test.ts +3 -4
- package/bundle/src/application/monit/log-buffer.ts +13 -27
- package/bundle/src/application/monit/monit.frame.test.ts +7 -8
- package/bundle/src/application/monit/monit.frame.ts +9 -14
- package/bundle/src/application/qgrid/oauth.ts +1 -1
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +7 -111
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +83 -179
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +62 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +25 -71
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -12
- package/bundle/src/application/qgrid/qgrid.types.ts +2 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +8 -18
- package/bundle/src/application/qgrid/token-subscriber.ts +10 -20
- package/bundle/src/application/qgrid/token-window-keepalive.test.ts +2 -2
- package/bundle/src/application/qgrid/token-window-keepalive.ts +2 -1
- package/bundle/src/application/qgrid/tool-emulation-schema.test.ts +6 -3
- package/bundle/src/application/qgrid/tool-emulation-schema.ts +3 -3
- package/bundle/src/application/qgrid/tool-emulation.test.ts +8 -1
- package/bundle/src/application/qgrid/tool-emulation.ts +29 -0
- package/bundle/src/application/request-log/request-log.model.ts +5 -5
- package/bundle/src/application/setting/setting.model.test.ts +1 -101
- package/bundle/src/application/setting/setting.model.ts +1 -74
- 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/sonamu.config.ts +5 -5
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +3 -6
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +26 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +70 -148
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.test.ts +36 -0
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +84 -0
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +74 -13
- package/bundle/src/utils/providers/anthropic/claude-session.ts +11 -20
- package/bundle/src/utils/providers/common/effort.ts +1 -1
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +4 -0
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -4
- package/bundle/src/utils/providers/common/thinking.ts +10 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +7 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +3 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +20 -33
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +97 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +113 -77
- package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
- package/bundle/web-dist/client/assets/{index-DtRTSTes.js → index-reW8VvJF.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-BhVYDEzC.js → logs-DDIt97D1.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-BxN4JWqK.js → monit-DkABv7PG.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CBCqRp2o.js → routes-BRmQ4M3-.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
- package/bundle/web-dist/client/assets/settings-BHvtycEZ.js +1 -0
- package/bundle/web-dist/client/assets/{show-BzYft5N4.js → show-qeTIzgDZ.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-PMybtI8X.js → tokens-5Bep_G-w.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-C51_d6-6.js → use-oauth-login-flow-Cx-QO25M.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/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CagNGTBj.js} +3 -3
- package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-DSxs4Jnl.js} +3 -3
- 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-i-sTqXMX.js} +4 -4
- 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-C4jk1Hrp.js} +2 -2
- 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 +1 -1
- package/skills/qgrid/SKILL.md +1 -0
- package/skills/qgrid/references/ai-sdk-provider-contract.md +2 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +7 -4
- package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
- package/skills/qgrid/references/decision-rationale.md +9 -0
- package/skills/qgrid/references/openai-codex-runtime.md +6 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
- package/skills/qgrid/references/tool-calling-and-multiturn.md +18 -1
- package/skills/qgrid/references/verification-and-debugging.md +1 -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/index-D_sagzsR.css +0 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
- package/bundle/web-dist/client/assets/settings-hBTEC7Ki.js +0 -1
|
@@ -25,19 +25,15 @@ export class TokenSubscriber {
|
|
|
25
25
|
attempt = 0;
|
|
26
26
|
connectedAt: Date | null = null;
|
|
27
27
|
lastReconcileAt: Date | null = null;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
operationChain: Promise<void> = Promise.resolve();
|
|
29
|
+
tokenChangeHandler: (() => void) | null = null;
|
|
30
30
|
|
|
31
31
|
constructor(
|
|
32
32
|
public connConfig: ClientConfig,
|
|
33
33
|
public dispatcher: QgridDispatcherClass,
|
|
34
34
|
) {}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
this.tokenChangeHandler = handler;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private notifyTokensChanged(): void {
|
|
36
|
+
notifyTokensChanged(): void {
|
|
41
37
|
try {
|
|
42
38
|
this.tokenChangeHandler?.();
|
|
43
39
|
} catch (error) {
|
|
@@ -168,18 +164,18 @@ export class TokenSubscriber {
|
|
|
168
164
|
return this.enqueueOperation(() => this.handleNotificationNow(payloadJson));
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
|
|
167
|
+
async handleNotificationNow(payloadJson: string): Promise<void> {
|
|
172
168
|
const payload = JSON.parse(payloadJson) as Payload;
|
|
173
169
|
const previousRow = this.tokenChangeHandler
|
|
174
170
|
? this.dispatcher.tokens.get(payload.id)
|
|
175
171
|
: undefined;
|
|
176
172
|
const wasKeepaliveTarget = previousRow?.active === true && previousRow.provider === "anthropic";
|
|
177
173
|
if (payload.op === "DELETE") {
|
|
178
|
-
this.dispatcher.
|
|
174
|
+
this.dispatcher.tokens.delete(payload.id);
|
|
179
175
|
await this.dispatcher.openaiDispatcher
|
|
180
176
|
?.onTokenRemoved(payload.id)
|
|
181
177
|
.catch((e) => logger.warn(`openai token remove failed: ${(e as Error).message}`));
|
|
182
|
-
//
|
|
178
|
+
// 삭제된 토큰의 provider를 모를 수 있으므로 양쪽 풀에서 제거한다.
|
|
183
179
|
this.dispatcher.anthropicDispatcher?.onTokenRemoved(payload.id);
|
|
184
180
|
logger.info(`NOTIFY ${payload.op} id=${payload.id} → removed from cache`);
|
|
185
181
|
if (previousRow === undefined || wasKeepaliveTarget) this.notifyTokensChanged();
|
|
@@ -187,7 +183,7 @@ export class TokenSubscriber {
|
|
|
187
183
|
}
|
|
188
184
|
const row = await TokenModel.findOne("A", { id: payload.id });
|
|
189
185
|
if (!row) {
|
|
190
|
-
this.dispatcher.
|
|
186
|
+
this.dispatcher.tokens.delete(payload.id);
|
|
191
187
|
await this.dispatcher.openaiDispatcher
|
|
192
188
|
?.onTokenRemoved(payload.id)
|
|
193
189
|
.catch((e) => logger.warn(`openai token remove failed: ${(e as Error).message}`));
|
|
@@ -197,7 +193,7 @@ export class TokenSubscriber {
|
|
|
197
193
|
return;
|
|
198
194
|
}
|
|
199
195
|
|
|
200
|
-
this.dispatcher.
|
|
196
|
+
this.dispatcher.tokens.set(payload.id, row);
|
|
201
197
|
|
|
202
198
|
if (row.provider === "openai") {
|
|
203
199
|
const creds = row.credentials as Record<string, unknown>;
|
|
@@ -229,9 +225,6 @@ export class TokenSubscriber {
|
|
|
229
225
|
openaiDispatcher?.onTokenDeactivated(payload.id);
|
|
230
226
|
}
|
|
231
227
|
} else if (row.provider === "anthropic") {
|
|
232
|
-
// anthropic 토큰 이벤트는 동기(void). worker 가 없고 풀(Map)만 관리하므로 단순:
|
|
233
|
-
// INSERT → 추가, active 면 갱신(onTokenUpdated 가 identity 변경도 처리), inactive 면 풀에서 제거.
|
|
234
|
-
// (별도 activate/deactivate 콜백이 없어 active 토글을 add/remove 로 매핑.)
|
|
235
228
|
const creds = row.credentials as AnthropicCredentials;
|
|
236
229
|
if (payload.op === "INSERT" && row.active) {
|
|
237
230
|
this.dispatcher.anthropicDispatcher?.onTokenAdded(
|
|
@@ -250,7 +243,6 @@ export class TokenSubscriber {
|
|
|
250
243
|
row.weight,
|
|
251
244
|
);
|
|
252
245
|
} else {
|
|
253
|
-
// inactive 면 INSERT 든 UPDATE 든 풀에 넣지 않는다.
|
|
254
246
|
this.dispatcher.anthropicDispatcher?.onTokenRemoved(payload.id);
|
|
255
247
|
}
|
|
256
248
|
}
|
|
@@ -263,7 +255,7 @@ export class TokenSubscriber {
|
|
|
263
255
|
return this.enqueueOperation(() => this.reconcileNow());
|
|
264
256
|
}
|
|
265
257
|
|
|
266
|
-
|
|
258
|
+
async reconcileNow(): Promise<void> {
|
|
267
259
|
const previousAnthropicIds = this.tokenChangeHandler
|
|
268
260
|
? new Set(
|
|
269
261
|
[...this.dispatcher.tokens.values()]
|
|
@@ -274,8 +266,6 @@ export class TokenSubscriber {
|
|
|
274
266
|
await TokenModel.deactivateExpiredTokens();
|
|
275
267
|
const rows = await TokenModel.findActive("A");
|
|
276
268
|
this.dispatcher.replaceCache(rows);
|
|
277
|
-
// NOTIFY 유실 대비: AnthropicDispatcher 풀도 DB active anthropic 토큰 기준으로 재동기화.
|
|
278
|
-
// (rows 는 active 만 — inactive/삭제된 토큰은 여기 없으므로 replaceTokens 가 풀에서 제거한다.)
|
|
279
269
|
const anthropicRows = rows
|
|
280
270
|
.filter((r) => r.provider === "anthropic")
|
|
281
271
|
.map((r) => ({
|
|
@@ -308,7 +298,7 @@ export class TokenSubscriber {
|
|
|
308
298
|
}
|
|
309
299
|
}
|
|
310
300
|
|
|
311
|
-
|
|
301
|
+
enqueueOperation(operation: () => Promise<void>): Promise<void> {
|
|
312
302
|
const next = this.operationChain.then(operation, operation);
|
|
313
303
|
this.operationChain = next.catch(() => {});
|
|
314
304
|
return next;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
KEEPALIVE_MODEL,
|
|
5
4
|
KEEPALIVE_PROJECT_NAME,
|
|
6
5
|
POST_KEEPALIVE_USAGE_DELAY_MS,
|
|
7
6
|
rescheduleTokenWindowKeepalive,
|
|
@@ -76,7 +75,8 @@ describe("token window keepalive", () => {
|
|
|
76
75
|
|
|
77
76
|
expect(dispatch).toHaveBeenCalledWith({
|
|
78
77
|
prompt: "Reply OK.",
|
|
79
|
-
model:
|
|
78
|
+
model: "anthropic/claude-haiku-5-5",
|
|
79
|
+
thinking: false,
|
|
80
80
|
effort: "low",
|
|
81
81
|
projectName: KEEPALIVE_PROJECT_NAME,
|
|
82
82
|
preferredTokenId: 1,
|
|
@@ -15,7 +15,7 @@ const RESET_GRACE_MS = 1_000;
|
|
|
15
15
|
// usage API 성공 응답은 1분 캐시된다. keepalive 직후 즉시 재조회하면 이전 윈도우를
|
|
16
16
|
// 다시 보므로 캐시가 지난 뒤 한 번 확인하고, 그때도 새 윈도우가 없으면 다음 5h까지 기다린다.
|
|
17
17
|
export const POST_KEEPALIVE_USAGE_DELAY_MS = 61_000;
|
|
18
|
-
export const KEEPALIVE_MODEL = "anthropic/claude-haiku-
|
|
18
|
+
export const KEEPALIVE_MODEL = "anthropic/claude-haiku-5-5";
|
|
19
19
|
export const KEEPALIVE_PROJECT_NAME = "qgrid-token-window-keepalive";
|
|
20
20
|
export const KEEPALIVE_SETTING_KEY = "qgrid.tokenWindowKeepaliveEnabled";
|
|
21
21
|
export const KEEPALIVE_ENV_KEY = TOKEN_WINDOW_KEEPALIVE_RUNNER_ENV_KEY;
|
|
@@ -163,6 +163,7 @@ async function inspectToken(
|
|
|
163
163
|
await deps.dispatch({
|
|
164
164
|
prompt: "Reply OK.",
|
|
165
165
|
model: KEEPALIVE_MODEL,
|
|
166
|
+
thinking: false,
|
|
166
167
|
effort: "low",
|
|
167
168
|
projectName: KEEPALIVE_PROJECT_NAME,
|
|
168
169
|
preferredTokenId: token.id,
|
|
@@ -23,16 +23,16 @@ describe("buildToolCallSchema", () => {
|
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
25
|
|
|
26
|
-
it("
|
|
26
|
+
it("requests tools in the response body and ends the turn until the client returns results", () => {
|
|
27
27
|
const schema = buildToolCallSchema(tools);
|
|
28
28
|
|
|
29
29
|
expect(schema).toHaveProperty(
|
|
30
30
|
"description",
|
|
31
|
-
expect.stringContaining("
|
|
31
|
+
expect.stringContaining("then end your turn"),
|
|
32
32
|
);
|
|
33
33
|
expect(schema).toHaveProperty(
|
|
34
34
|
["properties", "result", "anyOf", 1, "description"],
|
|
35
|
-
expect.stringContaining("
|
|
35
|
+
expect.stringContaining("wait for the next request containing tool results"),
|
|
36
36
|
);
|
|
37
37
|
expect(schema).toHaveProperty(
|
|
38
38
|
[
|
|
@@ -49,6 +49,9 @@ describe("buildToolCallSchema", () => {
|
|
|
49
49
|
],
|
|
50
50
|
expect.stringContaining("getWeather"),
|
|
51
51
|
);
|
|
52
|
+
expect(JSON.stringify(schema)).not.toContain("StructuredOutput");
|
|
53
|
+
expect(JSON.stringify(schema)).not.toContain("Claude Code");
|
|
54
|
+
expect(schema).toHaveProperty("description", expect.stringContaining("Do not invent tool results"));
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
it("makes degenerate combinations grammatically impossible", () => {
|
|
@@ -54,7 +54,7 @@ export function buildToolCallSchema(tools: QgridTool[], answerSchema?: JsonValue
|
|
|
54
54
|
return {
|
|
55
55
|
type: "object",
|
|
56
56
|
description:
|
|
57
|
-
"
|
|
57
|
+
"Return exactly one JSON object matching this schema as your final response, including when requesting a tool. This object is the response body, not a call to a built-in tool. To request client-side tools, set result.action to tool_call and include result.toolCalls, then end your turn. The client executes those tools and supplies their results in a later request. Do not invent tool results or emit an answer before receiving the required results.",
|
|
58
58
|
properties: {
|
|
59
59
|
result: {
|
|
60
60
|
anyOf: [
|
|
@@ -75,7 +75,7 @@ export function buildToolCallSchema(tools: QgridTool[], answerSchema?: JsonValue
|
|
|
75
75
|
{
|
|
76
76
|
type: "object",
|
|
77
77
|
description:
|
|
78
|
-
"Request client-side tool execution
|
|
78
|
+
"Request client-side tool execution. Return this object as the complete response, then wait for the next request containing tool results.",
|
|
79
79
|
properties: {
|
|
80
80
|
action: { type: "string", enum: ["tool_call"] },
|
|
81
81
|
answer: { type: "null" },
|
|
@@ -88,7 +88,7 @@ export function buildToolCallSchema(tools: QgridTool[], answerSchema?: JsonValue
|
|
|
88
88
|
toolName: {
|
|
89
89
|
type: "string",
|
|
90
90
|
enum: tools.map((tool) => tool.name),
|
|
91
|
-
description: `Client-side tool name
|
|
91
|
+
description: `Client-side tool name. The caller executes these tools after receiving your JSON response.\n${toolDescriptions}`,
|
|
92
92
|
},
|
|
93
93
|
args: {
|
|
94
94
|
type: "string",
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { CALLER_SCHEMA_LIMITS } from "../../utils/providers/common/schema-validation";
|
|
4
4
|
import { type QgridTool } from "./qgrid.types";
|
|
5
|
-
import { applyToolCallEmulation, ToolCallEmulationError } from "./tool-emulation";
|
|
5
|
+
import { applyToolCallEmulation, isToolCallEnvelope, ToolCallEmulationError } from "./tool-emulation";
|
|
6
6
|
|
|
7
7
|
const baseResult = {
|
|
8
8
|
text: "",
|
|
@@ -84,6 +84,13 @@ describe("applyToolCallEmulation image parts", () => {
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
describe("applyToolCallEmulation envelope validation", () => {
|
|
87
|
+
it("ignores intermediate prose but never repairs malformed final JSON or accepts an unknown control tool", () => {
|
|
88
|
+
const tool = JSON.stringify({result: {action: "tool_call", answer: null, toolCalls: [{toolName: "notAllowed", args: "{}"}]}});
|
|
89
|
+
const allowed = [{name: "lookup", inputSchema: {type: "object"}}];
|
|
90
|
+
expect(isToolCallEnvelope("Checking the source.", allowed, "json", "commentary")).toBe(false);
|
|
91
|
+
expect(() => isToolCallEnvelope(tool, allowed, "json", "commentary")).toThrow("unknown emulated tool");
|
|
92
|
+
expect(() => isToolCallEnvelope(tool + tool, allowed, "json", "final_answer")).toThrow("response is not JSON");
|
|
93
|
+
});
|
|
87
94
|
const tools: QgridTool[] = [
|
|
88
95
|
{
|
|
89
96
|
name: "lookup",
|
|
@@ -111,6 +111,35 @@ export function applyToolCallEmulation(
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
export function isToolCallEnvelope(
|
|
115
|
+
text: string,
|
|
116
|
+
tools: QgridTool[],
|
|
117
|
+
answerKind: AnswerKind,
|
|
118
|
+
phase?: string,
|
|
119
|
+
): boolean {
|
|
120
|
+
if (phase === "commentary") {
|
|
121
|
+
// Commentary is not the structured answer. Only an explicit tool control
|
|
122
|
+
// object is eligible, and that object must still pass the strict parser.
|
|
123
|
+
let value: unknown;
|
|
124
|
+
try {
|
|
125
|
+
value = JSON.parse(text);
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
!value ||
|
|
131
|
+
typeof value !== "object" ||
|
|
132
|
+
!("result" in value) ||
|
|
133
|
+
!value.result ||
|
|
134
|
+
typeof value.result !== "object" ||
|
|
135
|
+
!("action" in value.result) ||
|
|
136
|
+
value.result.action !== "tool_call"
|
|
137
|
+
)
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return parseEnvelope(text, tools, answerKind).action === "tool_call";
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
function parseEnvelope(text: string, tools: QgridTool[], answerKind: AnswerKind) {
|
|
115
144
|
if (Buffer.byteLength(text, "utf8") > STRUCTURED_ENVELOPE_LIMITS.maxUtf8Bytes) {
|
|
116
145
|
throw new ToolCallEmulationError(
|
|
@@ -257,7 +257,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
257
257
|
* 화면에서 필터를 걸면 행 수와 비용이 함께 좁혀져야 하는데, 두 쿼리가 각자 필터를
|
|
258
258
|
* 구현하면 한쪽에 축을 추가할 때 조용히 어긋난다.
|
|
259
259
|
*/
|
|
260
|
-
|
|
260
|
+
applyListFilters(
|
|
261
261
|
qb: ReturnType<RequestLogModelClass["getSubsetQueries"]>["qb"],
|
|
262
262
|
params: RequestLogListParams,
|
|
263
263
|
): void {
|
|
@@ -378,7 +378,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
378
378
|
string,
|
|
379
379
|
{ requests: number; errors: number; inputTokens: number; cacheReadTokens: number }
|
|
380
380
|
>();
|
|
381
|
-
|
|
381
|
+
rows.forEach((row) => {
|
|
382
382
|
const model = row.requested_model_name ?? "";
|
|
383
383
|
const provider = model.startsWith("openai/")
|
|
384
384
|
? "openai"
|
|
@@ -396,7 +396,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
396
396
|
agg.inputTokens += row.input_tokens;
|
|
397
397
|
agg.cacheReadTokens += row.cache_read_tokens;
|
|
398
398
|
byProvider.set(provider, agg);
|
|
399
|
-
}
|
|
399
|
+
});
|
|
400
400
|
return [...byProvider.entries()]
|
|
401
401
|
.map(([provider, agg]) => ({ provider, ...agg }))
|
|
402
402
|
.toSorted((a, b) => a.provider.localeCompare(b.provider));
|
|
@@ -584,9 +584,9 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
584
584
|
"is_image_generation",
|
|
585
585
|
"response_json_ok",
|
|
586
586
|
] as const;
|
|
587
|
-
|
|
587
|
+
fields.forEach((key) => {
|
|
588
588
|
if (params[key] !== undefined) update[key] = params[key];
|
|
589
|
-
}
|
|
589
|
+
});
|
|
590
590
|
if (params.history !== undefined) update.history = params.history;
|
|
591
591
|
update.ttft_ms = (await this.firstGenerateStepTtft(requestLogId)) ?? 0;
|
|
592
592
|
|
|
@@ -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,12 +212,11 @@ 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
|
}
|
|
253
218
|
|
|
254
|
-
|
|
219
|
+
runtimeInfo(): RuntimeInfoItem[] {
|
|
255
220
|
const env = process.env;
|
|
256
221
|
const password = env.QGRID_DB_PASSWORD ?? env.SONAMU_DB_PASSWORD ?? "";
|
|
257
222
|
return [
|
|
@@ -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}`,
|
|
@@ -195,14 +195,14 @@ export default defineConfig({
|
|
|
195
195
|
log.warn(`anthropic dispatcher failed: ${(e as Error).message}`);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
subscriber.
|
|
198
|
+
subscriber.tokenChangeHandler = () => {
|
|
199
199
|
void rescheduleTokenWindowKeepalive();
|
|
200
|
-
}
|
|
200
|
+
};
|
|
201
201
|
startExpiredTokenReminder();
|
|
202
202
|
void startTokenWindowKeepalive();
|
|
203
203
|
|
|
204
|
-
const anthropicCount = QgridDispatcher.anthropicDispatcher?.
|
|
205
|
-
const openaiCount = QgridDispatcher.openaiDispatcher?.
|
|
204
|
+
const anthropicCount = QgridDispatcher.anthropicDispatcher?.tokenPool.size ?? 0;
|
|
205
|
+
const openaiCount = QgridDispatcher.openaiDispatcher?.countActiveTokens() ?? 0;
|
|
206
206
|
|
|
207
207
|
log.info(`listening on http://${host}:${port}`);
|
|
208
208
|
log.info(`anthropic: ${anthropicCount} tokens ready`);
|
|
@@ -217,7 +217,7 @@ export default defineConfig({
|
|
|
217
217
|
stopTokenWindowKeepalive();
|
|
218
218
|
stopOpenAICallbackRelay();
|
|
219
219
|
setSettingChangeHandler(null);
|
|
220
|
-
QgridDispatcher.subscriber
|
|
220
|
+
if (QgridDispatcher.subscriber) QgridDispatcher.subscriber.tokenChangeHandler = null;
|
|
221
221
|
if (QgridDispatcher.openaiDispatcher) {
|
|
222
222
|
await QgridDispatcher.openaiDispatcher.stop();
|
|
223
223
|
}
|