@cartanova/qgrid-cli 2.10.0 → 2.10.1
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/conv-routing.js +4 -14
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +10 -5
- package/bundle/dist/application/request-log/request-log.model.js +14 -12
- package/bundle/dist/application/request-log-step/request-log-step.model.js +7 -11
- package/bundle/dist/application/setting/setting.model.js +13 -27
- package/bundle/dist/application/token/token.model.js +52 -47
- package/bundle/dist/server-error-handler.js +1 -1
- package/bundle/dist/sonamu.config.js +3 -15
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +4 -2
- package/bundle/dist/utils/providers/common/model-cost.js +10 -3
- package/bundle/src/application/qgrid/conv-routing.test.ts +0 -32
- package/bundle/src/application/qgrid/conv-routing.ts +7 -26
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +65 -121
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +12 -6
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +10 -24
- package/bundle/src/application/request-log/request-log.model.test.ts +37 -2
- package/bundle/src/application/request-log/request-log.model.ts +14 -7
- package/bundle/src/application/request-log-step/request-log-step.model.ts +3 -15
- package/bundle/src/application/setting/setting.constant.test.ts +1 -13
- package/bundle/src/application/setting/setting.model.ts +10 -26
- package/bundle/src/application/setting/setting.persistence.test.ts +27 -0
- package/bundle/src/application/token/token.model.test.ts +86 -19
- package/bundle/src/application/token/token.model.ts +59 -44
- package/bundle/src/server-error-handler.ts +1 -2
- package/bundle/src/sonamu.config.ts +4 -31
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +6 -1
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +5 -2
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +1 -26
- package/bundle/src/utils/providers/common/model-cost.test.ts +22 -1
- package/bundle/src/utils/providers/common/model-cost.ts +22 -5
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +4 -11
- package/bundle/web-dist/client/assets/index-Cq4_ExLO.js +106 -0
- package/bundle/web-dist/client/assets/logs-kg1KOMBk.js +1 -0
- package/bundle/web-dist/client/assets/{monit-DrYZkisv.js → monit-DmtHTmSM.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-C8Y6olae.js → routes-CXotE1OF.js} +1 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +51 -0
- package/bundle/web-dist/client/assets/{settings-CdVqtDQ5.js → settings-CfJDz4AC.js} +1 -1
- package/bundle/web-dist/client/assets/show-gBpWK4z2.js +2 -0
- package/bundle/web-dist/client/assets/{tokens-CxXtdk9o.js → tokens-CbFJjIyC.js} +1 -1
- package/bundle/web-dist/client/assets/use-oauth-login-flow-jJo7GV3r.js +1 -0
- package/bundle/web-dist/client/assets/{use-update-token-mutation-Dkv_HG-C.js → use-update-token-mutation-D-Y8OySK.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{lazyRouteComponent-DyizoQKi.js → lazyRouteComponent-Bj1cPq6x.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-DO8zBdBR.js → logs-DMlOXcDS.js} +20 -12
- package/bundle/web-dist/server/assets/{monit-COH4MckI.js → monit-Dg1wZCdx.js} +3 -3
- package/bundle/web-dist/server/assets/{plus-C1D8z9o0.js → plus-DVobMrag.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-PfWvArTX.js → react-dom-BjR1Iaz8.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-Btja-741.js → routes-C7lB2Q85.js} +5 -5
- package/bundle/web-dist/server/assets/{services.generated-BsKOUSXl.js → services.generated-BM4X3Ecj.js} +102 -6
- package/bundle/web-dist/server/assets/{settings-DPcWGvy-.js → settings-BaIBt_Vs.js} +4 -4
- package/bundle/web-dist/server/assets/{show-CMXbb3TJ.js → show-CDxGr4rS.js} +9 -8
- package/bundle/web-dist/server/assets/{tokens-DAUmTrgO.js → tokens-DJlfeGcx.js} +5 -5
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-GsGwqefs.js → use-oauth-login-flow-DYKnG4A2.js} +32 -15
- package/bundle/web-dist/server/assets/{use-update-token-mutation-RujWERvj.js → use-update-token-mutation-DqGMopII.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +381 -21
- package/package.json +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +4 -2
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -2
- package/skills/qgrid/references/request-log-run-lifecycle.md +2 -0
- package/skills/qgrid/references/sonamu-api-web-flow.md +1 -1
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
- package/skills/qgrid/references/verification-and-debugging.md +24 -13
- package/bundle/dist/utils/providers/common/token-picker.js +0 -14
- package/bundle/src/utils/providers/common/token-picker.ts +0 -18
- package/bundle/web-dist/client/assets/index-BfwrIBHs.js +0 -106
- package/bundle/web-dist/client/assets/logs-Brit-LqB.js +0 -1
- package/bundle/web-dist/client/assets/services.generated-Bg2XkD-D.js +0 -51
- package/bundle/web-dist/client/assets/show-2B2X9MkO.js +0 -2
- package/bundle/web-dist/client/assets/use-oauth-login-flow-B9EqnwY9.js +0 -1
|
@@ -50,7 +50,7 @@ class TokenModelClass extends BaseModelClass<
|
|
|
50
50
|
|
|
51
51
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "Token" })
|
|
52
52
|
async findById<T extends TokenSubsetKey>(subset: T, id: number): Promise<TokenSubsetMapping[T]> {
|
|
53
|
-
const { rows } = await this.findMany(subset, { id, num: 1, page: 1 });
|
|
53
|
+
const { rows } = await this.findMany(subset, { id, num: 1, page: 1, queryMode: "list" });
|
|
54
54
|
if (!rows[0]) {
|
|
55
55
|
throw new NotFoundException(SD("error.entityNotFound")("Token", id));
|
|
56
56
|
}
|
|
@@ -61,7 +61,12 @@ class TokenModelClass extends BaseModelClass<
|
|
|
61
61
|
subset: T,
|
|
62
62
|
listParams: TokenListParams,
|
|
63
63
|
): Promise<TokenSubsetMapping[T] | null> {
|
|
64
|
-
const { rows } = await this.findMany(subset, {
|
|
64
|
+
const { rows } = await this.findMany(subset, {
|
|
65
|
+
...listParams,
|
|
66
|
+
num: 1,
|
|
67
|
+
page: 1,
|
|
68
|
+
queryMode: "list",
|
|
69
|
+
});
|
|
65
70
|
return rows[0] ?? null;
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -78,7 +83,7 @@ class TokenModelClass extends BaseModelClass<
|
|
|
78
83
|
...rawParams,
|
|
79
84
|
} satisfies TokenListParams;
|
|
80
85
|
|
|
81
|
-
const { qb
|
|
86
|
+
const { qb } = this.getSubsetQueries(subset);
|
|
82
87
|
|
|
83
88
|
if (params.id) {
|
|
84
89
|
qb.whereIn("tokens.id", asArray(params.id));
|
|
@@ -105,11 +110,7 @@ class TokenModelClass extends BaseModelClass<
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
A: (row) => ({ ...row }),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
return this.executeSubsetQuery({ subset, qb, params, enhancers, debug: false });
|
|
113
|
+
return this.executeSubsetQuery({ subset, qb, params });
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
async findByAccountIdentifier<T extends TokenSubsetKey>(
|
|
@@ -121,13 +122,10 @@ class TokenModelClass extends BaseModelClass<
|
|
|
121
122
|
qb.where("tokens.provider", provider);
|
|
122
123
|
const jsonKey = provider === "anthropic" ? "accountUuid" : "accountId";
|
|
123
124
|
qb.whereRaw(`tokens.credentials->>'${jsonKey}' = ?`, [accountId]);
|
|
124
|
-
const enhancers = this.createEnhancers({ A: (row) => ({ ...row }) });
|
|
125
125
|
const result = await this.executeSubsetQuery({
|
|
126
126
|
subset,
|
|
127
127
|
qb,
|
|
128
|
-
params: { num:
|
|
129
|
-
enhancers,
|
|
130
|
-
debug: false,
|
|
128
|
+
params: { num: 0, page: 1, queryMode: "list" },
|
|
131
129
|
});
|
|
132
130
|
return result.rows;
|
|
133
131
|
}
|
|
@@ -139,13 +137,10 @@ class TokenModelClass extends BaseModelClass<
|
|
|
139
137
|
const { qb } = this.getSubsetQueries(subset);
|
|
140
138
|
qb.where("tokens.active", true);
|
|
141
139
|
qb.where("tokens.provider", provider);
|
|
142
|
-
const enhancers = this.createEnhancers({ A: (row) => ({ ...row }) });
|
|
143
140
|
const result = await this.executeSubsetQuery({
|
|
144
141
|
subset,
|
|
145
142
|
qb,
|
|
146
|
-
params: { num:
|
|
147
|
-
enhancers,
|
|
148
|
-
debug: false,
|
|
143
|
+
params: { num: 0, page: 1, queryMode: "list" },
|
|
149
144
|
});
|
|
150
145
|
return result.rows;
|
|
151
146
|
}
|
|
@@ -159,13 +154,10 @@ class TokenModelClass extends BaseModelClass<
|
|
|
159
154
|
qb.where("tokens.active", true);
|
|
160
155
|
qb.where("tokens.provider", provider);
|
|
161
156
|
qb.where("tokens.name", name);
|
|
162
|
-
const enhancers = this.createEnhancers({ A: (row) => ({ ...row }) });
|
|
163
157
|
const result = await this.executeSubsetQuery({
|
|
164
158
|
subset,
|
|
165
159
|
qb,
|
|
166
|
-
params: { num: 1, page: 1 },
|
|
167
|
-
enhancers,
|
|
168
|
-
debug: false,
|
|
160
|
+
params: { num: 1, page: 1, queryMode: "list" },
|
|
169
161
|
});
|
|
170
162
|
return result.rows[0];
|
|
171
163
|
}
|
|
@@ -175,13 +167,10 @@ class TokenModelClass extends BaseModelClass<
|
|
|
175
167
|
const { qb } = this.getSubsetQueries(subset);
|
|
176
168
|
qb.where("tokens.reauth_required", true);
|
|
177
169
|
qb.orderBy("tokens.name", "asc");
|
|
178
|
-
const enhancers = this.createEnhancers({ A: (row) => ({ ...row }) });
|
|
179
170
|
const result = await this.executeSubsetQuery({
|
|
180
171
|
subset,
|
|
181
172
|
qb,
|
|
182
|
-
params: { num:
|
|
183
|
-
enhancers,
|
|
184
|
-
debug: false,
|
|
173
|
+
params: { num: 0, page: 1, queryMode: "list" },
|
|
185
174
|
});
|
|
186
175
|
return result.rows;
|
|
187
176
|
}
|
|
@@ -189,13 +178,10 @@ class TokenModelClass extends BaseModelClass<
|
|
|
189
178
|
async findActive<T extends TokenSubsetKey>(subset: T): Promise<TokenSubsetMapping[T][]> {
|
|
190
179
|
const { qb } = this.getSubsetQueries(subset);
|
|
191
180
|
qb.where("tokens.active", true);
|
|
192
|
-
const enhancers = this.createEnhancers({ A: (row) => ({ ...row }) });
|
|
193
181
|
const result = await this.executeSubsetQuery({
|
|
194
182
|
subset,
|
|
195
183
|
qb,
|
|
196
|
-
params: { num:
|
|
197
|
-
enhancers,
|
|
198
|
-
debug: false,
|
|
184
|
+
params: { num: 0, page: 1, queryMode: "list" },
|
|
199
185
|
});
|
|
200
186
|
return result.rows;
|
|
201
187
|
}
|
|
@@ -246,24 +232,53 @@ class TokenModelClass extends BaseModelClass<
|
|
|
246
232
|
accountId: string | undefined,
|
|
247
233
|
saveParams: TokenSaveParams,
|
|
248
234
|
): Promise<void> {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
235
|
+
await this.getPuri("w").transaction(async (trx) => {
|
|
236
|
+
let replacement = saveParams;
|
|
237
|
+
if (accountId) {
|
|
238
|
+
const jsonKey = provider === "anthropic" ? "accountUuid" : "accountId";
|
|
239
|
+
const oldEntries = await trx
|
|
240
|
+
.from("tokens")
|
|
241
|
+
.select({
|
|
242
|
+
id: "id",
|
|
243
|
+
quota_threshold: "quota_threshold",
|
|
244
|
+
weight: "weight",
|
|
245
|
+
ord: "ord",
|
|
246
|
+
keepalive_enabled: "keepalive_enabled",
|
|
247
|
+
})
|
|
248
|
+
.where("provider", provider)
|
|
249
|
+
.whereRaw(`credentials->>'${jsonKey}' = ?`, [accountId])
|
|
250
|
+
.orderBy("id", "asc")
|
|
251
|
+
.forUpdate();
|
|
252
|
+
const first = oldEntries[0];
|
|
253
|
+
if (first) {
|
|
254
|
+
replacement = {
|
|
255
|
+
...saveParams,
|
|
256
|
+
quota_threshold:
|
|
257
|
+
saveParams.quota_threshold === undefined
|
|
258
|
+
? first.quota_threshold
|
|
259
|
+
: saveParams.quota_threshold,
|
|
260
|
+
weight: saveParams.weight ?? first.weight,
|
|
261
|
+
ord: saveParams.ord ?? first.ord,
|
|
262
|
+
keepalive_enabled:
|
|
263
|
+
saveParams.keepalive_enabled ?? oldEntries.some((entry) => entry.keepalive_enabled),
|
|
264
|
+
};
|
|
265
|
+
await trx
|
|
266
|
+
.from("tokens")
|
|
267
|
+
.whereIn(
|
|
268
|
+
"id",
|
|
269
|
+
oldEntries.map((entry) => entry.id),
|
|
270
|
+
)
|
|
271
|
+
.delete();
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
logger.warn(
|
|
275
|
+
`${provider} login without account identifier: dedup skipped for ${saveParams.name ?? "unnamed"}`,
|
|
276
|
+
);
|
|
254
277
|
}
|
|
255
|
-
if (oldEntries.length > 0) await this.del(oldEntries.map((o) => o.id));
|
|
256
|
-
} else {
|
|
257
|
-
logger.warn(
|
|
258
|
-
`${provider} login without account identifier: dedup skipped for ${saveParams.name ?? "unnamed"}`,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: { ...saveParams, keepalive_enabled: keepaliveEnabled },
|
|
266
|
-
]);
|
|
279
|
+
trx.ubRegister("tokens", applyCreateDefaults(replacement));
|
|
280
|
+
await trx.ubUpsert("tokens");
|
|
281
|
+
});
|
|
267
282
|
}
|
|
268
283
|
|
|
269
284
|
/**
|
|
@@ -2,8 +2,7 @@ import { getLogger } from "@logtape/logtape";
|
|
|
2
2
|
import { type FastifyReply } from "fastify";
|
|
3
3
|
import { SoException } from "sonamu";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// 재시도가 같은 503 을 다시 받는다.
|
|
5
|
+
// provider 초기화 중 즉시 재요청이 몰리지 않도록 재시도 간격을 안내한다.
|
|
7
6
|
const RETRY_AFTER_SECONDS = "30";
|
|
8
7
|
|
|
9
8
|
// statusCode 를 신뢰하는 범위를 Sonamu 예외로 한정한다. qgrid 는 프록시라 upstream HTTP
|
|
@@ -130,37 +130,10 @@ export default defineConfig({
|
|
|
130
130
|
},
|
|
131
131
|
guardHandler: () => {},
|
|
132
132
|
cacheControlHandler: (req) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return CachePresets.immutable;
|
|
138
|
-
}
|
|
139
|
-
return CachePresets.longLived;
|
|
140
|
-
|
|
141
|
-
case "api":
|
|
142
|
-
// GET 요청만 캐싱 고려
|
|
143
|
-
if (req.method === "GET") {
|
|
144
|
-
// 특정 경로는 짧은 캐시
|
|
145
|
-
if (req.path.startsWith("/api/static-data")) {
|
|
146
|
-
return CachePresets.shortLived;
|
|
147
|
-
}
|
|
148
|
-
if (req.path.startsWith("/api/terms")) {
|
|
149
|
-
return CachePresets.mediumLived;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// 기본: 캐시 없음
|
|
153
|
-
return CachePresets.noCache;
|
|
154
|
-
|
|
155
|
-
case "ssr":
|
|
156
|
-
// SSR 페이지: 10초 캐시
|
|
157
|
-
return CachePresets.ssr;
|
|
158
|
-
|
|
159
|
-
case "csr":
|
|
160
|
-
// CSR fallback (index.html): 1분 캐시
|
|
161
|
-
return CachePresets.shortLived;
|
|
162
|
-
}
|
|
163
|
-
return CachePresets.noCache;
|
|
133
|
+
if (req.type === "api") return CachePresets.noCache;
|
|
134
|
+
if (req.type === "csr") return CachePresets.shortLived;
|
|
135
|
+
// Sonamu serves fingerprinted assets with its default immutable policy.
|
|
136
|
+
return undefined;
|
|
164
137
|
},
|
|
165
138
|
},
|
|
166
139
|
cache: {
|
|
@@ -36,6 +36,7 @@ describe("Anthropic 1M context policy", () => {
|
|
|
36
36
|
expect(supports1MContext("claude-opus-5-5")).toBe(true);
|
|
37
37
|
expect(supports1MContext("claude-sonnet-5")).toBe(true);
|
|
38
38
|
expect(supports1MContext("claude-sonnet-5-5")).toBe(true);
|
|
39
|
+
expect(supports1MContext("claude-haiku-5-5")).toBe(true);
|
|
39
40
|
expect(supports1MContext("claude-sonnet-4-6")).toBe(true);
|
|
40
41
|
expect(supports1MContext("claude-opus-4-6")).toBe(true);
|
|
41
42
|
expect(supports1MContext("claude-opus-4-8")).toBe(true);
|
|
@@ -53,6 +54,7 @@ describe("Anthropic 1M context policy", () => {
|
|
|
53
54
|
expect(needsCli1mSuffix("claude-opus-5-5")).toBe(false);
|
|
54
55
|
expect(needsCli1mSuffix("claude-sonnet-5")).toBe(false);
|
|
55
56
|
expect(needsCli1mSuffix("claude-sonnet-5-5")).toBe(false);
|
|
57
|
+
expect(needsCli1mSuffix("claude-haiku-5-5")).toBe(false);
|
|
56
58
|
expect(needsCli1mSuffix("claude-sonnet-4-6")).toBe(true);
|
|
57
59
|
expect(needsCli1mSuffix("claude-opus-4-6")).toBe(true);
|
|
58
60
|
expect(needsCli1mSuffix("claude-opus-4-8")).toBe(false);
|
|
@@ -82,7 +84,7 @@ describe("Anthropic 1M context policy", () => {
|
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
describe("Anthropic thinking policy", () => {
|
|
85
|
-
it("Fable 5/5.1, Opus 5, Opus 5.5, Sonnet 5.5 는 adaptive thinking 을 보존한다", () => {
|
|
87
|
+
it("Fable 5/5.1, Opus 5, Opus 5.5, Sonnet 5.5, Haiku 5.5 는 adaptive thinking 을 보존한다", () => {
|
|
86
88
|
expect(usesAdaptiveThinking("claude-fable-5-1")).toBe(true);
|
|
87
89
|
expect(usesAdaptiveThinking("anthropic/claude-fable-5-1")).toBe(true);
|
|
88
90
|
expect(usesAdaptiveThinking("claude-fable-5")).toBe(true);
|
|
@@ -93,6 +95,9 @@ describe("Anthropic thinking policy", () => {
|
|
|
93
95
|
expect(usesAdaptiveThinking("anthropic/claude-opus-5-5[1m]")).toBe(true);
|
|
94
96
|
expect(usesAdaptiveThinking("claude-sonnet-5-5")).toBe(true);
|
|
95
97
|
expect(usesAdaptiveThinking("anthropic/claude-sonnet-5-5[1m]")).toBe(true);
|
|
98
|
+
expect(usesAdaptiveThinking("claude-haiku-5-5")).toBe(true);
|
|
99
|
+
expect(usesAdaptiveThinking("anthropic/claude-haiku-5-5")).toBe(true);
|
|
100
|
+
expect(usesAdaptiveThinking("claude-haiku-4-5")).toBe(false);
|
|
96
101
|
expect(usesAdaptiveThinking("claude-sonnet-5")).toBe(false);
|
|
97
102
|
expect(usesAdaptiveThinking("claude-opus-4-8")).toBe(false);
|
|
98
103
|
});
|
|
@@ -51,14 +51,16 @@ const ONE_MILLION_CONTEXT_MODELS = new Set([
|
|
|
51
51
|
"claude-opus-5-5",
|
|
52
52
|
"claude-sonnet-5-5",
|
|
53
53
|
"claude-sonnet-5",
|
|
54
|
+
"claude-haiku-5-5",
|
|
54
55
|
"claude-sonnet-4-6",
|
|
55
56
|
"claude-opus-4-6",
|
|
56
57
|
"claude-opus-4-8",
|
|
57
58
|
]);
|
|
58
59
|
|
|
59
60
|
const CLI_ONE_MILLION_SUFFIX_MODELS = new Set(["claude-sonnet-4-6", "claude-opus-4-6"]);
|
|
60
|
-
// Fable 5/5.1, Opus 5.5, Sonnet 5.5 는 thinking disabled 를 거부한다(Fable: CLI 카탈로그 `rejects_disabled_thinking`,
|
|
61
|
-
// Opus 5.5: 공식 사양 "can't be turned off", 2026-09-22 출시. Sonnet 5.5: API `{type: "disabled"}` 400, CLI 카탈로그 `rejects_disabled_thinking`, 2026-09-
|
|
61
|
+
// Fable 5/5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5 는 thinking disabled 를 거부한다(Fable: CLI 카탈로그 `rejects_disabled_thinking`,
|
|
62
|
+
// Opus 5.5: 공식 사양 "can't be turned off", 2026-09-22 출시. Sonnet 5.5: API `{type: "disabled"}` 400, CLI 카탈로그 `rejects_disabled_thinking`, 2026-09-28 출시.
|
|
63
|
+
// Haiku 5.5: API 는 high 이하에서만 disabled 허용·xhigh/max 400, Claude Code 2.1.293 카탈로그는 `rejects_disabled_thinking`, 2026-10-07 출시). Opus 5 는
|
|
62
64
|
// disabled 를 high 이하에서 지원하지만, 공식 기본값·권장 동작인 adaptive thinking 을 유지한다. qgrid
|
|
63
65
|
// 기본 effort=low 가 비용/지연 제어를 맡으며, 이 정책은 disabled+xhigh/max 400 오류와 disabled 시
|
|
64
66
|
// tool/XML 출력 오염도 피한다.
|
|
@@ -68,6 +70,7 @@ const ADAPTIVE_THINKING_MODELS = new Set([
|
|
|
68
70
|
"claude-opus-5",
|
|
69
71
|
"claude-opus-5-5",
|
|
70
72
|
"claude-sonnet-5-5",
|
|
73
|
+
"claude-haiku-5-5",
|
|
71
74
|
]);
|
|
72
75
|
|
|
73
76
|
// Fable 계열: 구독 플랜에서 usage credits 가 필요한 최상위 티어. 구독 usage API 의
|
|
@@ -153,24 +153,6 @@ describe("AnthropicDispatcher", () => {
|
|
|
153
153
|
expect(call.input).toEqual([{ type: "text", text: "hi", text_elements: [] }]);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
it("reuse/reuseInput 이 실려 와도 무시하고 coldInput/coldHistory 로 실행한다", async () => {
|
|
157
|
-
const d = new AnthropicDispatcher();
|
|
158
|
-
d.onTokenAdded(1, "tok-A", creds(), null, 1);
|
|
159
|
-
|
|
160
|
-
await d.generate(
|
|
161
|
-
baseReq({
|
|
162
|
-
coldHistory: [{ type: "message", role: "assistant", content: [] }],
|
|
163
|
-
reuse: { workerId: 1, threadId: "S1", epoch: 0 },
|
|
164
|
-
reuseInput: [{ type: "text", text: "delta", text_elements: [] }],
|
|
165
|
-
}),
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const call = firstRunRequest();
|
|
169
|
-
expect(call).not.toHaveProperty("resumeSessionId");
|
|
170
|
-
expect(call.coldHistory).toBeDefined();
|
|
171
|
-
expect(call.input).toEqual([{ type: "text", text: "hi", text_elements: [] }]);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
156
|
it("structured(outputSchema): jsonSchema 로 직렬화되어 전달", async () => {
|
|
175
157
|
const d = new AnthropicDispatcher();
|
|
176
158
|
d.onTokenAdded(1, "tok-A", creds(), null, 1);
|
|
@@ -245,14 +227,7 @@ describe("AnthropicDispatcher", () => {
|
|
|
245
227
|
d.onTokenAdded(1, "tok-A", creds(), null, 1);
|
|
246
228
|
runClaudeSessionMock.mockRejectedValueOnce(new Error("closed without result"));
|
|
247
229
|
|
|
248
|
-
await expect(
|
|
249
|
-
d.generate(
|
|
250
|
-
baseReq({
|
|
251
|
-
reuse: { workerId: 1, threadId: "S1", epoch: 0 },
|
|
252
|
-
reuseInput: [{ type: "text", text: "delta", text_elements: [] }],
|
|
253
|
-
}),
|
|
254
|
-
),
|
|
255
|
-
).rejects.toThrow("closed without result");
|
|
230
|
+
await expect(d.generate(baseReq())).rejects.toThrow("closed without result");
|
|
256
231
|
expect(runClaudeSessionMock).toHaveBeenCalledTimes(1);
|
|
257
232
|
});
|
|
258
233
|
|
|
@@ -11,6 +11,25 @@ describe("calculateCostUsd", () => {
|
|
|
11
11
|
calculateCostUsd("openai/gpt-6-astra", { inputTokens, outputTokens: 1_000 }),
|
|
12
12
|
).toBeCloseTo(expected, 10);
|
|
13
13
|
});
|
|
14
|
+
it.each([
|
|
15
|
+
[100_000, 0.0105],
|
|
16
|
+
[100_001, 0.0525005],
|
|
17
|
+
])("Haiku 5.5 applies the over-100K prompt tier to the whole request (%i input)", (inputTokens, expected) => {
|
|
18
|
+
expect(
|
|
19
|
+
calculateCostUsd("anthropic/claude-haiku-5-5", { inputTokens, outputTokens: 1_000 }),
|
|
20
|
+
).toBeCloseTo(expected, 10);
|
|
21
|
+
});
|
|
22
|
+
it("Haiku 5.5 over-100K tier multiplies cache read and cache write rates by 5x", () => {
|
|
23
|
+
// 50K uncached × $0.50 + 1K output × $2.50 + 200K cache read × $0.05 + 50K 1h cache write × $1.
|
|
24
|
+
expect(
|
|
25
|
+
calculateCostUsd("anthropic/claude-haiku-5-5", {
|
|
26
|
+
inputTokens: 300_000,
|
|
27
|
+
outputTokens: 1_000,
|
|
28
|
+
cachedInputTokens: 200_000,
|
|
29
|
+
cacheCreationInputTokens: 50_000,
|
|
30
|
+
}),
|
|
31
|
+
).toBeCloseTo(0.0875, 10);
|
|
32
|
+
});
|
|
14
33
|
it.each([
|
|
15
34
|
["gpt-6-astra", 10, 50, 1, 12.5],
|
|
16
35
|
["gpt-6.1-sol", 2, 10, 0.1, 2.5],
|
|
@@ -60,7 +79,9 @@ describe("calculateCostUsd", () => {
|
|
|
60
79
|
// Opus 5.5 는 cache read 만 0.05x 특례($0.20), cache write 배율은 표준(5m $5 / 1h $8).
|
|
61
80
|
["claude-opus-5-5", 4, 20, 0.2, 8],
|
|
62
81
|
["claude-sonnet-5", 2, 10, 0.2, 4],
|
|
63
|
-
|
|
82
|
+
// Sonnet 5.5 는 2026-10-07 부터 cache read 0.05x($0.10), cache write 배율은 표준(5m $2.50 / 1h $4).
|
|
83
|
+
["claude-sonnet-5-5", 2, 10, 0.1, 4],
|
|
84
|
+
["claude-haiku-5-5", 0.1, 0.5, 0.01, 0.2],
|
|
64
85
|
])(
|
|
65
86
|
"%s official Anthropic rates for 5m/1h cache writes",
|
|
66
87
|
(model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens) => {
|
|
@@ -157,7 +157,7 @@ const OPENAI_COSTS: Record<string, OpenAIModelSpec> = {
|
|
|
157
157
|
function anthropicCosts(
|
|
158
158
|
inputTokens: number,
|
|
159
159
|
outputTokens: number,
|
|
160
|
-
overrides: Pick<Partial<ModelCosts>, "cachedInputTokens"> = {},
|
|
160
|
+
overrides: Pick<Partial<ModelCosts>, "cachedInputTokens" | "longContext"> = {},
|
|
161
161
|
): ModelCosts {
|
|
162
162
|
return {
|
|
163
163
|
inputTokens,
|
|
@@ -169,10 +169,13 @@ function anthropicCosts(
|
|
|
169
169
|
cacheCreationInputTokens: inputTokens * 2,
|
|
170
170
|
cacheCreationInputTokens5m: inputTokens * 1.25,
|
|
171
171
|
cacheCreationInputTokens1h: inputTokens * 2,
|
|
172
|
+
// Haiku 5.5 처럼 프롬프트 길이 티어가 있는 모델만 long-context 배율을 가진다.
|
|
173
|
+
...(overrides.longContext ? { longContext: overrides.longContext } : {}),
|
|
172
174
|
};
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
// 가격 출처: https://platform.claude.com/docs/en/about-claude/pricing (2026-
|
|
177
|
+
// 가격 출처: https://platform.claude.com/docs/en/about-claude/pricing (2026-10-08 확인. 2026-10-07 에 Sonnet 5.5
|
|
178
|
+
// cache read 가 $0.20→$0.10(0.05x)으로 인하됐고, 그 외 기존 모델 단가는 변동 없음)
|
|
176
179
|
const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
|
|
177
180
|
// Fable 5.1 (2026-09-01 출시): input/output 은 Fable 5 와 같고 cache read 만 0.025x($0.25) 특례.
|
|
178
181
|
// @see https://platform.claude.com/docs/en/models/fable-5-1/overview
|
|
@@ -200,9 +203,23 @@ const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
|
|
|
200
203
|
// Sonnet 5 의 introductory $2/$10 이 정식 단가로 확정됐다. 2026-09-01 에 예정됐던 $3/$15 인상은
|
|
201
204
|
// 취소됐으므로 날짜 분기 없이 고정 단가로 계산한다.
|
|
202
205
|
"claude-sonnet-5": anthropicCosts(2, 10),
|
|
203
|
-
// Sonnet 5.5 (2026-09-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
+
// Sonnet 5.5 (2026-09-28 출시): Sonnet 5 와 같은 $2/$10. cache read 는 출시 시 표준 0.1x($0.20)였다가
|
|
207
|
+
// 2026-10-07 Haiku 5.5 출시와 함께 Opus 5.5 와 같은 0.05x($0.10)로 인하됐다(API 릴리스 노트). 비용은 요청 시점
|
|
208
|
+
// 단가로 기록되므로 날짜 분기 없이 현행 단가만 둔다. cache write 는 표준 배율($2.50/$4).
|
|
209
|
+
// @see https://platform.claude.com/docs/en/release-notes/overview (2026-10-07)
|
|
210
|
+
"claude-sonnet-5-5": anthropicCosts(2, 10, { cachedInputTokens: 0.1 }),
|
|
211
|
+
// Haiku 5.5 (2026-10-07 출시): 프롬프트 길이 티어 단가. 100K 이하 $0.10/$0.50, cache read 0.1x($0.01),
|
|
212
|
+
// cache write 5m $0.125 / 1h $0.20. 입력이 100K 를 넘으면 input/cache read/cache write/output 모두 5x
|
|
213
|
+
// ($0.50/$0.05/$0.625·$1/$2.50)가 요청 전체에 적용된다. 1M 컨텍스트 모델 중 유일하게 길이별 단가를 쓴다.
|
|
214
|
+
// @see https://platform.claude.com/docs/en/models/haiku-5-5/overview
|
|
215
|
+
"claude-haiku-5-5": anthropicCosts(0.1, 0.5, {
|
|
216
|
+
longContext: {
|
|
217
|
+
threshold: 100_000,
|
|
218
|
+
inputMultiplier: 5,
|
|
219
|
+
cachedInputMultiplier: 5,
|
|
220
|
+
outputMultiplier: 5,
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
206
223
|
};
|
|
207
224
|
|
|
208
225
|
// gpt-5.3-codex-spark 는 research preview 로 공식 token 단가가 아직 final 이 아니다.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ProviderDispatcher — provider 별 LLM 요청 실행 인터페이스.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provider 실행 결과와 stream callback의 공통 계약.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
@@ -18,8 +18,7 @@ export type ProviderTokenUsageBreakdown = TokenUsageBreakdown & {
|
|
|
18
18
|
cacheCreationInputTokens1h?: number;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// thread
|
|
22
|
-
// dispatcher 는 이 좌표가 가리키는 worker 의 기존 thread 에 turn 만 실행한다.
|
|
21
|
+
// 공개 thread 좌표의 provider 결과 부분. 이름은 호환성을 위해 유지한다.
|
|
23
22
|
export interface ReuseThreadCoord {
|
|
24
23
|
workerId: number;
|
|
25
24
|
threadId: string;
|
|
@@ -61,9 +60,8 @@ export interface GenerateRequest {
|
|
|
61
60
|
// provider 실행 제한(ms). Queue selection 이후의 active provider request 전체에 적용한다.
|
|
62
61
|
timeoutMs?: number;
|
|
63
62
|
abortSignal?: AbortSignal;
|
|
64
|
-
//
|
|
63
|
+
// 매 turn에 전달하는 현재 input과 전체 history.
|
|
65
64
|
coldInput: Array<UserInput>;
|
|
66
|
-
// 첫 turn / 폴백 시 inject 할 전체 history.
|
|
67
65
|
coldHistory?: Array<JsonValue>;
|
|
68
66
|
// Provider-neutral cache affinity hints. Direct OpenAI currently receives these while still
|
|
69
67
|
// replaying coldInput + coldHistory on every turn; a dispatcher may use them for token choice
|
|
@@ -72,13 +70,8 @@ export interface GenerateRequest {
|
|
|
72
70
|
preferredTokenId?: number;
|
|
73
71
|
// 사람이 지정한 exact target. Cache affinity 선호와 달리 부적격 시 다른 토큰으로 대체하지 않는다.
|
|
74
72
|
requirePreferredToken?: boolean;
|
|
75
|
-
// 재사용 좌표 + delta input. 둘은 한 쌍 — 검증 통과 시에만 설정한다.
|
|
76
|
-
// dispatcher 가 worker/thread 생존을 재검증해 성공하면 reuseInput(delta)을, 실패하면
|
|
77
|
-
// coldInput + coldHistory 로 폴백한다(전체 history 로 문맥 복구).
|
|
78
|
-
reuse?: ReuseThreadCoord;
|
|
79
|
-
reuseInput?: Array<UserInput>;
|
|
80
73
|
// OpenAI image_generation tool 을 켠다(OpenAI 경로 전용, opt-in).
|
|
81
|
-
// 이 플래그가 있으면
|
|
74
|
+
// 이 플래그가 있으면 OpenAI prompt-cache affinity 라우팅을 건너뛴다.
|
|
82
75
|
imageGeneration?: boolean | "auto";
|
|
83
76
|
imageGenerationOptions?: ImageGenerationOptions;
|
|
84
77
|
}
|