@cartanova/qgrid-cli 2.3.5 → 2.3.6
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/oauth.js +15 -12
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -1
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +2 -1
- package/bundle/dist/application/qgrid/qgrid.frame.js +23 -10
- package/bundle/dist/application/qgrid/qgrid.types.js +8 -2
- package/bundle/dist/application/qgrid/token-subscriber.js +16 -8
- package/bundle/dist/application/qgrid/token-trigger-setup.js +3 -2
- package/bundle/dist/application/request-log/request-log.model.js +12 -7
- package/bundle/dist/application/sonamu.generated.js +11 -3
- package/bundle/dist/application/sonamu.generated.sso.js +7 -2
- package/bundle/dist/application/token/token.model.js +10 -2
- package/bundle/dist/application/token/token.types.js +5 -3
- package/bundle/dist/i18n/sd.generated.js +4 -1
- package/bundle/dist/migrations/20260630223950_alter_tokens_add1_alter1.js +16 -0
- package/bundle/dist/migrations/20260701105327_alter_request_logs_add1_alter1.js +16 -0
- package/bundle/dist/migrations/20260701105328_alter_request_log_steps_add1_alter1.js +16 -0
- package/bundle/dist/sonamu.config.js +13 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +99 -12
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +31 -0
- package/bundle/dist/utils/providers/anthropic/claude-session.js +8 -3
- package/bundle/dist/utils/providers/common/ttft.js +25 -0
- package/bundle/dist/utils/providers/openai/codex-worker.js +35 -7
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +237 -39
- package/bundle/dist/utils/providers/openai/openai-quota.js +30 -0
- package/bundle/src/application/qgrid/oauth.ts +17 -7
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +96 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +1 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +16 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +1 -0
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +128 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +41 -20
- package/bundle/src/application/qgrid/qgrid.types.ts +6 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +89 -0
- package/bundle/src/application/qgrid/token-subscriber.ts +40 -11
- package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -1
- package/bundle/src/application/qgrid/tool-emulation.test.ts +7 -3
- package/bundle/src/application/request-log/request-log.entity.json +3 -0
- package/bundle/src/application/request-log/request-log.model.test.ts +92 -0
- package/bundle/src/application/request-log/request-log.model.ts +20 -6
- package/bundle/src/application/request-log-step/request-log-step.entity.json +3 -0
- package/bundle/src/application/sonamu.generated.http +7 -2
- package/bundle/src/application/sonamu.generated.sso.ts +5 -0
- package/bundle/src/application/sonamu.generated.ts +11 -1
- package/bundle/src/application/token/token.entity.json +8 -2
- package/bundle/src/application/token/token.model.test.ts +69 -0
- package/bundle/src/application/token/token.model.ts +8 -1
- package/bundle/src/application/token/token.types.test.ts +34 -0
- package/bundle/src/application/token/token.types.ts +5 -0
- package/bundle/src/i18n/sd.generated.ts +3 -0
- package/bundle/src/migrations/20260630223950_alter_tokens_add1_alter1.ts +15 -0
- package/bundle/src/migrations/20260701105327_alter_request_logs_add1_alter1.ts +15 -0
- package/bundle/src/migrations/20260701105328_alter_request_log_steps_add1_alter1.ts +15 -0
- package/bundle/src/sonamu.config.ts +8 -1
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +183 -15
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +142 -12
- package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +89 -0
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +27 -0
- package/bundle/src/utils/providers/anthropic/claude-session.ts +7 -2
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -0
- package/bundle/src/utils/providers/common/ttft.test.ts +37 -0
- package/bundle/src/utils/providers/common/ttft.ts +26 -0
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +148 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +50 -8
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +378 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +332 -37
- package/bundle/src/utils/providers/openai/openai-quota.test.ts +85 -0
- package/bundle/src/utils/providers/openai/openai-quota.ts +46 -0
- package/bundle/web-dist/client/assets/index-5WdFEqZT.css +1 -0
- package/bundle/web-dist/client/assets/{index-DeThumA3.js → index-jnUY7nJ-.js} +3 -3
- package/bundle/web-dist/client/assets/logs-B0pWpDrg.js +1 -0
- package/bundle/web-dist/client/assets/routes-7XhVuG7o.js +5 -0
- package/bundle/web-dist/client/assets/{sd.generated-7jzAQL2_.js → sd.generated-DXhjjL2f.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-Bx1Eh7ot.js → services.generated-B7u0U1wC.js} +1 -1
- package/bundle/web-dist/client/assets/{show-C6OofoAL.js → show-CcjZ9nr7.js} +3 -3
- package/bundle/web-dist/client/assets/tokens-DlwAm0v4.js +1 -0
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BEaI4mOq.js → lazyRouteComponent-B0LkbiYf.js} +1 -2
- package/bundle/web-dist/server/assets/{logs-CHM0fhfO.js → logs-dLx6gMXT.js} +17 -5
- package/bundle/web-dist/server/assets/{routes-C9xFMJ_r.js → routes-DD3Hb9xO.js} +201 -13
- package/bundle/web-dist/server/assets/{sd.generated-BeUHLWKH.js → sd.generated-DdfpD_iN.js} +131 -1
- package/bundle/web-dist/server/assets/{services.generated-C39hdSv0.js → services.generated-D3ZCvIkt.js} +5 -4
- package/bundle/web-dist/server/assets/{show-BZWqH09U.js → show-BQjeyv6A.js} +76 -25
- package/bundle/web-dist/server/assets/{tokens-IPwv3pNB.js → tokens-CSOsBpP7.js} +4 -3
- package/bundle/web-dist/server/entry-server.generated.js +6 -7
- package/dist/cli.js +67 -15
- package/package.json +1 -1
- package/bundle/web-dist/client/assets/index-B5DSo5uH.css +0 -1
- package/bundle/web-dist/client/assets/logs-CtmOVH58.js +0 -1
- package/bundle/web-dist/client/assets/routes-Cfoe9J0I.js +0 -5
- package/bundle/web-dist/client/assets/tokens-BAZq8sTo.js +0 -1
- package/bundle/web-dist/server/assets/shim-B9ZrFt3Z.js +0 -129
- /package/bundle/web-dist/server/assets/{cost-DzjfVtUr.js → cost-CSH_t17I.js} +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fetchUsageWithMeta } from "../../../application/qgrid/oauth";
|
|
2
|
+
|
|
3
|
+
export type AnthropicQuotaUsageResult =
|
|
4
|
+
| { kind: "ok"; utilizationPct: number; cacheAgeMs: number }
|
|
5
|
+
| { kind: "lookup_failed"; reason: string };
|
|
6
|
+
|
|
7
|
+
export async function readAnthropicQuotaUsage(
|
|
8
|
+
accessToken: string,
|
|
9
|
+
): Promise<AnthropicQuotaUsageResult> {
|
|
10
|
+
try {
|
|
11
|
+
const { data, cachedAt } = await fetchUsageWithMeta(accessToken);
|
|
12
|
+
if (data.error) return { kind: "lookup_failed", reason: data.error };
|
|
13
|
+
|
|
14
|
+
const utilization = data.five_hour?.utilization;
|
|
15
|
+
if (typeof utilization !== "number" || !Number.isFinite(utilization)) {
|
|
16
|
+
return { kind: "lookup_failed", reason: "five_hour utilization unavailable" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
kind: "ok",
|
|
21
|
+
utilizationPct: utilization,
|
|
22
|
+
cacheAgeMs: Math.max(Date.now() - cachedAt, 0),
|
|
23
|
+
};
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return { kind: "lookup_failed", reason: (e as Error).message };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -21,6 +21,7 @@ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
21
21
|
import { type JsonValue } from "../../../codex-protocol/serde_json/JsonValue";
|
|
22
22
|
import { type TokenUsageBreakdown } from "../../../codex-protocol/v2/TokenUsageBreakdown";
|
|
23
23
|
import { type UserInput } from "../../../codex-protocol/v2/UserInput";
|
|
24
|
+
import { createTtftTracker } from "../common/ttft";
|
|
24
25
|
import {
|
|
25
26
|
ANTHROPIC_CLAUDE_CWD,
|
|
26
27
|
anthropicConfigDir,
|
|
@@ -111,6 +112,7 @@ export interface ClaudeSessionRequest {
|
|
|
111
112
|
export interface ClaudeSessionResult extends ClaudeStreamResult {
|
|
112
113
|
sessionId: string;
|
|
113
114
|
workerId: number;
|
|
115
|
+
ttftMs: number | null;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// per-token CLAUDE_CONFIG_DIR 보장(격리). 경로 규칙은 anthropicConfigDir(순수 함수)이 소유 —
|
|
@@ -305,6 +307,8 @@ export function runClaudeSession(
|
|
|
305
307
|
let settled = false;
|
|
306
308
|
let buffer = "";
|
|
307
309
|
const streamState: ClaudeStreamJsonState = {};
|
|
310
|
+
const ttftTracker = createTtftTracker();
|
|
311
|
+
const onTrackedDelta = ttftTracker.wrapDelta(onDelta);
|
|
308
312
|
// bounded stderr 버퍼(최근 ~4KB만 유지 — 무한 누적 방지).
|
|
309
313
|
let stderrBuf = "";
|
|
310
314
|
const STDERR_CAP = 4096;
|
|
@@ -339,7 +343,7 @@ export function runClaudeSession(
|
|
|
339
343
|
clearTimeout(timer);
|
|
340
344
|
cleanupAbort?.();
|
|
341
345
|
cleanupSpawnFiles();
|
|
342
|
-
resolve({ ...result, sessionId, workerId });
|
|
346
|
+
resolve({ ...result, sessionId, workerId, ttftMs: ttftTracker.value() });
|
|
343
347
|
};
|
|
344
348
|
|
|
345
349
|
const onAbort = () => {
|
|
@@ -360,7 +364,7 @@ export function runClaudeSession(
|
|
|
360
364
|
const lines = buffer.split("\n");
|
|
361
365
|
buffer = lines.pop() ?? "";
|
|
362
366
|
for (const line of lines) {
|
|
363
|
-
const result = handleStreamJsonLine(line,
|
|
367
|
+
const result = handleStreamJsonLine(line, onTrackedDelta, {
|
|
364
368
|
structuredOutput: useStructured,
|
|
365
369
|
state: streamState,
|
|
366
370
|
});
|
|
@@ -389,6 +393,7 @@ export function runClaudeSession(
|
|
|
389
393
|
});
|
|
390
394
|
|
|
391
395
|
// stdin 으로 입력 JSONL 흘리고 닫는다.
|
|
396
|
+
ttftTracker.markStart();
|
|
392
397
|
child.stdin?.write(stdinPayload);
|
|
393
398
|
child.stdin?.end();
|
|
394
399
|
});
|
|
@@ -49,6 +49,7 @@ export interface GenerateResult {
|
|
|
49
49
|
tokenName: string;
|
|
50
50
|
usage: ProviderTokenUsageBreakdown;
|
|
51
51
|
durationMs: number;
|
|
52
|
+
ttftMs?: number | null;
|
|
52
53
|
// Provider 가 직접 산출한 비용. Anthropic Claude Code 는 total_cost_usd 를 주므로 이 값을
|
|
53
54
|
// 우선 사용하고, 없으면 상위가 모델별 가격표로 계산한다.
|
|
54
55
|
costUsd?: number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createTtftTracker } from "./ttft";
|
|
4
|
+
|
|
5
|
+
describe("createTtftTracker", () => {
|
|
6
|
+
it("records the first delta once relative to the marked start", () => {
|
|
7
|
+
let now = 100;
|
|
8
|
+
const tracker = createTtftTracker(() => now);
|
|
9
|
+
const deltas: string[] = [];
|
|
10
|
+
const onDelta = tracker.wrapDelta((text) => deltas.push(text));
|
|
11
|
+
|
|
12
|
+
tracker.markStart();
|
|
13
|
+
now = 135;
|
|
14
|
+
onDelta("a");
|
|
15
|
+
now = 200;
|
|
16
|
+
onDelta("b");
|
|
17
|
+
|
|
18
|
+
expect(tracker.value()).toBe(35);
|
|
19
|
+
expect(deltas).toEqual(["a", "b"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("keeps null when no delta is recorded", () => {
|
|
23
|
+
const tracker = createTtftTracker(() => 100);
|
|
24
|
+
|
|
25
|
+
tracker.markStart();
|
|
26
|
+
|
|
27
|
+
expect(tracker.value()).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not treat a delta before start as zero", () => {
|
|
31
|
+
const tracker = createTtftTracker(() => 100);
|
|
32
|
+
|
|
33
|
+
tracker.recordFirstDelta();
|
|
34
|
+
|
|
35
|
+
expect(tracker.value()).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createTtftTracker(now: () => number = Date.now): {
|
|
2
|
+
markStart: () => void;
|
|
3
|
+
recordFirstDelta: () => void;
|
|
4
|
+
value: () => number | null;
|
|
5
|
+
wrapDelta: (onDelta: (text: string) => void) => (text: string) => void;
|
|
6
|
+
} {
|
|
7
|
+
let startedAt: number | null = null;
|
|
8
|
+
let ttftMs: number | null = null;
|
|
9
|
+
|
|
10
|
+
const recordFirstDelta = () => {
|
|
11
|
+
if (startedAt === null || ttftMs !== null) return;
|
|
12
|
+
ttftMs = Math.max(0, now() - startedAt);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
markStart: () => {
|
|
17
|
+
startedAt = now();
|
|
18
|
+
},
|
|
19
|
+
recordFirstDelta,
|
|
20
|
+
value: () => ttftMs,
|
|
21
|
+
wrapDelta: (onDelta) => (text) => {
|
|
22
|
+
recordFirstDelta();
|
|
23
|
+
onDelta(text);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -53,6 +53,41 @@ function createWorkerWithRequestSpy() {
|
|
|
53
53
|
return { worker, request };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function createWorkerWithNotificationRpc(
|
|
57
|
+
onRequest: (
|
|
58
|
+
method: string,
|
|
59
|
+
params: unknown,
|
|
60
|
+
handlers: Map<string, (params: never) => void>,
|
|
61
|
+
) => Promise<unknown>,
|
|
62
|
+
) {
|
|
63
|
+
const worker = new CodexAppServerWorker({
|
|
64
|
+
tokenId: 1,
|
|
65
|
+
tokenName: "token",
|
|
66
|
+
accessToken: "access",
|
|
67
|
+
accountId: "account",
|
|
68
|
+
});
|
|
69
|
+
const handlers = new Map<string, (params: never) => void>();
|
|
70
|
+
const request = vi.fn((method: string, params?: unknown) => onRequest(method, params, handlers));
|
|
71
|
+
const rpc = {
|
|
72
|
+
request,
|
|
73
|
+
onNotification(method: string, handler: (params: never) => void) {
|
|
74
|
+
handlers.set(method, handler);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
(
|
|
78
|
+
worker as unknown as {
|
|
79
|
+
rpc: typeof rpc;
|
|
80
|
+
ready: boolean;
|
|
81
|
+
}
|
|
82
|
+
).rpc = rpc;
|
|
83
|
+
(
|
|
84
|
+
worker as unknown as {
|
|
85
|
+
ready: boolean;
|
|
86
|
+
}
|
|
87
|
+
).ready = true;
|
|
88
|
+
return { worker, request, handlers };
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
describe("CodexAppServerWorker active turn cleanup", () => {
|
|
57
92
|
it("rejects a non-streaming turn when the worker process exits", async () => {
|
|
58
93
|
const { worker } = createWorkerWithFakeRpc();
|
|
@@ -101,6 +136,119 @@ describe("CodexAppServerWorker active turn cleanup", () => {
|
|
|
101
136
|
});
|
|
102
137
|
});
|
|
103
138
|
|
|
139
|
+
describe("CodexAppServerWorker TTFT", () => {
|
|
140
|
+
it("captures fast non-stream deltas registered before turn/start completes", async () => {
|
|
141
|
+
let now = 1_000;
|
|
142
|
+
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
143
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
144
|
+
if (method === "thread/start") {
|
|
145
|
+
return { thread: { id: "thread-1" }, model: "gpt-test" };
|
|
146
|
+
}
|
|
147
|
+
if (method === "turn/start") {
|
|
148
|
+
now = 1_025;
|
|
149
|
+
handlers.get("item/agentMessage/delta")?.({
|
|
150
|
+
threadId: "thread-1",
|
|
151
|
+
delta: "h",
|
|
152
|
+
} as never);
|
|
153
|
+
now = 1_050;
|
|
154
|
+
handlers.get("item/agentMessage/delta")?.({
|
|
155
|
+
threadId: "thread-1",
|
|
156
|
+
delta: "i",
|
|
157
|
+
} as never);
|
|
158
|
+
handlers.get("turn/completed")?.({
|
|
159
|
+
threadId: "thread-1",
|
|
160
|
+
turn: { status: "completed", durationMs: 90 },
|
|
161
|
+
} as never);
|
|
162
|
+
return { turn: { id: "turn-1" } };
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`unexpected request: ${method}`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await worker.executeTurn({
|
|
169
|
+
input: [{ type: "text", text: "hi", text_elements: [] }],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.text).toBe("hi");
|
|
173
|
+
expect(result.ttftMs).toBe(25);
|
|
174
|
+
} finally {
|
|
175
|
+
nowSpy.mockRestore();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("keeps non-stream ttft null when no delta arrives", async () => {
|
|
180
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
181
|
+
if (method === "thread/start") {
|
|
182
|
+
return { thread: { id: "thread-1" }, model: "gpt-test" };
|
|
183
|
+
}
|
|
184
|
+
if (method === "turn/start") {
|
|
185
|
+
handlers.get("turn/completed")?.({
|
|
186
|
+
threadId: "thread-1",
|
|
187
|
+
turn: { status: "completed", durationMs: 90 },
|
|
188
|
+
} as never);
|
|
189
|
+
return { turn: { id: "turn-1" } };
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`unexpected request: ${method}`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await worker.executeTurn({
|
|
195
|
+
input: [{ type: "text", text: "hi", text_elements: [] }],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.ttftMs).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("captures stream ttft from the first delta only", async () => {
|
|
202
|
+
let now = 2_000;
|
|
203
|
+
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
204
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
205
|
+
if (method === "thread/start") {
|
|
206
|
+
return { thread: { id: "thread-1" }, model: "gpt-test" };
|
|
207
|
+
}
|
|
208
|
+
if (method === "turn/start") {
|
|
209
|
+
now = 2_030;
|
|
210
|
+
handlers.get("item/agentMessage/delta")?.({
|
|
211
|
+
threadId: "thread-1",
|
|
212
|
+
delta: "a",
|
|
213
|
+
} as never);
|
|
214
|
+
now = 2_080;
|
|
215
|
+
handlers.get("item/agentMessage/delta")?.({
|
|
216
|
+
threadId: "thread-1",
|
|
217
|
+
delta: "b",
|
|
218
|
+
} as never);
|
|
219
|
+
handlers.get("turn/completed")?.({
|
|
220
|
+
threadId: "thread-1",
|
|
221
|
+
turn: { status: "completed", durationMs: 120 },
|
|
222
|
+
} as never);
|
|
223
|
+
return { turn: { id: "turn-1" } };
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`unexpected request: ${method}`);
|
|
226
|
+
});
|
|
227
|
+
const callbacks: StreamCallbacks = {
|
|
228
|
+
onDelta: vi.fn(),
|
|
229
|
+
onComplete: vi.fn(),
|
|
230
|
+
onError: vi.fn(),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await worker.executeTurnStream(
|
|
235
|
+
{
|
|
236
|
+
input: [{ type: "text", text: "hi", text_elements: [] }],
|
|
237
|
+
},
|
|
238
|
+
callbacks,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(callbacks.onDelta).toHaveBeenCalledWith("a");
|
|
242
|
+
expect(callbacks.onDelta).toHaveBeenCalledWith("b");
|
|
243
|
+
expect(callbacks.onComplete).toHaveBeenCalledWith(
|
|
244
|
+
expect.objectContaining({ text: "ab", ttftMs: 30 }),
|
|
245
|
+
);
|
|
246
|
+
} finally {
|
|
247
|
+
nowSpy.mockRestore();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
104
252
|
describe("CodexAppServerWorker prompt prefix", () => {
|
|
105
253
|
it("uses the same base instructions with and without an output schema", async () => {
|
|
106
254
|
type WorkerInternals = {
|
|
@@ -10,6 +10,7 @@ import { type TokenUsageBreakdown } from "../../../codex-protocol/v2/TokenUsageB
|
|
|
10
10
|
import { type TurnStartParams } from "../../../codex-protocol/v2/TurnStartParams";
|
|
11
11
|
import { type TurnStartResponse } from "../../../codex-protocol/v2/TurnStartResponse";
|
|
12
12
|
import { type StreamCallbacks as CommonStreamCallbacks } from "../common/provider-dispatcher";
|
|
13
|
+
import { createTtftTracker } from "../common/ttft";
|
|
13
14
|
import { CodexRpcClient } from "./codex-rpc";
|
|
14
15
|
|
|
15
16
|
const logger = getLogger(["qgrid", "codex-worker"]);
|
|
@@ -44,6 +45,7 @@ export interface TurnResult {
|
|
|
44
45
|
text: string;
|
|
45
46
|
usage: TokenUsageBreakdown;
|
|
46
47
|
durationMs: number;
|
|
48
|
+
ttftMs: number | null;
|
|
47
49
|
model: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -387,8 +389,17 @@ export class CodexAppServerWorker {
|
|
|
387
389
|
} else {
|
|
388
390
|
({ threadId, model } = await this.createThread(req));
|
|
389
391
|
}
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
+
const ttftTracker = createTtftTracker();
|
|
393
|
+
const resultPromise = this.consumeTurnNotifications(threadId, model, ttftTracker);
|
|
394
|
+
try {
|
|
395
|
+
ttftTracker.markStart();
|
|
396
|
+
await this.startTurnOnThread(threadId, req);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
this.failActiveTurn(e as Error);
|
|
399
|
+
await resultPromise.catch(() => {});
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
const result = await resultPromise;
|
|
392
403
|
return { ...result, threadId };
|
|
393
404
|
}
|
|
394
405
|
|
|
@@ -405,10 +416,20 @@ export class CodexAppServerWorker {
|
|
|
405
416
|
} else {
|
|
406
417
|
({ threadId, model } = await this.createThread(req));
|
|
407
418
|
}
|
|
408
|
-
const { turnId } = await this.startTurnOnThread(threadId, req);
|
|
409
419
|
cb.onThreadId?.(threadId);
|
|
420
|
+
const ttftTracker = createTtftTracker();
|
|
421
|
+
const resultPromise = this.consumeStreamNotifications(threadId, model, cb, ttftTracker);
|
|
422
|
+
let turnId: string;
|
|
423
|
+
try {
|
|
424
|
+
ttftTracker.markStart();
|
|
425
|
+
({ turnId } = await this.startTurnOnThread(threadId, req));
|
|
426
|
+
} catch (e) {
|
|
427
|
+
this.failActiveTurn(e as Error);
|
|
428
|
+
await resultPromise.catch(() => {});
|
|
429
|
+
throw e;
|
|
430
|
+
}
|
|
410
431
|
cb.onTurnId?.(turnId);
|
|
411
|
-
await
|
|
432
|
+
await resultPromise;
|
|
412
433
|
return { threadId };
|
|
413
434
|
}
|
|
414
435
|
|
|
@@ -456,7 +477,11 @@ export class CodexAppServerWorker {
|
|
|
456
477
|
abort?.(error);
|
|
457
478
|
}
|
|
458
479
|
|
|
459
|
-
private consumeTurnNotifications(
|
|
480
|
+
private consumeTurnNotifications(
|
|
481
|
+
threadId: string,
|
|
482
|
+
model: string,
|
|
483
|
+
ttftTracker = createTtftTracker(),
|
|
484
|
+
): Promise<TurnResult> {
|
|
460
485
|
return new Promise<TurnResult>((resolve, reject) => {
|
|
461
486
|
let settled = false;
|
|
462
487
|
let abort: ActiveTurnAbort;
|
|
@@ -510,6 +535,12 @@ export class CodexAppServerWorker {
|
|
|
510
535
|
if (p.item.type === "agentMessage") text = p.item.text;
|
|
511
536
|
});
|
|
512
537
|
|
|
538
|
+
this.rpc!.onNotification("item/agentMessage/delta", (p) => {
|
|
539
|
+
if (p.threadId !== threadId) return;
|
|
540
|
+
ttftTracker.recordFirstDelta();
|
|
541
|
+
text += p.delta;
|
|
542
|
+
});
|
|
543
|
+
|
|
513
544
|
this.rpc!.onNotification("thread/tokenUsage/updated", (p) => {
|
|
514
545
|
if (p.threadId !== threadId) return;
|
|
515
546
|
// thread 재사용 시 .total 은 대화 전체 누적이라, cold 였던 이전 turn 까지 섞여
|
|
@@ -521,8 +552,11 @@ export class CodexAppServerWorker {
|
|
|
521
552
|
this.rpc!.onNotification("turn/completed", (p) => {
|
|
522
553
|
if (p.threadId !== threadId) return;
|
|
523
554
|
durationMs = p.turn.durationMs ?? 0;
|
|
524
|
-
if (p.turn.status === "completed")
|
|
525
|
-
|
|
555
|
+
if (p.turn.status === "completed") {
|
|
556
|
+
finishWithResult({ text, usage, durationMs, ttftMs: ttftTracker.value(), model });
|
|
557
|
+
} else {
|
|
558
|
+
finishWithError(new Error(`turn failed: ${JSON.stringify(p.turn.error)}`));
|
|
559
|
+
}
|
|
526
560
|
});
|
|
527
561
|
|
|
528
562
|
this.rpc!.onNotification("error", (p) => {
|
|
@@ -538,6 +572,7 @@ export class CodexAppServerWorker {
|
|
|
538
572
|
threadId: string,
|
|
539
573
|
model: string,
|
|
540
574
|
cb: StreamCallbacks,
|
|
575
|
+
ttftTracker = createTtftTracker(),
|
|
541
576
|
): Promise<void> {
|
|
542
577
|
return new Promise<void>((resolve, reject) => {
|
|
543
578
|
let settled = false;
|
|
@@ -590,6 +625,7 @@ export class CodexAppServerWorker {
|
|
|
590
625
|
|
|
591
626
|
this.rpc!.onNotification("item/agentMessage/delta", (p) => {
|
|
592
627
|
if (p.threadId !== threadId) return;
|
|
628
|
+
ttftTracker.recordFirstDelta();
|
|
593
629
|
text += p.delta;
|
|
594
630
|
cb.onDelta(p.delta);
|
|
595
631
|
});
|
|
@@ -610,7 +646,13 @@ export class CodexAppServerWorker {
|
|
|
610
646
|
this.rpc!.onNotification("turn/completed", (p) => {
|
|
611
647
|
if (p.threadId !== threadId) return;
|
|
612
648
|
if (p.turn.status === "completed") {
|
|
613
|
-
finishWithComplete({
|
|
649
|
+
finishWithComplete({
|
|
650
|
+
text,
|
|
651
|
+
usage,
|
|
652
|
+
durationMs: p.turn.durationMs ?? 0,
|
|
653
|
+
ttftMs: ttftTracker.value(),
|
|
654
|
+
model,
|
|
655
|
+
});
|
|
614
656
|
} else {
|
|
615
657
|
finishWithError(new Error(`turn ${p.turn.status}: ${JSON.stringify(p.turn.error)}`));
|
|
616
658
|
}
|