@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
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
type GenerateStreamCallbacks,
|
|
7
7
|
} from "../../utils/providers/common/provider-dispatcher";
|
|
8
8
|
import { systemHash } from "./conv-routing";
|
|
9
|
+
import { OpenAIDispatcher } from "../../utils/providers/openai/openai-dispatcher";
|
|
10
|
+
import { normalizeOpenAIEvent } from "../../utils/providers/openai/openai-backend-protocol";
|
|
9
11
|
import { buildStrictOutputSchema, QgridDispatcherClass } from "./qgrid.dispatcher";
|
|
10
|
-
import { type QueryOutput } from "./qgrid.types";
|
|
12
|
+
import { QueryInput, type QueryOutput } from "./qgrid.types";
|
|
11
13
|
import { type TokenSubsetA } from "../sonamu.generated";
|
|
12
14
|
|
|
13
15
|
function providerResult(overrides: Partial<GenerateResult> = {}): GenerateResult {
|
|
@@ -34,6 +36,65 @@ function deeplyNestedOutputSchema(depth: number): string {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
describe("QgridDispatcherClass", () => {
|
|
39
|
+
it.each(["generate", "stream"])("%s yields a completed tool-call message before a speculative later answer", async (mode) => {
|
|
40
|
+
const tool = JSON.stringify({result: {action: "tool_call", answer: null, toolCalls: [{toolName: "lookupMarker", args: "{}"}]}});
|
|
41
|
+
const answer = JSON.stringify({result: {action: "answer", answer: {marker: ""}, toolCalls: null}});
|
|
42
|
+
const provider = new OpenAIDispatcher("https", {clientFactory: () => ({
|
|
43
|
+
async *responses() {
|
|
44
|
+
yield normalizeOpenAIEvent({type: "response.output_item.added", item: {id: "preamble", type: "message", phase: "commentary", content: []}})!;
|
|
45
|
+
yield normalizeOpenAIEvent({type: "response.output_text.delta", item_id: "preamble", delta: "We need request tool structured."})!;
|
|
46
|
+
yield normalizeOpenAIEvent({type: "response.output_item.done", item: {id: "preamble", type: "message", phase: "commentary", content: [{type: "output_text", text: "We need request tool structured."}]}})!;
|
|
47
|
+
for (const [id, phase, text] of [["tool", "commentary", tool], ["answer", "final_answer", answer]]) {
|
|
48
|
+
yield normalizeOpenAIEvent({type: "response.output_item.added", item: {id, type: "message", role: "assistant", phase, content: []}})!;
|
|
49
|
+
yield normalizeOpenAIEvent({type: "response.output_text.delta", item_id: id, delta: text})!;
|
|
50
|
+
yield normalizeOpenAIEvent({type: "response.output_item.done", item: {id, type: "message", role: "assistant", phase, content: [{type: "output_text", text}]}})!;
|
|
51
|
+
}
|
|
52
|
+
yield normalizeOpenAIEvent({type: "response.completed", response: {id: "r", model: "gpt-6-luna", usage: {input_tokens: 10, output_tokens: 30, total_tokens: 40}}})!;
|
|
53
|
+
},
|
|
54
|
+
})});
|
|
55
|
+
await provider.onTokenAdded(1, "test", {accessToken: "unused", refreshToken: "", accountId: "unused", accessTokenExpiresAt: Date.now()+60000});
|
|
56
|
+
const dispatcher = new QgridDispatcherClass();
|
|
57
|
+
dispatcher.openaiDispatcher = provider;
|
|
58
|
+
const input = {model: "openai/gpt-6-luna", prompt: "lookup", tools: [{name: "lookupMarker", inputSchema: {type: "object"}}], jsonSchema: JSON.stringify({type: "object",properties: {marker: {type: "string"}}})};
|
|
59
|
+
const deltas: string[] = [];
|
|
60
|
+
let result: QueryOutput | undefined;
|
|
61
|
+
if (mode === "generate") result = await dispatcher.query(input);
|
|
62
|
+
else await dispatcher.queryStream(input, {onDelta: text => deltas.push(text), onComplete: value => {result = value;}, onError: error => {throw error;}});
|
|
63
|
+
expect(result?.finishReason).toBe("tool-calls");
|
|
64
|
+
expect(result?.content[0]).toMatchObject({type: "tool-call", toolName: "lookupMarker", input: "{}"});
|
|
65
|
+
expect(result?.usage.output_tokens).toBe(30); // Drain through completion; don't undercount discarded text.
|
|
66
|
+
if (mode === "stream") expect(deltas.join("")).toBe(tool);
|
|
67
|
+
await provider.stop();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("accepts only boolean thinking values on both query and prepareStream's shared input", () => {
|
|
71
|
+
for (const thinking of [true, false, undefined]) {
|
|
72
|
+
expect(QueryInput.parse({ prompt: "hi", thinking }).thinking).toBe(thinking);
|
|
73
|
+
}
|
|
74
|
+
expect(QueryInput.safeParse({ prompt: "hi", thinking: "false" }).success).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each(["openai/gpt-6-sol", "anthropic/claude-sonnet-4-6"])(
|
|
78
|
+
"forwards thinking on generate and stream, suppressing effort only for explicit off (%s)",
|
|
79
|
+
async (model) => {
|
|
80
|
+
const dispatcher = new QgridDispatcherClass();
|
|
81
|
+
const generate = vi.fn(async () => providerResult());
|
|
82
|
+
const generateStream = vi.fn(async (_req: GenerateRequest, cb: GenerateStreamCallbacks) => {
|
|
83
|
+
cb.onComplete(providerResult());
|
|
84
|
+
});
|
|
85
|
+
dispatcher.openaiDispatcher = { generate, generateStream } as never;
|
|
86
|
+
dispatcher.anthropicDispatcher = { generate, generateStream } as never;
|
|
87
|
+
for (const thinking of [false, true, undefined]) {
|
|
88
|
+
const input = { prompt: "hi", model, thinking, effort: "high" };
|
|
89
|
+
await dispatcher.query(input);
|
|
90
|
+
await dispatcher.queryStream(input, { onDelta: vi.fn(), onComplete: vi.fn(), onError: vi.fn() });
|
|
91
|
+
const expected = { thinking, effort: thinking === false ? undefined : "high" };
|
|
92
|
+
expect(generate).toHaveBeenLastCalledWith(expect.objectContaining(expected));
|
|
93
|
+
expect(generateStream).toHaveBeenLastCalledWith(expect.objectContaining(expected), expect.any(Object));
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
37
98
|
it("counts completed query and stream generations separately for providers sharing a token name", async () => {
|
|
38
99
|
const dispatcher = new QgridDispatcherClass();
|
|
39
100
|
dispatcher.replaceCache(
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* QgridDispatcher — provider dispatcher 라우팅 + 토큰 캐시/통계 싱글턴.
|
|
3
|
-
*
|
|
4
|
-
* - 메모리 캐시 (Map<id, TokenSubsetA>) 는 TokenSubscriber 가 pg LISTEN/NOTIFY 로 갱신
|
|
5
|
-
* - OpenAI/Anthropic 요청은 각 provider dispatcher 로만 실행
|
|
6
|
-
* - QuotaError 는 그대로 상위 전파
|
|
7
|
-
* - dispatcher 미준비는 기동 중(503)/초기화 실패(500)로 구분해 던진다
|
|
8
|
-
*/
|
|
1
|
+
// provider 라우팅과 토큰 캐시·통계. 캐시는 TokenSubscriber가 갱신한다.
|
|
9
2
|
|
|
10
3
|
import { InternalServerErrorException, ServiceUnavailableException } from "sonamu";
|
|
11
4
|
|
|
@@ -37,7 +30,7 @@ import {
|
|
|
37
30
|
} from "./qgrid.types";
|
|
38
31
|
import { composeSystemWithSchemaContract } from "./schema-prompt";
|
|
39
32
|
import { type TokenSubscriber } from "./token-subscriber";
|
|
40
|
-
import { applyToolCallEmulation } from "./tool-emulation";
|
|
33
|
+
import { applyToolCallEmulation, isToolCallEnvelope } from "./tool-emulation";
|
|
41
34
|
import { buildToolCallSchema } from "./tool-emulation-schema";
|
|
42
35
|
|
|
43
36
|
export type InternalQueryInput = QueryInput & {
|
|
@@ -50,55 +43,31 @@ export class QgridDispatcherClass {
|
|
|
50
43
|
|
|
51
44
|
// 프로세스 시작 이후 완료된 provider generation 수. 실패/중간 시도는 제외하며,
|
|
52
45
|
// 후속 tool turn도 한 generation으로 센다. 동명 토큰은 provider별로 분리한다.
|
|
53
|
-
|
|
46
|
+
requestCounts = new Map<string, number>();
|
|
54
47
|
|
|
55
|
-
// sonamu.config onStart 에서 처리하는 변수
|
|
56
48
|
subscriber: TokenSubscriber | null = null;
|
|
57
49
|
openaiDispatcher: OpenAIDispatcher | null = null;
|
|
58
50
|
anthropicDispatcher: AnthropicDispatcher | null = null;
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
* provider 별 기동 상태. dispatcher 가 없는 이유를 구분하기 위해 필요하다 —
|
|
62
|
-
* HTTP 리스닝은 dispatcher 준비보다 먼저 열리므로(sonamu.config onStart),
|
|
63
|
-
* 그 사이 들어온 요청은 "잠시 후 되는" 상태와 "재시도해도 안 되는" 상태가 다르다.
|
|
64
|
-
*
|
|
65
|
-
* - `starting`: provider 토큰·런타임 초기화 중 → 재시도 가능
|
|
66
|
-
* - `ready`: 정상
|
|
67
|
-
* - `failed`: start() 가 예외로 끝남 → 재시도해도 같은 결과
|
|
68
|
-
*/
|
|
52
|
+
// HTTP가 dispatcher보다 먼저 열리므로 초기화 중과 초기화 실패를 구분한다.
|
|
69
53
|
startupState: Record<"openai" | "anthropic", ProviderStartupState> = {
|
|
70
54
|
openai: "starting",
|
|
71
55
|
anthropic: "starting",
|
|
72
56
|
};
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
* 기동 중이면 503(+Retry-After) 으로 재시도 가능함을 알리고, 초기화가 실패로 끝났으면
|
|
78
|
-
* 500 으로 재시도가 무의미함을 알린다. 이전에는 둘 다 QuotaError 였는데, 쿼터 소진이
|
|
79
|
-
* 아닌 상태를 그렇게 표현하면 호출자가 토큰 문제로 오해하고 재시도 판단도 못 한다.
|
|
80
|
-
*/
|
|
81
|
-
private notReadyError(provider: "openai" | "anthropic"): Error {
|
|
58
|
+
// 초기화 중에는 503 + Retry-After, 초기화 실패에는 500을 반환한다.
|
|
59
|
+
notReadyError(provider: "openai" | "anthropic"): Error {
|
|
82
60
|
const label = provider === "openai" ? "OpenAI" : "Anthropic";
|
|
83
61
|
return this.startupState[provider] === "failed"
|
|
84
62
|
? new InternalServerErrorException(SD("qgrid.dispatcherFailed")(label))
|
|
85
63
|
: new ServiceUnavailableException(SD("qgrid.dispatcherStarting")(label));
|
|
86
64
|
}
|
|
87
65
|
|
|
88
|
-
|
|
66
|
+
recordCompleted(provider: string, tokenName: string): void {
|
|
89
67
|
const key = `${provider}\0${tokenName}`;
|
|
90
68
|
this.requestCounts.set(key, (this.requestCounts.get(key) ?? 0) + 1);
|
|
91
69
|
}
|
|
92
70
|
|
|
93
|
-
// TokenSubscriber 콜백 — 캐시 mutation
|
|
94
|
-
upsertCache(id: number, row: TokenSubsetA): void {
|
|
95
|
-
this.tokens.set(id, row);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
removeCache(id: number): void {
|
|
99
|
-
this.tokens.delete(id);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
71
|
replaceCache(rows: TokenSubsetA[]): void {
|
|
103
72
|
this.tokens = new Map(rows.map((r) => [r.id, r]));
|
|
104
73
|
}
|
|
@@ -115,10 +84,8 @@ export class QgridDispatcherClass {
|
|
|
115
84
|
async query(input: InternalQueryInput, abortSignal?: AbortSignal): Promise<QueryOutput> {
|
|
116
85
|
const route = parseProviderRoute(input.model);
|
|
117
86
|
const outputSchema = buildAndValidateStrictOutputSchema(input, route.provider);
|
|
118
|
-
// answer 인코딩 판정: jsonSchema 있으면 사용자 스키마 JSON, 없으면 평문 string. 4개 방출 지점이 공유.
|
|
119
87
|
const answerKind = input.jsonSchema ? ("json" as const) : ("text" as const);
|
|
120
88
|
|
|
121
|
-
// provider prefix routing: 'openai/gpt-5.4' → OpenAIDispatcher
|
|
122
89
|
if (route.provider === "openai") {
|
|
123
90
|
if (!this.openaiDispatcher) throw this.notReadyError("openai");
|
|
124
91
|
|
|
@@ -130,7 +97,11 @@ export class QgridDispatcherClass {
|
|
|
130
97
|
model: route.model,
|
|
131
98
|
systemPrompt: input.system,
|
|
132
99
|
outputSchema,
|
|
133
|
-
|
|
100
|
+
stopAfterOutputMessage: input.tools?.length
|
|
101
|
+
? (text, phase) => isToolCallEnvelope(text, input.tools!, answerKind, phase)
|
|
102
|
+
: undefined,
|
|
103
|
+
thinking: input.thinking,
|
|
104
|
+
effort: input.thinking === false ? undefined : input.effort,
|
|
134
105
|
verbosity: input.verbosity,
|
|
135
106
|
reasoningSummary: input.reasoningSummary,
|
|
136
107
|
serviceTier: input.serviceTier,
|
|
@@ -163,13 +134,10 @@ export class QgridDispatcherClass {
|
|
|
163
134
|
|
|
164
135
|
const decision = decideConvRouting(input);
|
|
165
136
|
const result = await this.anthropicDispatcher.generate({
|
|
166
|
-
// AnthropicDispatcher 내부에서 알아서 provider prefix 를 canonical model 로 정규화함
|
|
167
137
|
model: input.model,
|
|
168
|
-
// 스키마/envelope 계약은 --json-schema 대신 system 말미의 텍스트로 안내한다(SON-532).
|
|
169
|
-
// outputSchema 는 전달하지 않는다 — anthropic 에서 항상 undefined(U1)이며,
|
|
170
|
-
// CC 로 가는 --json-schema 채널 자체가 닫혀 있음을 여기서 명시한다.
|
|
171
138
|
systemPrompt: composeSystemWithSchemaContract(input.system, input),
|
|
172
|
-
|
|
139
|
+
thinking: input.thinking,
|
|
140
|
+
effort: input.thinking === false ? undefined : input.effort,
|
|
173
141
|
timeoutMs: input.timeout,
|
|
174
142
|
abortSignal,
|
|
175
143
|
coldInput: decision.coldInput,
|
|
@@ -196,7 +164,6 @@ export class QgridDispatcherClass {
|
|
|
196
164
|
): Promise<void> {
|
|
197
165
|
const route = parseProviderRoute(input.model);
|
|
198
166
|
const outputSchema = buildAndValidateStrictOutputSchema(input, route.provider);
|
|
199
|
-
// answer 인코딩 판정: jsonSchema 있으면 사용자 스키마 JSON, 없으면 평문 string. 4개 방출 지점이 공유.
|
|
200
167
|
const answerKind = input.jsonSchema ? ("json" as const) : ("text" as const);
|
|
201
168
|
|
|
202
169
|
if (route.provider === "openai") {
|
|
@@ -211,7 +178,11 @@ export class QgridDispatcherClass {
|
|
|
211
178
|
model: route.model,
|
|
212
179
|
systemPrompt: input.system,
|
|
213
180
|
outputSchema,
|
|
214
|
-
|
|
181
|
+
stopAfterOutputMessage: input.tools?.length
|
|
182
|
+
? (text, phase) => isToolCallEnvelope(text, input.tools!, answerKind, phase)
|
|
183
|
+
: undefined,
|
|
184
|
+
thinking: input.thinking,
|
|
185
|
+
effort: input.thinking === false ? undefined : input.effort,
|
|
215
186
|
verbosity: input.verbosity,
|
|
216
187
|
reasoningSummary: input.reasoningSummary,
|
|
217
188
|
serviceTier: input.serviceTier,
|
|
@@ -222,7 +193,6 @@ export class QgridDispatcherClass {
|
|
|
222
193
|
preferredTokenId: input.preferredTokenId ?? decision.preferredTokenId,
|
|
223
194
|
requirePreferredToken: input.requirePreferredToken,
|
|
224
195
|
abortSignal,
|
|
225
|
-
// 이미지 플래그를 전달해야 generateStream 의 non-stream 전용 거부(R2)가 발동한다.
|
|
226
196
|
imageGeneration: input.imageGeneration,
|
|
227
197
|
imageGenerationOptions: input.imageGenerationOptions,
|
|
228
198
|
},
|
|
@@ -250,9 +220,7 @@ export class QgridDispatcherClass {
|
|
|
250
220
|
} else if (route.provider === "anthropic") {
|
|
251
221
|
if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
|
|
252
222
|
|
|
253
|
-
//
|
|
254
|
-
// 비스트림 최종 텍스트는 stream-json-adapter 의 stripFences 가 같은 시맨틱으로
|
|
255
|
-
// 처리하므로 델타 연결과 done.text 가 어긋나지 않는다. 일반 텍스트 요청은 무변경.
|
|
223
|
+
// 구조화 응답은 최종 텍스트와 델타 모두 코드펜스를 제거한다.
|
|
256
224
|
const hasSchemaContract = input.jsonSchema !== undefined || Boolean(input.tools?.length);
|
|
257
225
|
const fenceStrip = hasSchemaContract ? createFenceStripTransform() : undefined;
|
|
258
226
|
const emitDelta = fenceStrip
|
|
@@ -266,9 +234,9 @@ export class QgridDispatcherClass {
|
|
|
266
234
|
await this.anthropicDispatcher.generateStream(
|
|
267
235
|
{
|
|
268
236
|
model: input.model,
|
|
269
|
-
// 스키마/envelope 계약은 --json-schema 대신 system 말미의 텍스트로 안내한다(SON-532)
|
|
270
237
|
systemPrompt: composeSystemWithSchemaContract(input.system, input),
|
|
271
|
-
|
|
238
|
+
thinking: input.thinking,
|
|
239
|
+
effort: input.thinking === false ? undefined : input.effort,
|
|
272
240
|
coldInput: decision.coldInput,
|
|
273
241
|
coldHistory: decision.coldHistory,
|
|
274
242
|
preferredTokenId: input.preferredTokenId,
|
|
@@ -282,8 +250,7 @@ export class QgridDispatcherClass {
|
|
|
282
250
|
onThreadId: cb.onThreadId,
|
|
283
251
|
onComplete: (turnResult) => {
|
|
284
252
|
this.recordCompleted("anthropic", turnResult.tokenName);
|
|
285
|
-
//
|
|
286
|
-
// 델타를 조립하는 클라이언트(EnvelopeStreamParser 등)의 무손실 보장.
|
|
253
|
+
// 닫는 펜스 판정 때문에 보류한 텍스트를 완료 전에 방출한다.
|
|
287
254
|
const rest = fenceStrip?.flush();
|
|
288
255
|
if (rest) cb.onDelta(rest);
|
|
289
256
|
|
|
@@ -315,19 +282,12 @@ function parseProviderRoute(model: string | undefined): { provider?: string; mod
|
|
|
315
282
|
return { provider, model: routedModel };
|
|
316
283
|
}
|
|
317
284
|
|
|
318
|
-
// qgrid provider dispatcher 에서 처리하지 않는 모델은 향후 직접 LLM API 호출 지점으로 보낸다.
|
|
319
285
|
function directLlmApiFallbackNotImplemented(input: QueryInput): ProcessError {
|
|
320
286
|
return new ProcessError(
|
|
321
287
|
`Direct LLM API fallback not implemented for model: ${input.model ?? "<default>"}`,
|
|
322
288
|
);
|
|
323
289
|
}
|
|
324
290
|
|
|
325
|
-
// 모든 provider(OpenAI/Anthropic)가 동일하게 strict output schema 를 쓴다.
|
|
326
|
-
//
|
|
327
|
-
// SON-495 교훈: 한때 Anthropic route 만 required 를 제거(optionalize)해 partial/rejected attempt 가
|
|
328
|
-
// 섞여도 통과하게 했으나, 실측 결과 그게 오히려 "모델이 비는 필드를 키째 생략"하도록 유도했다
|
|
329
|
-
// (required 살린 schema 는 누락 0, loose 는 누락 다발). strict 로 두면 모델이 빠짐없이 채우고,
|
|
330
|
-
// 드물게 어겨 provider retry 예산을 소진한 비정상 종료도 정직한 에러로 받아준다.
|
|
331
291
|
export function buildStrictOutputSchema(
|
|
332
292
|
input: Pick<QueryInput, "tools" | "jsonSchema">,
|
|
333
293
|
provider?: string,
|
|
@@ -343,11 +303,8 @@ export function buildAndValidateStrictOutputSchema(
|
|
|
343
303
|
input: Pick<QueryInput, "model" | "tools" | "jsonSchema">,
|
|
344
304
|
provider = parseProviderRoute(input.model).provider,
|
|
345
305
|
): JsonValue | undefined {
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// 4.7~9.1배 출력 청구 실측). 스키마/envelope 계약은 프롬프트 텍스트로 안내하고 판정은
|
|
349
|
-
// 소비자(zod)/parseEnvelope 가 맡는다. 따라서 strict 변환·argv 64KiB 제한 없이 수신
|
|
350
|
-
// 원형의 구문·복잡도(전역 512KiB 한도)만 검증해 caller-fault 를 조기에 돌려준다.
|
|
306
|
+
// Claude Code의 --json-schema는 불필요한 내부 재시도를 유발한다.
|
|
307
|
+
// Anthropic은 원본 스키마를 검증한 뒤 프롬프트로 전달하고, 응답 검증은 소비자에 맡긴다.
|
|
351
308
|
if (provider === "anthropic") {
|
|
352
309
|
parseAndValidateCallerSchemas(input);
|
|
353
310
|
return undefined;
|
|
@@ -368,10 +325,7 @@ function buildRawOutputSchema(
|
|
|
368
325
|
: callerOutputSchema;
|
|
369
326
|
}
|
|
370
327
|
|
|
371
|
-
// GenerateResult(provider dispatcher 응답)를 applyToolCallEmulation 입력 shape 로 매핑한다.
|
|
372
|
-
// OpenAI/Anthropic, query/queryStream 4 경로가 동일하게 쓴다.
|
|
373
328
|
// Anthropic adapter 는 cache creation 을 inputTokens 에 포함해 표준화하고, 별도 필드에도 보존한다.
|
|
374
|
-
// per-request cost 는 provider 가 준 값을 우선 사용하고, 없으면 공통 cost 함수를 쓴다.
|
|
375
329
|
function toEmulationResult(
|
|
376
330
|
result: GenerateResult,
|
|
377
331
|
): Omit<QueryOutput, "content" | "finishReason" | "runContext"> {
|
|
@@ -21,7 +21,6 @@ const {
|
|
|
21
21
|
dispatcherQueryStreamMock,
|
|
22
22
|
beforeQueryMock,
|
|
23
23
|
afterQueryMock,
|
|
24
|
-
assertNativeRunAdmissionMock,
|
|
25
24
|
finishRunWithErrorMock,
|
|
26
25
|
finishRunAbortedMock,
|
|
27
26
|
getRateLimitsByTokenIdMock,
|
|
@@ -45,7 +44,6 @@ const {
|
|
|
45
44
|
dispatcherQueryStreamMock: vi.fn(),
|
|
46
45
|
beforeQueryMock: vi.fn(),
|
|
47
46
|
afterQueryMock: vi.fn(),
|
|
48
|
-
assertNativeRunAdmissionMock: vi.fn(),
|
|
49
47
|
finishRunWithErrorMock: vi.fn(),
|
|
50
48
|
finishRunAbortedMock: vi.fn(),
|
|
51
49
|
getRateLimitsByTokenIdMock: vi.fn(),
|
|
@@ -95,7 +93,6 @@ vi.mock("./qgrid.dispatcher", async (importOriginal) => {
|
|
|
95
93
|
});
|
|
96
94
|
|
|
97
95
|
vi.mock("./qgrid-run-lifecycle", () => ({
|
|
98
|
-
assertNativeRunAdmission: assertNativeRunAdmissionMock,
|
|
99
96
|
beforeQuery: beforeQueryMock,
|
|
100
97
|
afterQuery: afterQueryMock,
|
|
101
98
|
finishRunWithError: finishRunWithErrorMock,
|
|
@@ -215,7 +212,6 @@ describe("QgridFrame.query request logging", () => {
|
|
|
215
212
|
beforeEach(() => {
|
|
216
213
|
beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 41, stepIndex: 0 });
|
|
217
214
|
afterQueryMock.mockReset().mockResolvedValue({});
|
|
218
|
-
assertNativeRunAdmissionMock.mockReset();
|
|
219
215
|
finishRunWithErrorMock.mockReset();
|
|
220
216
|
dispatcherQueryMock.mockReset();
|
|
221
217
|
findActiveByProviderAndNameMock.mockReset();
|
|
@@ -368,18 +364,6 @@ describe("QgridFrame.query request logging", () => {
|
|
|
368
364
|
expect(finishRunWithErrorMock).not.toHaveBeenCalled();
|
|
369
365
|
});
|
|
370
366
|
|
|
371
|
-
it("rejects logger-disabled native dispatch while restart admission is closed", async () => {
|
|
372
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
373
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await expect(
|
|
377
|
-
QgridFrame.query({ prompt: "hi", model: "openai/gpt-5-codex", logger: false }),
|
|
378
|
-
).rejects.toMatchObject({ statusCode: 503 });
|
|
379
|
-
expect(dispatcherQueryMock).not.toHaveBeenCalled();
|
|
380
|
-
expect(beforeQueryMock).not.toHaveBeenCalled();
|
|
381
|
-
});
|
|
382
|
-
|
|
383
367
|
it("merges a tool run id with the provider thread coordinate", async () => {
|
|
384
368
|
dispatcherQueryMock.mockResolvedValueOnce(queryOutput());
|
|
385
369
|
afterQueryMock.mockResolvedValueOnce({ runContext: { requestLogId: 41 } });
|
|
@@ -952,7 +936,6 @@ describe("QgridFrame.queryStream request logging", () => {
|
|
|
952
936
|
beforeEach(() => {
|
|
953
937
|
beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 52, stepIndex: 0 });
|
|
954
938
|
afterQueryMock.mockReset().mockResolvedValue({});
|
|
955
|
-
assertNativeRunAdmissionMock.mockReset();
|
|
956
939
|
finishRunWithErrorMock.mockReset();
|
|
957
940
|
finishRunAbortedMock.mockReset();
|
|
958
941
|
dispatcherQueryStreamMock.mockReset();
|
|
@@ -986,27 +969,6 @@ describe("QgridFrame.queryStream request logging", () => {
|
|
|
986
969
|
};
|
|
987
970
|
}
|
|
988
971
|
|
|
989
|
-
it("rejects stream preparation while restart admission is closed", async () => {
|
|
990
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
991
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
await expect(QgridFrame.prepareStream({ prompt: "hi" })).rejects.toMatchObject({
|
|
995
|
-
statusCode: 503,
|
|
996
|
-
});
|
|
997
|
-
expect(dispatcherQueryStreamMock).not.toHaveBeenCalled();
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it("rechecks admission before dispatching a previously prepared stream", async () => {
|
|
1001
|
-
const { streamId } = await QgridFrame.prepareStream({ prompt: "hi", logger: false });
|
|
1002
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
1003
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
await expect(QgridFrame.queryStream(streamId)).rejects.toMatchObject({ statusCode: 503 });
|
|
1007
|
-
expect(dispatcherQueryStreamMock).not.toHaveBeenCalled();
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
972
|
function installSseContext() {
|
|
1011
973
|
let closeHandler: (() => void) | undefined;
|
|
1012
974
|
const sse = {
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
RefreshFailedError,
|
|
39
39
|
} from "./oauth";
|
|
40
40
|
import {
|
|
41
|
-
assertNativeRunAdmission,
|
|
42
41
|
afterQuery,
|
|
43
42
|
beforeQuery,
|
|
44
43
|
finishRunAborted,
|
|
@@ -248,7 +247,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
248
247
|
|
|
249
248
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
250
249
|
async query(args: QueryInput): Promise<QueryOutput> {
|
|
251
|
-
assertNativeRunAdmission();
|
|
252
250
|
rejectInvalidCallerSchemas(args);
|
|
253
251
|
const resolvedArgs = await resolveTokenName(args);
|
|
254
252
|
const disconnect = createHttpDisconnectHandle();
|
|
@@ -299,7 +297,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
299
297
|
|
|
300
298
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
301
299
|
async prepareStream(args: QueryInput): Promise<{ streamId: string }> {
|
|
302
|
-
assertNativeRunAdmission();
|
|
303
300
|
rejectImageGenerationStream(args);
|
|
304
301
|
rejectInvalidCallerSchemas(args);
|
|
305
302
|
const resolvedArgs = await resolveTokenName(args);
|
|
@@ -315,7 +312,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
315
312
|
const args = pendingStreams.get(streamId);
|
|
316
313
|
pendingStreams.delete(streamId);
|
|
317
314
|
if (!args) throw new Error("invalid or expired streamId");
|
|
318
|
-
assertNativeRunAdmission();
|
|
319
315
|
rejectImageGenerationStream(args);
|
|
320
316
|
|
|
321
317
|
const ctx = Sonamu.getContext();
|
|
@@ -630,11 +626,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
630
626
|
}
|
|
631
627
|
|
|
632
628
|
// 교환 + 계정 중복 제거 + 저장 — redirect 콜백과 코드 붙여넣기 플로우가 공유한다.
|
|
633
|
-
|
|
634
|
-
code: string,
|
|
635
|
-
state: string,
|
|
636
|
-
pending: PendingOAuth,
|
|
637
|
-
): Promise<void> {
|
|
629
|
+
async completeAnthropicLogin(code: string, state: string, pending: PendingOAuth): Promise<void> {
|
|
638
630
|
const tokens = await exchangeCodeForTokens(
|
|
639
631
|
code,
|
|
640
632
|
pending.codeVerifier,
|
|
@@ -656,7 +648,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
656
648
|
notifyTokenAdded(pending.name, "anthropic");
|
|
657
649
|
}
|
|
658
650
|
|
|
659
|
-
|
|
651
|
+
async completeOpenAILogin(code: string, pending: PendingOAuth): Promise<void> {
|
|
660
652
|
const creds = await exchangeOpenAICode(code, pending.codeVerifier, pending.redirectUri);
|
|
661
653
|
await TokenModel.replaceByAccount("openai", creds.accountId, {
|
|
662
654
|
provider: "openai",
|
|
@@ -801,7 +793,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
801
793
|
}
|
|
802
794
|
}
|
|
803
795
|
|
|
804
|
-
|
|
796
|
+
async doRefreshToken(token: TokenSubsetA): Promise<string> {
|
|
805
797
|
const creds = token.credentials;
|
|
806
798
|
const rt = getRefreshToken(creds);
|
|
807
799
|
if (!rt) throw new Error("No refresh token");
|
|
@@ -810,7 +802,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
810
802
|
try {
|
|
811
803
|
refreshed = await refreshAccessToken(rt);
|
|
812
804
|
} catch (e) {
|
|
813
|
-
if (e instanceof RefreshFailedError && e.isAuthDead) {
|
|
805
|
+
if (e instanceof RefreshFailedError && e.isAuthDead()) {
|
|
814
806
|
await deactivateAuthDeadToken(token, `anthropic:${e.status}`);
|
|
815
807
|
}
|
|
816
808
|
throw e;
|
|
@@ -150,6 +150,8 @@ export const QueryInput = z.looseObject({
|
|
|
150
150
|
timeout: QueryTimeoutMs.optional(),
|
|
151
151
|
jsonSchema: z.string().optional(),
|
|
152
152
|
tools: z.array(QgridTool).optional(),
|
|
153
|
+
// Boolean literals keep Sonamu's HTTP caster from accepting "true"/"false" strings.
|
|
154
|
+
thinking: z.union([z.literal(true), z.literal(false)]).optional(),
|
|
153
155
|
effort: z.string().optional(),
|
|
154
156
|
verbosity: Verbosity.optional(),
|
|
155
157
|
reasoningSummary: ReasoningSummary.optional(),
|
|
@@ -32,8 +32,7 @@ function subscriberWith(openaiDispatcher: Record<string, unknown>) {
|
|
|
32
32
|
return new TokenSubscriber(
|
|
33
33
|
{} as never,
|
|
34
34
|
{
|
|
35
|
-
|
|
36
|
-
upsertCache: vi.fn(),
|
|
35
|
+
tokens: new Map(),
|
|
37
36
|
replaceCache: vi.fn(),
|
|
38
37
|
openaiDispatcher,
|
|
39
38
|
anthropicDispatcher: null,
|
|
@@ -82,8 +81,7 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
82
81
|
const subscriber = new TokenSubscriber(
|
|
83
82
|
{} as never,
|
|
84
83
|
{
|
|
85
|
-
|
|
86
|
-
upsertCache: vi.fn(),
|
|
84
|
+
tokens: new Map(),
|
|
87
85
|
replaceCache: vi.fn(),
|
|
88
86
|
openaiDispatcher: null,
|
|
89
87
|
anthropicDispatcher,
|
|
@@ -112,15 +110,13 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
112
110
|
{} as never,
|
|
113
111
|
{
|
|
114
112
|
tokens: new Map(),
|
|
115
|
-
removeCache: vi.fn(),
|
|
116
|
-
upsertCache: vi.fn(),
|
|
117
113
|
replaceCache: vi.fn(),
|
|
118
114
|
openaiDispatcher: null,
|
|
119
115
|
anthropicDispatcher,
|
|
120
116
|
} as never,
|
|
121
117
|
);
|
|
122
118
|
const onTokensChanged = vi.fn();
|
|
123
|
-
subscriber.
|
|
119
|
+
subscriber.tokenChangeHandler = onTokensChanged;
|
|
124
120
|
findOneMock.mockResolvedValueOnce({
|
|
125
121
|
...openaiToken(true),
|
|
126
122
|
provider: "anthropic",
|
|
@@ -143,15 +139,13 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
143
139
|
{} as never,
|
|
144
140
|
{
|
|
145
141
|
tokens: new Map([[previous.id, previous]]),
|
|
146
|
-
removeCache: vi.fn(),
|
|
147
|
-
upsertCache: vi.fn(),
|
|
148
142
|
replaceCache: vi.fn(),
|
|
149
143
|
openaiDispatcher: null,
|
|
150
144
|
anthropicDispatcher,
|
|
151
145
|
} as never,
|
|
152
146
|
);
|
|
153
147
|
const onTokensChanged = vi.fn();
|
|
154
|
-
subscriber.
|
|
148
|
+
subscriber.tokenChangeHandler = onTokensChanged;
|
|
155
149
|
findOneMock.mockResolvedValueOnce({
|
|
156
150
|
...previous,
|
|
157
151
|
credentials: { accessToken: "new", refreshToken: "refresh" },
|
|
@@ -173,15 +167,13 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
173
167
|
{} as never,
|
|
174
168
|
{
|
|
175
169
|
tokens: new Map([[previous.id, previous]]),
|
|
176
|
-
removeCache: vi.fn(),
|
|
177
|
-
upsertCache: vi.fn(),
|
|
178
170
|
replaceCache: vi.fn(),
|
|
179
171
|
openaiDispatcher: null,
|
|
180
172
|
anthropicDispatcher: { onTokenRemoved: vi.fn() },
|
|
181
173
|
} as never,
|
|
182
174
|
);
|
|
183
175
|
const onTokensChanged = vi.fn();
|
|
184
|
-
subscriber.
|
|
176
|
+
subscriber.tokenChangeHandler = onTokensChanged;
|
|
185
177
|
findOneMock.mockResolvedValueOnce({ ...previous, active: false });
|
|
186
178
|
|
|
187
179
|
await subscriber.handleNotification(JSON.stringify({ op: "UPDATE", id: previous.id }));
|
|
@@ -198,8 +190,7 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
198
190
|
const subscriber = new TokenSubscriber(
|
|
199
191
|
{} as never,
|
|
200
192
|
{
|
|
201
|
-
|
|
202
|
-
upsertCache: vi.fn(),
|
|
193
|
+
tokens: new Map(),
|
|
203
194
|
replaceCache: vi.fn(),
|
|
204
195
|
openaiDispatcher: null,
|
|
205
196
|
anthropicDispatcher,
|
|
@@ -243,8 +234,7 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
243
234
|
const subscriber = new TokenSubscriber(
|
|
244
235
|
{} as never,
|
|
245
236
|
{
|
|
246
|
-
|
|
247
|
-
upsertCache: vi.fn(),
|
|
237
|
+
tokens: new Map(),
|
|
248
238
|
replaceCache: vi.fn(),
|
|
249
239
|
openaiDispatcher,
|
|
250
240
|
anthropicDispatcher,
|
|
@@ -290,7 +280,7 @@ describe("TokenSubscriber OpenAI notifications", () => {
|
|
|
290
280
|
};
|
|
291
281
|
const subscriber = new TokenSubscriber({} as never, dispatcher as never);
|
|
292
282
|
const onTokensChanged = vi.fn();
|
|
293
|
-
subscriber.
|
|
283
|
+
subscriber.tokenChangeHandler = onTokensChanged;
|
|
294
284
|
findActiveMock
|
|
295
285
|
.mockResolvedValueOnce([currentAnthropic])
|
|
296
286
|
.mockResolvedValueOnce([
|