@cartanova/qgrid-cli 2.4.4 → 2.4.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/monit/log-buffer.js +76 -0
- package/bundle/dist/application/monit/monit.frame.js +86 -0
- package/bundle/dist/application/monit/monit.types.js +42 -0
- package/bundle/dist/application/qgrid/oauth.js +1 -1
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +3 -3
- package/bundle/dist/application/qgrid/qgrid.frame.js +14 -12
- package/bundle/dist/application/qgrid/qgrid.types.js +3 -2
- package/bundle/dist/application/queries.generated.js +7 -2
- package/bundle/dist/application/request-log/request-log.model.js +1 -1
- package/bundle/dist/sonamu.config.js +14 -10
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +5 -2
- package/bundle/dist/utils/providers/anthropic/claude-session.js +3 -3
- package/bundle/dist/utils/providers/openai/codex-worker.js +5 -3
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +179 -15
- package/bundle/src/application/monit/log-buffer.test.ts +121 -0
- package/bundle/src/application/monit/log-buffer.ts +121 -0
- package/bundle/src/application/monit/monit.frame.test.ts +176 -0
- package/bundle/src/application/monit/monit.frame.ts +77 -0
- package/bundle/src/application/monit/monit.types.ts +54 -0
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +24 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +2 -0
- package/bundle/src/application/qgrid/qgrid.types.ts +1 -0
- package/bundle/src/application/queries.generated.ts +8 -0
- package/bundle/src/application/sonamu.generated.http +12 -0
- package/bundle/src/sonamu.config.ts +5 -2
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +5 -0
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +26 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +3 -1
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +212 -7
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +297 -17
- package/bundle/web-dist/client/assets/index-BOXhNo0k.js +78 -0
- package/bundle/web-dist/client/assets/index-yZcjDoV0.css +1 -0
- package/bundle/web-dist/client/assets/{logs-vuI-Zgnq.js → logs-BShH_sut.js} +1 -1
- package/bundle/web-dist/client/assets/monit-CoEAp5S0.js +1 -0
- package/bundle/web-dist/client/assets/{routes-0Fki0JEi.js → routes-Dr3cGnV3.js} +2 -2
- package/bundle/web-dist/client/assets/{sd.generated-WA5SGJYZ.js → sd.generated-D2Y9QeiN.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-DzZLnb7S.js → services.generated-CmxXl220.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BreAgaFH.js → show-CXII6Iy7.js} +3 -3
- package/bundle/web-dist/client/assets/{tokens-4VgHQtXd.js → tokens-hP1NecIg.js} +1 -1
- package/bundle/web-dist/client/assets/use-update-token-mutation-6J1T9U2-.js +1 -0
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BrPAFtBA.js → lazyRouteComponent-BlfOh9S3.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-CKM6W3W6.js → logs-DYly4ipQ.js} +3 -3
- package/bundle/web-dist/server/assets/monit-CYyY2S2f.js +1577 -0
- package/bundle/web-dist/server/assets/{routes-DhIpPgXz.js → routes-Dqu3osXt.js} +14 -6
- package/bundle/web-dist/server/assets/{sd.generated-Cwg2f53z.js → sd.generated-CE6D5clm.js} +1 -1
- package/bundle/web-dist/server/assets/{services.generated-CsotZP44.js → services.generated-DyZUzXiq.js} +39 -2
- package/bundle/web-dist/server/assets/{show-CV2UFjaH.js → show-BDul8xLb.js} +3 -3
- package/bundle/web-dist/server/assets/{tokens-DfGj1y2v.js → tokens-fzMW4G74.js} +4 -4
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BL96w-Pg.js → use-update-token-mutation-BejUW5sw.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +71 -27
- package/package.json +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -0
- package/bundle/web-dist/client/assets/index-D1ExP77_.js +0 -78
- package/bundle/web-dist/client/assets/index-Dc-qeEPm.css +0 -1
- package/bundle/web-dist/client/assets/use-update-token-mutation-BV6msjuy.js +0 -1
|
@@ -92,6 +92,32 @@ function createWorkerWithNotificationRpc(
|
|
|
92
92
|
return { worker, request, handlers };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
describe("CodexAppServerWorker home isolation", () => {
|
|
96
|
+
it("places worker homes under the configured OS temp directory", () => {
|
|
97
|
+
const previousTmpdir = process.env.TMPDIR;
|
|
98
|
+
const customTmpdir = mkdtempSync(join(tmpdir(), "qgrid-tmp-root-test-"));
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
process.env.TMPDIR = customTmpdir;
|
|
102
|
+
const worker = new CodexAppServerWorker({
|
|
103
|
+
tokenId: 41,
|
|
104
|
+
tokenName: "token",
|
|
105
|
+
accessToken: "access",
|
|
106
|
+
accountId: "account",
|
|
107
|
+
workerIndex: 14,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect((worker as unknown as { codexHome: string }).codexHome).toBe(
|
|
111
|
+
join(customTmpdir, "qgrid-codex", "41-14"),
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
if (previousTmpdir === undefined) delete process.env.TMPDIR;
|
|
115
|
+
else process.env.TMPDIR = previousTmpdir;
|
|
116
|
+
rmSync(customTmpdir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
95
121
|
describe("CodexAppServerWorker active turn cleanup", () => {
|
|
96
122
|
it("rejects a non-streaming turn when the worker process exits", async () => {
|
|
97
123
|
const { worker } = createWorkerWithFakeRpc();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
|
|
4
6
|
import { getLogger } from "@logtape/logtape";
|
|
5
7
|
|
|
@@ -169,7 +171,7 @@ export class CodexAppServerWorker {
|
|
|
169
171
|
|
|
170
172
|
constructor(private config: WorkerConfig) {
|
|
171
173
|
const suffix = config.workerIndex !== undefined ? `-${config.workerIndex}` : "";
|
|
172
|
-
this.codexHome =
|
|
174
|
+
this.codexHome = join(tmpdir(), "qgrid-codex", `${config.tokenId}${suffix}`);
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
// ── Lifecycle ───────────────────────────────────────────────────
|
|
@@ -4,7 +4,15 @@ import { QuotaThresholdExceededError } from "../../../application/qgrid/qgrid.ty
|
|
|
4
4
|
import { type OpenAICredentials } from "../../../application/token/token.types";
|
|
5
5
|
import { type GenerateRequest, type GenerateResult } from "../common/provider-dispatcher";
|
|
6
6
|
import { type CodexAppServerWorker } from "./codex-worker";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
estimateOpenAIWorkerRssGiB,
|
|
9
|
+
ImageGenerationError,
|
|
10
|
+
makeOpenAIWorkerId,
|
|
11
|
+
MAX_OPENAI_WORKERS_PER_TOKEN,
|
|
12
|
+
type OpenAIWorkerPoolConfig,
|
|
13
|
+
OpenAIDispatcher,
|
|
14
|
+
resolveOpenAIWorkerPoolConfig,
|
|
15
|
+
} from "./openai-dispatcher";
|
|
8
16
|
import { type OpenAIQuotaUsageResult } from "./openai-quota";
|
|
9
17
|
|
|
10
18
|
const { readOpenAIQuotaUsageMock, loggerInfoMock, loggerWarnMock } = vi.hoisted(() => ({
|
|
@@ -70,7 +78,7 @@ function resultFor(worker: CodexAppServerWorker, threadId = "thread-new"): Gener
|
|
|
70
78
|
durationMs: 10,
|
|
71
79
|
model: "gpt-5-codex",
|
|
72
80
|
threadCoord: {
|
|
73
|
-
workerId: worker.tokenId
|
|
81
|
+
workerId: makeOpenAIWorkerId(worker.tokenId, worker.workerIndex),
|
|
74
82
|
threadId,
|
|
75
83
|
epoch: worker.epoch,
|
|
76
84
|
},
|
|
@@ -159,6 +167,21 @@ function addWorkers(dispatcher: OpenAIDispatcher, workers: CodexAppServerWorker[
|
|
|
159
167
|
for (const [tokenId, group] of byToken) dispatcher.workerPool.set(tokenId, group);
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
function autoscaleConfig(
|
|
171
|
+
overrides: Partial<OpenAIWorkerPoolConfig> = {},
|
|
172
|
+
): OpenAIWorkerPoolConfig {
|
|
173
|
+
return {
|
|
174
|
+
autoscale: true,
|
|
175
|
+
minWorkersPerToken: 1,
|
|
176
|
+
maxWorkersPerToken: 3,
|
|
177
|
+
scaleIntervalMs: 5_000,
|
|
178
|
+
scaleDownIdleMs: 600_000,
|
|
179
|
+
maxEstimatedRssGiB: 16,
|
|
180
|
+
minHostAvailableGiB: 20,
|
|
181
|
+
...overrides,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
162
185
|
async function waitForQueue(dispatcher: OpenAIDispatcher, length = 1): Promise<void> {
|
|
163
186
|
for (let i = 0; i < 10; i++) {
|
|
164
187
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -167,6 +190,175 @@ async function waitForQueue(dispatcher: OpenAIDispatcher, length = 1): Promise<v
|
|
|
167
190
|
expect(dispatcher.queueLength).toBe(length);
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
describe("OpenAIDispatcher worker capacity", () => {
|
|
194
|
+
it("supports 20 workers per token without worker id collisions", () => {
|
|
195
|
+
expect(MAX_OPENAI_WORKERS_PER_TOKEN).toBe(20);
|
|
196
|
+
|
|
197
|
+
const tokenIds = [1, 2, 41, 47, 55, 56, 58];
|
|
198
|
+
const workerIds = tokenIds.flatMap((tokenId) =>
|
|
199
|
+
Array.from({ length: MAX_OPENAI_WORKERS_PER_TOKEN }, (_, workerIndex) =>
|
|
200
|
+
makeOpenAIWorkerId(tokenId, workerIndex),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(new Set(workerIds)).toHaveLength(workerIds.length);
|
|
205
|
+
expect(makeOpenAIWorkerId(1, 10)).not.toBe(makeOpenAIWorkerId(2, 0));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("defaults to autoscaling from 5 to 15 workers per token", () => {
|
|
209
|
+
expect(resolveOpenAIWorkerPoolConfig({})).toMatchObject({
|
|
210
|
+
autoscale: true,
|
|
211
|
+
minWorkersPerToken: 5,
|
|
212
|
+
maxWorkersPerToken: 15,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("keeps an explicit fixed worker mode for emergency rollback", () => {
|
|
217
|
+
expect(
|
|
218
|
+
resolveOpenAIWorkerPoolConfig({
|
|
219
|
+
QGRID_WORKERS_PER_TOKEN: "5",
|
|
220
|
+
QGRID_OPENAI_AUTOSCALE: "false",
|
|
221
|
+
}),
|
|
222
|
+
).toMatchObject({
|
|
223
|
+
autoscale: false,
|
|
224
|
+
minWorkersPerToken: 5,
|
|
225
|
+
maxWorkersPerToken: 5,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("estimates worker RSS from the dev0 measurement model", () => {
|
|
230
|
+
expect(estimateOpenAIWorkerRssGiB(50)).toBeCloseTo(8.56, 2);
|
|
231
|
+
expect(estimateOpenAIWorkerRssGiB(60)).toBeCloseTo(10.13, 2);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("OpenAIDispatcher monit stats", () => {
|
|
236
|
+
it("reports worker counts grouped by token name", () => {
|
|
237
|
+
const dispatcher = new OpenAIDispatcher(autoscaleConfig({ autoscale: false }), () => 64);
|
|
238
|
+
addWorkers(dispatcher, [
|
|
239
|
+
fakeWorker({ tokenId: 1, tokenName: "openai/nk" }),
|
|
240
|
+
fakeWorker({ tokenId: 1, tokenName: "openai/nk", workerIndex: 1 }),
|
|
241
|
+
fakeWorker({ tokenId: 2, tokenName: "openai/haze" }),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
expect(dispatcher.workerCountsByToken).toEqual([
|
|
245
|
+
{ name: "openai/haze", count: 1 },
|
|
246
|
+
{ name: "openai/nk", count: 2 },
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("OpenAIDispatcher autoscaling", () => {
|
|
252
|
+
it("scales six active tokens from 5 to 15 workers per token", async () => {
|
|
253
|
+
const dispatcher = new OpenAIDispatcher(
|
|
254
|
+
autoscaleConfig({
|
|
255
|
+
minWorkersPerToken: 5,
|
|
256
|
+
maxWorkersPerToken: 15,
|
|
257
|
+
maxEstimatedRssGiB: 16,
|
|
258
|
+
}),
|
|
259
|
+
() => 64,
|
|
260
|
+
);
|
|
261
|
+
vi.spyOn(dispatcher, "spawnWorkers").mockResolvedValue(undefined);
|
|
262
|
+
|
|
263
|
+
for (let tokenId = 1; tokenId <= 6; tokenId++) {
|
|
264
|
+
await dispatcher.onTokenAdded(tokenId, `token-${tokenId}`, credentials(), null, 1);
|
|
265
|
+
dispatcher.workerPool.set(
|
|
266
|
+
tokenId,
|
|
267
|
+
Array.from({ length: 5 }, (_, workerIndex) => fakeWorker({ tokenId, workerIndex })),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
vi.spyOn(dispatcher, "spawnSingleWorker").mockImplementation(
|
|
272
|
+
async (tokenId, tokenName, _credentials, workerIndex) =>
|
|
273
|
+
fakeWorker({ tokenId, tokenName, workerIndex }),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
for (let step = 0; step < 12; step++) await dispatcher.scaleUpOneStep();
|
|
277
|
+
|
|
278
|
+
expect(dispatcher.workerCount).toBe(90);
|
|
279
|
+
expect([...dispatcher.workerPool.values()].map((workers) => workers.length)).toEqual([
|
|
280
|
+
15, 15, 15, 15, 15, 15,
|
|
281
|
+
]);
|
|
282
|
+
expect(estimateOpenAIWorkerRssGiB(dispatcher.workerCount)).toBeLessThan(16);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("requests a scale-up evaluation when a request enters the queue", async () => {
|
|
286
|
+
const dispatcher = new OpenAIDispatcher(autoscaleConfig(), () => 64);
|
|
287
|
+
addWorkers(dispatcher, [fakeWorker({ tokenId: 1, busy: true })]);
|
|
288
|
+
const scaleUpOneStep = vi.spyOn(dispatcher, "scaleUpOneStep").mockResolvedValue(undefined);
|
|
289
|
+
|
|
290
|
+
const queued = dispatcher.enqueue(async () => "ok");
|
|
291
|
+
const rejection = expect(queued).rejects.toThrow("TEST_CLEANUP");
|
|
292
|
+
await waitForQueue(dispatcher);
|
|
293
|
+
dispatcher.rejectAllQueued("TEST_CLEANUP");
|
|
294
|
+
|
|
295
|
+
await rejection;
|
|
296
|
+
expect(scaleUpOneStep).toHaveBeenCalledTimes(1);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("adds one worker to each active token in a scale-up step", async () => {
|
|
300
|
+
const dispatcher = new OpenAIDispatcher(autoscaleConfig(), () => 64);
|
|
301
|
+
const tokenCredentials = credentials();
|
|
302
|
+
vi.spyOn(dispatcher, "spawnWorkers").mockResolvedValue(undefined);
|
|
303
|
+
await dispatcher.onTokenAdded(1, "token", tokenCredentials, null, 1);
|
|
304
|
+
dispatcher.workerPool.set(1, [fakeWorker({ tokenId: 1, workerIndex: 0 })]);
|
|
305
|
+
const added = fakeWorker({ tokenId: 1, workerIndex: 1 });
|
|
306
|
+
const spawnSingleWorker = vi.spyOn(dispatcher, "spawnSingleWorker").mockResolvedValue(added);
|
|
307
|
+
|
|
308
|
+
await dispatcher.scaleUpOneStep();
|
|
309
|
+
|
|
310
|
+
expect(spawnSingleWorker).toHaveBeenCalledWith(1, "token", tokenCredentials, 1);
|
|
311
|
+
expect(dispatcher.workerPool.get(1)).toHaveLength(2);
|
|
312
|
+
expect(dispatcher.workerCount).toBe(2);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("does not scale past the estimated RSS guard", async () => {
|
|
316
|
+
const dispatcher = new OpenAIDispatcher(
|
|
317
|
+
autoscaleConfig({ maxEstimatedRssGiB: 0.9 }),
|
|
318
|
+
() => 64,
|
|
319
|
+
);
|
|
320
|
+
vi.spyOn(dispatcher, "spawnWorkers").mockResolvedValue(undefined);
|
|
321
|
+
await dispatcher.onTokenAdded(1, "token", credentials(), null, 1);
|
|
322
|
+
dispatcher.workerPool.set(1, [fakeWorker({ tokenId: 1, workerIndex: 0 })]);
|
|
323
|
+
const spawnSingleWorker = vi.spyOn(dispatcher, "spawnSingleWorker");
|
|
324
|
+
|
|
325
|
+
await dispatcher.scaleUpOneStep();
|
|
326
|
+
|
|
327
|
+
expect(spawnSingleWorker).not.toHaveBeenCalled();
|
|
328
|
+
expect(dispatcher.workerCount).toBe(1);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("does not scale when host available memory is below the floor", async () => {
|
|
332
|
+
const dispatcher = new OpenAIDispatcher(autoscaleConfig(), () => 10);
|
|
333
|
+
vi.spyOn(dispatcher, "spawnWorkers").mockResolvedValue(undefined);
|
|
334
|
+
await dispatcher.onTokenAdded(1, "token", credentials(), null, 1);
|
|
335
|
+
dispatcher.workerPool.set(1, [fakeWorker({ tokenId: 1, workerIndex: 0 })]);
|
|
336
|
+
const spawnSingleWorker = vi.spyOn(dispatcher, "spawnSingleWorker");
|
|
337
|
+
|
|
338
|
+
await dispatcher.scaleUpOneStep();
|
|
339
|
+
|
|
340
|
+
expect(spawnSingleWorker).not.toHaveBeenCalled();
|
|
341
|
+
expect(dispatcher.workerCount).toBe(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("removes only idle workers above the configured minimum for inactive tokens", async () => {
|
|
345
|
+
const dispatcher = new OpenAIDispatcher(autoscaleConfig(), () => 64);
|
|
346
|
+
vi.spyOn(dispatcher, "spawnWorkers").mockResolvedValue(undefined);
|
|
347
|
+
await dispatcher.onTokenAdded(1, "token", credentials(), null, 1);
|
|
348
|
+
const busy = fakeWorker({ tokenId: 1, workerIndex: 0, busy: true });
|
|
349
|
+
const idle = fakeWorker({ tokenId: 1, workerIndex: 1 });
|
|
350
|
+
dispatcher.workerPool.set(1, [busy, idle]);
|
|
351
|
+
dispatcher.onTokenDeactivated(1);
|
|
352
|
+
|
|
353
|
+
await dispatcher.scaleDownOneStep();
|
|
354
|
+
await dispatcher.scaleDownOneStep();
|
|
355
|
+
|
|
356
|
+
expect(idle.kill).toHaveBeenCalledTimes(1);
|
|
357
|
+
expect(busy.kill).not.toHaveBeenCalled();
|
|
358
|
+
expect(dispatcher.workerPool.get(1)).toEqual([busy]);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
170
362
|
describe("OpenAIDispatcher token updates", () => {
|
|
171
363
|
it("keeps existing workers when only OpenAI auth tokens rotate", async () => {
|
|
172
364
|
const dispatcher = new OpenAIDispatcher();
|
|
@@ -329,7 +521,9 @@ describe("OpenAIDispatcher weighted routing", () => {
|
|
|
329
521
|
|
|
330
522
|
const cold1 = await dispatcher.generate(baseReq());
|
|
331
523
|
const reused = await dispatcher.generate(
|
|
332
|
-
baseReq({
|
|
524
|
+
baseReq({
|
|
525
|
+
reuse: { workerId: makeOpenAIWorkerId(2, 0), threadId: "thread-B", epoch: 1 },
|
|
526
|
+
}),
|
|
333
527
|
);
|
|
334
528
|
const cold2 = await dispatcher.generate(baseReq());
|
|
335
529
|
|
|
@@ -470,7 +664,9 @@ describe("OpenAIDispatcher quota threshold gate", () => {
|
|
|
470
664
|
);
|
|
471
665
|
|
|
472
666
|
const result = await dispatcher.generate(
|
|
473
|
-
baseReq({
|
|
667
|
+
baseReq({
|
|
668
|
+
reuse: { workerId: makeOpenAIWorkerId(1, 0), threadId: "thread-1", epoch: 1 },
|
|
669
|
+
}),
|
|
474
670
|
);
|
|
475
671
|
|
|
476
672
|
expect(result.tokenName).toBe("tok-B");
|
|
@@ -492,7 +688,9 @@ describe("OpenAIDispatcher quota threshold gate", () => {
|
|
|
492
688
|
);
|
|
493
689
|
|
|
494
690
|
const result = await dispatcher.generate(
|
|
495
|
-
baseReq({
|
|
691
|
+
baseReq({
|
|
692
|
+
reuse: { workerId: makeOpenAIWorkerId(1, 0), threadId: "thread-1", epoch: 1 },
|
|
693
|
+
}),
|
|
496
694
|
);
|
|
497
695
|
|
|
498
696
|
expect(result.tokenName).toBe("tok-A");
|
|
@@ -543,7 +741,11 @@ describe("OpenAIDispatcher quota threshold gate", () => {
|
|
|
543
741
|
readOpenAIQuotaUsageMock.mockResolvedValueOnce(quotaOk(85));
|
|
544
742
|
|
|
545
743
|
await expect(
|
|
546
|
-
dispatcher.generate(
|
|
744
|
+
dispatcher.generate(
|
|
745
|
+
baseReq({
|
|
746
|
+
reuse: { workerId: makeOpenAIWorkerId(1, 0), threadId: "thread-1", epoch: 1 },
|
|
747
|
+
}),
|
|
748
|
+
),
|
|
547
749
|
).rejects.toBeInstanceOf(QuotaThresholdExceededError);
|
|
548
750
|
});
|
|
549
751
|
});
|
|
@@ -621,7 +823,10 @@ describe("OpenAIDispatcher image generation routing", () => {
|
|
|
621
823
|
it("forces cold thread for image requests even when a reuse coordinate is present", async () => {
|
|
622
824
|
const dispatcher = new OpenAIDispatcher();
|
|
623
825
|
const worker = await dispatcher.acquireReuseWorker(
|
|
624
|
-
baseReq({
|
|
826
|
+
baseReq({
|
|
827
|
+
imageGeneration: true,
|
|
828
|
+
reuse: { workerId: makeOpenAIWorkerId(1, 0), threadId: "t1", epoch: 1 },
|
|
829
|
+
}),
|
|
625
830
|
);
|
|
626
831
|
expect(worker).toBeNull();
|
|
627
832
|
});
|