@cartanova/qgrid-cli 2.4.7 → 2.4.8

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.
Files changed (73) hide show
  1. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +15 -1
  2. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -4
  3. package/bundle/dist/application/qgrid/qgrid.frame.js +20 -1
  4. package/bundle/dist/application/qgrid/qgrid.types.js +38 -2
  5. package/bundle/dist/application/request-log/request-log.model.js +49 -22
  6. package/bundle/dist/application/request-log-step/request-log-step.types.js +8 -1
  7. package/bundle/dist/application/sonamu.generated.js +37 -1
  8. package/bundle/dist/application/sonamu.generated.sso.js +25 -1
  9. package/bundle/dist/i18n/sd.generated.js +13 -1
  10. package/bundle/dist/migrations/20260718180824_alter_request_logs_add5_alter1.js +20 -0
  11. package/bundle/dist/migrations/20260718180825_alter_request_log_steps_add7_alter1.js +22 -0
  12. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +8 -2
  13. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +6 -3
  14. package/bundle/dist/utils/providers/anthropic/claude-session.js +11 -7
  15. package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +117 -7
  16. package/bundle/dist/utils/providers/common/model-cost.js +45 -98
  17. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +68 -0
  18. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +14 -0
  19. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +81 -0
  20. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -9
  21. package/bundle/src/application/qgrid/qgrid.frame.ts +19 -0
  22. package/bundle/src/application/qgrid/qgrid.types.ts +36 -0
  23. package/bundle/src/application/qgrid/tool-emulation.test.ts +1 -0
  24. package/bundle/src/application/request-log/request-log.entity.json +42 -0
  25. package/bundle/src/application/request-log/request-log.model.test.ts +58 -0
  26. package/bundle/src/application/request-log/request-log.model.ts +107 -18
  27. package/bundle/src/application/request-log-step/request-log-step.entity.json +54 -0
  28. package/bundle/src/application/request-log-step/request-log-step.types.ts +7 -0
  29. package/bundle/src/application/sonamu.generated.http +26 -0
  30. package/bundle/src/application/sonamu.generated.sso.ts +24 -0
  31. package/bundle/src/application/sonamu.generated.ts +48 -0
  32. package/bundle/src/i18n/sd.generated.ts +12 -0
  33. package/bundle/src/migrations/20260718180824_alter_request_logs_add5_alter1.ts +25 -0
  34. package/bundle/src/migrations/20260718180825_alter_request_log_steps_add7_alter1.ts +29 -0
  35. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +15 -0
  36. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +11 -2
  37. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +7 -2
  38. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +26 -0
  39. package/bundle/src/utils/providers/anthropic/claude-session.ts +16 -4
  40. package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +131 -0
  41. package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +194 -11
  42. package/bundle/src/utils/providers/common/model-cost.test.ts +136 -2
  43. package/bundle/src/utils/providers/common/model-cost.ts +76 -99
  44. package/bundle/src/utils/providers/common/provider-dispatcher.ts +18 -0
  45. package/bundle/web-dist/client/assets/{index-CYnw2kNG.js → index-CmuPjbCl.js} +3 -3
  46. package/bundle/web-dist/client/assets/{logs-CnXIN-jp.js → logs-cXOsDli3.js} +1 -1
  47. package/bundle/web-dist/client/assets/{monit-DRuDGJj4.js → monit-B5Edhvy-.js} +1 -1
  48. package/bundle/web-dist/client/assets/{routes-CuyNd-ya.js → routes-UPWScORY.js} +1 -1
  49. package/bundle/web-dist/client/assets/{sd.generated-D2Y9QeiN.js → sd.generated-DYuKzBY6.js} +1 -1
  50. package/bundle/web-dist/client/assets/{services.generated-CmxXl220.js → services.generated-D_SkWOo-.js} +1 -1
  51. package/bundle/web-dist/client/assets/show-pRp-eZIK.js +30 -0
  52. package/bundle/web-dist/client/assets/{tokens-DxFeKC8Q.js → tokens-DJlttxCM.js} +1 -1
  53. package/bundle/web-dist/client/assets/{use-update-token-mutation-6J1T9U2-.js → use-update-token-mutation-CpFGLWUl.js} +1 -1
  54. package/bundle/web-dist/client/index.html +2 -2
  55. package/bundle/web-dist/server/assets/{lazyRouteComponent-BlfOh9S3.js → lazyRouteComponent-Cu6IkL8A.js} +1 -1
  56. package/bundle/web-dist/server/assets/{logs-DYly4ipQ.js → logs-Dx2knN2T.js} +10 -7
  57. package/bundle/web-dist/server/assets/{monit-CYyY2S2f.js → monit-DU6PLatf.js} +2 -2
  58. package/bundle/web-dist/server/assets/{routes-Dqu3osXt.js → routes-CTco1oJZ.js} +3 -3
  59. package/bundle/web-dist/server/assets/{sd.generated-CE6D5clm.js → sd.generated-qzuRe35X.js} +12 -0
  60. package/bundle/web-dist/server/assets/{services.generated-DyZUzXiq.js → services.generated-C8q1M41a.js} +1 -1
  61. package/bundle/web-dist/server/assets/{show-BDul8xLb.js → show-B3q8_GDh.js} +47 -4
  62. package/bundle/web-dist/server/assets/{tokens-fzMW4G74.js → tokens-Bvjn15h9.js} +3 -3
  63. package/bundle/web-dist/server/assets/{use-update-token-mutation-BejUW5sw.js → use-update-token-mutation-BlZo6swF.js} +1 -1
  64. package/bundle/web-dist/server/entry-server.generated.js +8 -8
  65. package/package.json +1 -1
  66. package/skills/qgrid/SKILL.md +4 -0
  67. package/skills/qgrid/references/ai-sdk-provider-contract.md +30 -2
  68. package/skills/qgrid/references/anthropic-claude-code-runtime.md +43 -5
  69. package/skills/qgrid/references/decision-rationale.md +2 -0
  70. package/skills/qgrid/references/prompt-cache-and-usage.md +4 -1
  71. package/skills/qgrid/references/request-log-run-lifecycle.md +6 -2
  72. package/skills/qgrid/references/sonamu-api-web-flow.md +13 -0
  73. package/bundle/web-dist/client/assets/show-EleAOzJv.js +0 -30
@@ -5,12 +5,12 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/sonamu.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Qgrid</title>
8
- <script type="module" crossorigin src="/assets/index-CYnw2kNG.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CmuPjbCl.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dw2cE7zH.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-ca-nBW7U.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DH4kUfK2.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-DJRQAbrY.js">
13
- <link rel="modulepreload" crossorigin href="/assets/sd.generated-D2Y9QeiN.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/sd.generated-DYuKzBY6.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-yZcjDoV0.css">
15
15
  </head>
16
16
  <body>
@@ -1,4 +1,4 @@
1
- import { J as __commonJSMin, Z as __toESM, _ as require_jsx_runtime, m as require_shim, p as require_react_dom, v as require_react } from "./sd.generated-CE6D5clm.js";
1
+ import { J as __commonJSMin, Z as __toESM, _ as require_jsx_runtime, m as require_shim, p as require_react_dom, v as require_react } from "./sd.generated-qzuRe35X.js";
2
2
  //#region ../../node_modules/.pnpm/@tanstack+router-core@1.143.6/node_modules/@tanstack/router-core/dist/esm/utils.js
3
3
  function last(arr) {
4
4
  return arr[arr.length - 1];
@@ -1,6 +1,6 @@
1
- import { _ as require_jsx_runtime, d as string, l as number, u as object, v as require_react } from "./sd.generated-CE6D5clm.js";
2
- import { a as TokenService, n as QgridService, r as RequestLogService } from "./services.generated-DyZUzXiq.js";
3
- import { a as Link, o as useNavigate } from "./lazyRouteComponent-BlfOh9S3.js";
1
+ import { _ as require_jsx_runtime, d as string, l as number, u as object, v as require_react } from "./sd.generated-qzuRe35X.js";
2
+ import { a as TokenService, n as QgridService, r as RequestLogService } from "./services.generated-C8q1M41a.js";
3
+ import { a as Link, o as useNavigate } from "./lazyRouteComponent-Cu6IkL8A.js";
4
4
  import { n as Route } from "../entry-server.generated.js";
5
5
  import { n as formatMicroUsd, r as formatUsd, t as cacheHitRate } from "./cost-CSH_t17I.js";
6
6
  require_react();
@@ -271,12 +271,15 @@ function RequestLogTable({ search, onSearchChange }) {
271
271
  children: row.token_name
272
272
  })
273
273
  }),
274
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", {
275
275
  className: "px-4 py-1.5 whitespace-nowrap",
276
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
276
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
277
277
  className: "text-xs text-sand-500",
278
- children: row.model_name ?? "—"
279
- })
278
+ children: row.requested_model_name && row.model_name && row.requested_model_name !== row.model_name ? `${row.requested_model_name} → ${row.model_name}` : row.model_name ?? "—"
279
+ }), (row.fallback_count ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
280
+ className: "ml-1 text-[9px] text-caution-500",
281
+ children: ["×", row.fallback_count]
282
+ })]
280
283
  }),
281
284
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
282
285
  className: "px-4 py-1.5 text-left tabular-nums text-sand-500 whitespace-nowrap",
@@ -1,5 +1,5 @@
1
- import { Z as __toESM, _ as require_jsx_runtime, c as _enum, p as require_react_dom, u as object, v as require_react } from "./sd.generated-CE6D5clm.js";
2
- import { s as useQuery, t as MonitService } from "./services.generated-DyZUzXiq.js";
1
+ import { Z as __toESM, _ as require_jsx_runtime, c as _enum, p as require_react_dom, u as object, v as require_react } from "./sd.generated-qzuRe35X.js";
2
+ import { s as useQuery, t as MonitService } from "./services.generated-C8q1M41a.js";
3
3
  import { a as clsx, r as Route } from "../entry-server.generated.js";
4
4
  //#region ../../node_modules/.pnpm/@tanstack+virtual-core@3.17.4/node_modules/@tanstack/virtual-core/dist/esm/lazy-measurements.js
5
5
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
@@ -1,8 +1,8 @@
1
- import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, p as require_react_dom, v as require_react } from "./sd.generated-CE6D5clm.js";
2
- import { a as TokenService, n as QgridService } from "./services.generated-DyZUzXiq.js";
1
+ import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, p as require_react_dom, v as require_react } from "./sd.generated-qzuRe35X.js";
2
+ import { a as TokenService, n as QgridService } from "./services.generated-C8q1M41a.js";
3
3
  import { i as Q } from "../entry-server.generated.js";
4
4
  import { r as formatUsd } from "./cost-CSH_t17I.js";
5
- import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BejUW5sw.js";
5
+ import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BlZo6swF.js";
6
6
  //#region ../../node_modules/.pnpm/@dnd-kit+utilities@3.2.2_react@19.2.4/node_modules/@dnd-kit/utilities/dist/utilities.esm.js
7
7
  var import_react_dom = /* @__PURE__ */ __toESM(require_react_dom());
8
8
  var import_react = /* @__PURE__ */ __toESM(require_react());
@@ -22539,6 +22539,8 @@ var entityLabels = {
22539
22539
  "entity.RequestLog.token_name": "토큰 이름",
22540
22540
  "entity.RequestLog.project_name": "프로젝트 이름",
22541
22541
  "entity.RequestLog.model_name": "모델 이름",
22542
+ "entity.RequestLog.requested_model_name": "요청 모델 이름 (fallback 전)",
22543
+ "entity.RequestLog.fallback_count": "모델 fallback 횟수",
22542
22544
  "entity.RequestLog.user_prompt": "사용자 프롬프트",
22543
22545
  "entity.RequestLog.system_prompt": "시스템 프롬프트",
22544
22546
  "entity.RequestLog.response": "llm 응답 전문",
@@ -22546,9 +22548,12 @@ var entityLabels = {
22546
22548
  "entity.RequestLog.output_tokens": "출력 토큰",
22547
22549
  "entity.RequestLog.cache_read_tokens": "캐시 읽기 토큰",
22548
22550
  "entity.RequestLog.cache_creation_tokens": "캐시 생성 토큰",
22551
+ "entity.RequestLog.cache_creation_5m_tokens": "5분 TTL 캐시 생성 토큰",
22552
+ "entity.RequestLog.cache_creation_1h_tokens": "1시간 TTL 캐시 생성 토큰",
22549
22553
  "entity.RequestLog.duration_ms": "응답 소요시간(ms)",
22550
22554
  "entity.RequestLog.ttft_ms": "first gen TTFT(ms)",
22551
22555
  "entity.RequestLog.cost_usd": "API 비용",
22556
+ "entity.RequestLog.cost_source": "비용 출처(provider/pricing_table/mixed)",
22552
22557
  "entity.RequestLog.image_cost_usd": "이미지 생성 추정 비용",
22553
22558
  "entity.RequestLog.image_cost_method": "이미지 비용 추정 방식",
22554
22559
  "entity.RequestLog.effort": "effort level",
@@ -22571,10 +22576,17 @@ var entityLabels = {
22571
22576
  "entity.RequestLogStep.request_log": "부모 request log ID",
22572
22577
  "entity.RequestLogStep.step_index": "ai-sdk step number (0-based)",
22573
22578
  "entity.RequestLogStep.type": "step 타입",
22579
+ "entity.RequestLogStep.model_name": "실제 serving 모델",
22580
+ "entity.RequestLogStep.requested_model_name": "요청 모델 (fallback 전)",
22581
+ "entity.RequestLogStep.fallback_count": "모델 fallback 횟수",
22574
22582
  "entity.RequestLogStep.input_tokens": "입력 토큰 (generate)",
22575
22583
  "entity.RequestLogStep.output_tokens": "출력 토큰 (generate)",
22576
22584
  "entity.RequestLogStep.cache_read_tokens": "캐시 읽기 토큰",
22577
22585
  "entity.RequestLogStep.cache_creation_tokens": "캐시 생성 토큰",
22586
+ "entity.RequestLogStep.cache_creation_5m_tokens": "5분 TTL 캐시 생성 토큰",
22587
+ "entity.RequestLogStep.cache_creation_1h_tokens": "1시간 TTL 캐시 생성 토큰",
22588
+ "entity.RequestLogStep.cost_usd": "step 비용 (micro-USD)",
22589
+ "entity.RequestLogStep.cost_source": "비용 출처(provider/pricing_table)",
22578
22590
  "entity.RequestLogStep.duration_ms": "소요시간(ms)",
22579
22591
  "entity.RequestLogStep.ttft_ms": "Gen TTFT(ms)",
22580
22592
  "entity.RequestLogStep.finish_reason": "완료 사유",
@@ -1,4 +1,4 @@
1
- import { B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, G as timeoutManager, H as shouldThrowError, I as noop, K as focusManager, N as isValidTimeout, O as pendingThenable, R as replaceData, S as hasPreviousPage, T as fetchState, V as shallowEqualObjects, W as timeUntilStale, Z as __toESM, _ as require_jsx_runtime, a as fetch, b as getDefaultState, f as require_lib, g as useQueryClient, i as dedupeAndFlatten, j as hashKey, k as environmentManager, o as useRefreshable, q as Subscribable, s as useSSEStream, v as require_react, x as hasNextPage, z as resolveEnabled } from "./sd.generated-CE6D5clm.js";
1
+ import { B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, G as timeoutManager, H as shouldThrowError, I as noop, K as focusManager, N as isValidTimeout, O as pendingThenable, R as replaceData, S as hasPreviousPage, T as fetchState, V as shallowEqualObjects, W as timeUntilStale, Z as __toESM, _ as require_jsx_runtime, a as fetch, b as getDefaultState, f as require_lib, g as useQueryClient, i as dedupeAndFlatten, j as hashKey, k as environmentManager, o as useRefreshable, q as Subscribable, s as useSSEStream, v as require_react, x as hasNextPage, z as resolveEnabled } from "./sd.generated-qzuRe35X.js";
2
2
  //#region ../../node_modules/.pnpm/@tanstack+query-core@5.95.2/node_modules/@tanstack/query-core/build/modern/queryObserver.js
3
3
  var QueryObserver = class extends Subscribable {
4
4
  constructor(client, options) {
@@ -1,6 +1,6 @@
1
- import { J as __commonJSMin, Y as __exportAll, Z as __toESM, _ as require_jsx_runtime, v as require_react } from "./sd.generated-CE6D5clm.js";
2
- import { i as RequestLogStepService, r as RequestLogService } from "./services.generated-DyZUzXiq.js";
3
- import { a as Link } from "./lazyRouteComponent-BlfOh9S3.js";
1
+ import { J as __commonJSMin, Y as __exportAll, Z as __toESM, _ as require_jsx_runtime, v as require_react } from "./sd.generated-qzuRe35X.js";
2
+ import { i as RequestLogStepService, r as RequestLogService } from "./services.generated-C8q1M41a.js";
3
+ import { a as Link } from "./lazyRouteComponent-Cu6IkL8A.js";
4
4
  import { t as Route } from "../entry-server.generated.js";
5
5
  import { n as formatMicroUsd, t as cacheHitRate } from "./cost-CSH_t17I.js";
6
6
  import minpath from "node:path";
@@ -25308,7 +25308,11 @@ function HeaderBar({ data }) {
25308
25308
  children: [
25309
25309
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
25310
25310
  className: "text-[15px] font-semibold text-sand-900",
25311
- children: data.model_name ?? "Unknown model"
25311
+ children: data.requested_model_name !== null && data.model_name !== null && data.requested_model_name !== data.model_name ? `${data.requested_model_name} → ${data.model_name}` : data.model_name ?? "Unknown model"
25312
+ }),
25313
+ (data.fallback_count ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
25314
+ className: "text-[10px] px-1.5 py-0.5 rounded-full bg-caution-400/15 text-caution-500 font-medium uppercase",
25315
+ children: ["fallback ×", data.fallback_count]
25312
25316
  }),
25313
25317
  status && status !== "succeeded" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
25314
25318
  className: `text-[10px] px-1.5 py-0.5 rounded-full font-medium uppercase ${STATUS_STYLE[status] ?? "bg-sand-100 text-sand-500"}`,
@@ -25352,6 +25356,10 @@ function MetricsPanel({ data, toolCallCount }) {
25352
25356
  label: "Driver Cost",
25353
25357
  value: data.cost_usd !== null ? formatMicroUsd(data.cost_usd) : "—"
25354
25358
  }),
25359
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
25360
+ label: "Cost Source",
25361
+ value: data.cost_source ?? "legacy"
25362
+ }),
25355
25363
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
25356
25364
  label: "Image Cost",
25357
25365
  value: data.image_cost_usd !== null ? formatMicroUsd(data.image_cost_usd) : "—"
@@ -25380,6 +25388,14 @@ function MetricsPanel({ data, toolCallCount }) {
25380
25388
  label: "Cache Write",
25381
25389
  value: formatNum(data.cache_creation_tokens)
25382
25390
  }),
25391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
25392
+ label: "Cache Write (5m)",
25393
+ value: data.cache_creation_5m_tokens === null ? "—" : formatNum(data.cache_creation_5m_tokens)
25394
+ }),
25395
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
25396
+ label: "Cache Write (1h)",
25397
+ value: data.cache_creation_1h_tokens === null ? "—" : formatNum(data.cache_creation_1h_tokens)
25398
+ }),
25383
25399
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Metric, {
25384
25400
  label: "Cache Hit",
25385
25401
  value: cacheHitRate(data)
@@ -25614,6 +25630,14 @@ function StepTreeItem({ entry, tokensPerSecEnabled }) {
25614
25630
  className: "text-[10px] px-1.5 py-0.5 rounded bg-sand-100 text-sand-500 font-mono shrink-0",
25615
25631
  children: generate.finishReason
25616
25632
  }),
25633
+ generate && (generate.fallbackCount ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
25634
+ className: "text-[10px] px-1.5 py-0.5 rounded-full bg-caution-400/15 text-caution-500 font-medium uppercase shrink-0",
25635
+ children: [
25636
+ generate.requestedModelName,
25637
+ " → ",
25638
+ generate.modelName
25639
+ ]
25640
+ }),
25617
25641
  hasReasoning && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
25618
25642
  className: "text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 font-medium shrink-0",
25619
25643
  children: "reasoning"
@@ -25662,6 +25686,18 @@ function StepTreeItem({ entry, tokensPerSecEnabled }) {
25662
25686
  label: "Cache Write",
25663
25687
  value: formatNum(generate.cacheCreationTokens ?? 0)
25664
25688
  }),
25689
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompactMetric, {
25690
+ label: "Cache 5m / 1h",
25691
+ value: `${generate.cacheCreation5mTokens === null ? "—" : formatNum(generate.cacheCreation5mTokens)} / ${generate.cacheCreation1hTokens === null ? "—" : formatNum(generate.cacheCreation1hTokens)}`
25692
+ }),
25693
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompactMetric, {
25694
+ label: "Cost",
25695
+ value: generate.costUsd === null ? "—" : formatMicroUsd(generate.costUsd)
25696
+ }),
25697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompactMetric, {
25698
+ label: "Cost Source",
25699
+ value: generate.costSource ?? "legacy"
25700
+ }),
25665
25701
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompactMetric, {
25666
25702
  label: "Reasoning",
25667
25703
  value: generate.reasoningTokens !== null && generate.reasoningTokens !== void 0 ? formatNum(generate.reasoningTokens) : "—"
@@ -25708,10 +25744,17 @@ function buildStepTree(steps) {
25708
25744
  group.generate = {
25709
25745
  id: step.id,
25710
25746
  stepIndex: step.step_index,
25747
+ modelName: step.model_name,
25748
+ requestedModelName: step.requested_model_name,
25749
+ fallbackCount: step.fallback_count,
25711
25750
  inputTokens: step.input_tokens,
25712
25751
  outputTokens: step.output_tokens,
25713
25752
  cacheReadTokens: step.cache_read_tokens,
25714
25753
  cacheCreationTokens: step.cache_creation_tokens,
25754
+ cacheCreation5mTokens: step.cache_creation_5m_tokens,
25755
+ cacheCreation1hTokens: step.cache_creation_1h_tokens,
25756
+ costUsd: step.cost_usd,
25757
+ costSource: step.cost_source,
25715
25758
  durationMs: step.duration_ms,
25716
25759
  ttftMs: step.ttft_ms,
25717
25760
  finishReason: step.finish_reason,
@@ -1,7 +1,7 @@
1
- import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, v as require_react } from "./sd.generated-CE6D5clm.js";
2
- import { a as TokenService, n as QgridService } from "./services.generated-DyZUzXiq.js";
1
+ import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, v as require_react } from "./sd.generated-qzuRe35X.js";
2
+ import { a as TokenService, n as QgridService } from "./services.generated-C8q1M41a.js";
3
3
  import { i as Q, o as lucideKeyRound } from "../entry-server.generated.js";
4
- import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BejUW5sw.js";
4
+ import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BlZo6swF.js";
5
5
  //#region ~icons/lucide/plus.jsx
6
6
  var import_react = /* @__PURE__ */ __toESM(require_react());
7
7
  var import_jsx_runtime = require_jsx_runtime();
@@ -1,4 +1,4 @@
1
- import { n as QgridService, o as useMutation } from "./services.generated-DyZUzXiq.js";
1
+ import { n as QgridService, o as useMutation } from "./services.generated-C8q1M41a.js";
2
2
  //#region src/services/token/use-update-token-mutation.ts
3
3
  function useUpdateTokenMutation() {
4
4
  return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight }) => QgridService.updateToken(id, name, quotaThreshold, weight) });
@@ -1,5 +1,5 @@
1
- import { A as functionalUpdate, B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, E as onlineManager, F as matchQuery, I as noop$1, J as __commonJSMin, K as focusManager, L as partialMatchKey, M as hashQueryKeyByOptions, P as matchMutation, U as skipToken, X as __require, Z as __toESM, _ as require_jsx_runtime, c as _enum, d as string, h as QueryClientProvider, j as hashKey, l as number, m as require_shim, n as SUPPORTED_LOCALES, p as require_react_dom, q as Subscribable, r as setLocale, t as SD, u as object, v as require_react, w as Query, y as Mutation } from "./assets/sd.generated-CE6D5clm.js";
2
- import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-BlfOh9S3.js";
1
+ import { A as functionalUpdate, B as resolveStaleTime, C as infiniteQueryBehavior, D as notifyManager, E as onlineManager, F as matchQuery, I as noop$1, J as __commonJSMin, K as focusManager, L as partialMatchKey, M as hashQueryKeyByOptions, P as matchMutation, U as skipToken, X as __require, Z as __toESM, _ as require_jsx_runtime, c as _enum, d as string, h as QueryClientProvider, j as hashKey, l as number, m as require_shim, n as SUPPORTED_LOCALES, p as require_react_dom, q as Subscribable, r as setLocale, t as SD, u as object, v as require_react, w as Query, y as Mutation } from "./assets/sd.generated-qzuRe35X.js";
2
+ import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-Cu6IkL8A.js";
3
3
  //#region ../../node_modules/.pnpm/@tanstack+query-core@5.95.2/node_modules/@tanstack/query-core/build/modern/hydration.js
4
4
  function defaultTransformerFn(data) {
5
5
  return data;
@@ -22703,7 +22703,7 @@ var sections = [{
22703
22703
  }
22704
22704
  ]
22705
22705
  }];
22706
- var qgridVersion = "2.4.7";
22706
+ var qgridVersion = "2.4.8";
22707
22707
  function Sidebar({ className }) {
22708
22708
  const pathname = useRouterState({ select: (s) => s.location.pathname });
22709
22709
  const isActive = (path) => {
@@ -44584,11 +44584,11 @@ function RootComponent() {
44584
44584
  }
44585
44585
  //#endregion
44586
44586
  //#region src/routes/tokens.tsx
44587
- var $$splitComponentImporter$4 = () => import("./assets/tokens-fzMW4G74.js");
44587
+ var $$splitComponentImporter$4 = () => import("./assets/tokens-Bvjn15h9.js");
44588
44588
  var Route$4 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
44589
44589
  //#endregion
44590
44590
  //#region src/routes/monit.tsx
44591
- var $$splitComponentImporter$3 = () => import("./assets/monit-CYyY2S2f.js");
44591
+ var $$splitComponentImporter$3 = () => import("./assets/monit-DU6PLatf.js");
44592
44592
  var monitSearchSchema = object({ level: _enum([
44593
44593
  "all",
44594
44594
  "scaling",
@@ -44601,7 +44601,7 @@ var Route$3 = createFileRoute("/monit")({
44601
44601
  });
44602
44602
  //#endregion
44603
44603
  //#region src/routes/logs.tsx
44604
- var $$splitComponentImporter$2 = () => import("./assets/logs-DYly4ipQ.js");
44604
+ var $$splitComponentImporter$2 = () => import("./assets/logs-Dx2knN2T.js");
44605
44605
  var logsSearchSchema = object({
44606
44606
  token: string().optional(),
44607
44607
  project: string().optional(),
@@ -44613,11 +44613,11 @@ var Route$2 = createFileRoute("/logs")({
44613
44613
  });
44614
44614
  //#endregion
44615
44615
  //#region src/routes/index.tsx
44616
- var $$splitComponentImporter$1 = () => import("./assets/routes-Dqu3osXt.js");
44616
+ var $$splitComponentImporter$1 = () => import("./assets/routes-CTco1oJZ.js");
44617
44617
  var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
44618
44618
  //#endregion
44619
44619
  //#region src/routes/requests/show.tsx
44620
- var $$splitComponentImporter = () => import("./assets/show-BDul8xLb.js");
44620
+ var $$splitComponentImporter = () => import("./assets/show-B3q8_GDh.js");
44621
44621
  var showSearchSchema = object({ id: number() });
44622
44622
  var Route = createFileRoute("/requests/show")({
44623
44623
  validateSearch: showSearchSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"
@@ -32,12 +32,16 @@ When helping a user set up local `@cartanova/qgrid-ai-sdk`, `createQgridLogger`,
32
32
 
33
33
  Prefer `QGRID_PROJECT_NAME` as the project-wide default. Use config `projectName` only for a deliberate per-call/per-provider override. Do not add `projectName` or `project_name` under `providerOptions.qgrid`; current code does not read it there.
34
34
 
35
+ When writing AI SDK examples or setup code, import the public `QgridProviderOptions` type and apply `satisfies QgridProviderOptions` to the value under `providerOptions.qgrid`. AI SDK types the outer `providerOptions` as a generic JSON record, so it cannot infer qgrid's option names and values by itself. `QgridProviderOptions` is the inner qgrid namespace type, not the outer record type.
36
+
35
37
  ## Non-Negotiable Boundaries
36
38
 
37
39
  - Use `packages/ai-sdk` as the active public SDK surface.
38
40
  - Treat `packages/sdk` as deprecated. Read it only for legacy context or migration clues. Do not add new examples or features on top of it unless explicitly asked for legacy work.
39
41
  - Route provider models by prefix: `openai/*` goes to the OpenAI Codex runtime, `anthropic/*` goes to the Anthropic Claude Code runtime. Prefix-less model fallback is not implemented.
42
+ - Treat Fable 5 safety fallback as upstream Claude Code behavior, not qgrid routing: a classifier refusal can retry on Opus 4.8. Do not add a second qgrid retry. Preserve requested Fable versus actual serving Opus, fallback history, and provider-reported cost; a fresh Claude Code process means the fallback is not sticky across qgrid requests.
40
43
  - Treat dashboard work as Sonamu API/model/generated-client/web work, not isolated frontend work.
44
+ - Never hand-author migration files for Sonamu-managed schema. Change the entity definition, inspect `sonamu migrate status`, and create migration files with `pnpm --dir packages/api sonamu migrate generate`. Inspect the generated files before applying them. If Sonamu cannot express a required schema change, stop and ask rather than silently replacing its workflow with a custom migration.
41
45
  - Keep OpenAI Codex built-in tools, apps, plugins, skills, web search, shell, and environment instruction blocks disabled unless the user explicitly asks for agentic Codex behavior.
42
46
  - Treat OpenAI image generation as an opt-in Codex `image_generation` tool path. Inspect current code before modifying it; it is not a direct Images API call and its image cost is an estimate.
43
47
 
@@ -36,6 +36,25 @@ Setup agents should treat a missing project label as an actionable configuration
36
36
 
37
37
  qgrid-specific behavior is controlled through `providerOptions.qgrid`. Explain these options precisely because they are qgrid's extension point beyond normal AI SDK usage.
38
38
 
39
+ AI SDK declares the outer `providerOptions` as a generic JSON record, so it does not infer qgrid-specific keys or literal values. qgrid exports `QgridProviderOptions` for the inner `providerOptions.qgrid` value. Import it and put the `satisfies` check at that inner boundary:
40
+
41
+ ```ts
42
+ import { generateText } from "ai";
43
+ import { qgrid, type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
44
+
45
+ const result = await generateText({
46
+ model: qgrid("anthropic/claude-fable-5"),
47
+ prompt,
48
+ providerOptions: {
49
+ qgrid: {
50
+ effort: "high",
51
+ } satisfies QgridProviderOptions,
52
+ },
53
+ });
54
+ ```
55
+
56
+ Do not write `providerOptions: { ... } satisfies QgridProviderOptions`: the exported type describes the nested qgrid options object, not AI SDK's outer provider-name map. Use this typed pattern in qgrid setup guidance and examples so misspelled keys and invalid option values fail at compile time.
57
+
39
58
  `providerOptions.qgrid` supports:
40
59
 
41
60
  - `sessionKey`: OpenAI thread reuse key. Disabled for Anthropic models.
@@ -43,7 +62,7 @@ qgrid-specific behavior is controlled through `providerOptions.qgrid`. Explain t
43
62
  - `verbosity`: OpenAI/Codex route only.
44
63
  - `reasoningSummary`: OpenAI/Codex route only.
45
64
  - `serviceTier`: OpenAI/Codex route only.
46
- - `fallbackModels`: reserved for future fallback routing.
65
+ - `fallbackModels`: reserved for future qgrid server-side fallback routing. It is not the Fable 5 safety-refusal fallback, which is owned by Claude Code upstream.
47
66
  - `imageGeneration`: OpenAI/Codex non-stream only. Enables Codex's built-in `image_generation` tool for that request.
48
67
  - `imageGenerationOptions`: optional image quality/size hints and cost-estimation basis. Current supported values are `quality: "low" | "medium" | "high"` and `size: "1024x1024" | "1024x1536" | "1536x1024"`.
49
68
 
@@ -60,7 +79,7 @@ const result = await generateText({
60
79
  providerOptions: {
61
80
  qgrid: {
62
81
  serviceTier: "fast",
63
- },
82
+ } satisfies QgridProviderOptions,
64
83
  },
65
84
  });
66
85
  ```
@@ -112,6 +131,15 @@ qgrid response content maps to AI SDK content:
112
131
 
113
132
  When `imageGeneration` was requested and the server returns no image part, the AI SDK provider throws a version-skew/error guard instead of silently accepting text-only output.
114
133
 
134
+ For a successful Fable 5 refusal fallback:
135
+
136
+ - `response.modelId` and `providerMetadata.qgrid.model` are the actual serving model, normally `claude-opus-4-8`.
137
+ - `providerMetadata.qgrid.requestedModel` remains `claude-fable-5`.
138
+ - `providerMetadata.qgrid.modelFallbacks` preserves the refusal route and optional category/explanation.
139
+ - `providerMetadata.qgrid.costSource` reports whether cost came from Claude Code or qgrid's pricing table. Prefer the provider-reported combined cost for this path.
140
+
141
+ Do not expose the requested Fable model as `response.modelId` after Opus served the answer, and do not map this upstream safety behavior onto the reserved `providerOptions.qgrid.fallbackModels` option.
142
+
115
143
  Usage maps qgrid standard usage into AI SDK V3 usage:
116
144
 
117
145
  - `inputTokens.total = input_tokens`
@@ -12,6 +12,7 @@ Use this reference before changing Anthropic provider behavior, Claude Code spaw
12
12
  - Config isolation
13
13
  - Input adaptation
14
14
  - Output adaptation
15
+ - Fable refusal fallback
15
16
  - Structured output
16
17
  - 1M context
17
18
 
@@ -70,7 +71,7 @@ claude -p
70
71
  --model <canonical-model-or-1m-suffix>
71
72
  --system-prompt <text> # small system prompt
72
73
  --system-prompt-file <path> # large system prompt
73
- --thinking disabled
74
+ --thinking disabled # omitted for Fable 5
74
75
  --effort <effort-or-low>
75
76
  --disable-slash-commands
76
77
  --session-id <uuid>
@@ -83,7 +84,7 @@ Important details:
83
84
  - `--setting-sources project` plus seeded settings isolates user configuration.
84
85
  - `--system-prompt` or `--system-prompt-file` is always supplied. Omitting it would allow Claude Code default system prompt injection.
85
86
  - Large system prompts over 64 KiB are written to a temporary file to avoid argv `E2BIG`.
86
- - `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off.
87
+ - `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off for existing models. Fable 5 requires always-on adaptive thinking, so qgrid omits all three suppressors for it.
87
88
 
88
89
  ## Spawn env
89
90
 
@@ -96,13 +97,13 @@ Included env:
96
97
  - `CLAUDE_CODE_OAUTH_TOKEN`
97
98
  - `CLAUDE_CONFIG_DIR`
98
99
  - `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`
99
- - `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1`
100
+ - `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` except for Fable 5
100
101
  - `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`
101
102
  - `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`
102
103
  - `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS=1`
103
104
  - `CLAUDE_CODE_DISABLE_WORKFLOWS=1`
104
105
  - `CLAUDE_CODE_ATTRIBUTION_HEADER=0`
105
- - `MAX_THINKING_TOKENS=0`
106
+ - `MAX_THINKING_TOKENS=0` except for Fable 5
106
107
  - `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` when model does not support qgrid's 1M path
107
108
  - `MAX_STRUCTURED_OUTPUT_RETRIES` for structured output only
108
109
 
@@ -144,8 +145,43 @@ Anthropic native usage categories are mutually exclusive. qgrid normalizes them
144
145
  inputTokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens
145
146
  cachedInputTokens = cache_read_input_tokens
146
147
  cacheCreationInputTokens = cache_creation_input_tokens
148
+ cacheCreationInputTokens5m = cache_creation.ephemeral_5m_input_tokens
149
+ cacheCreationInputTokens1h = cache_creation.ephemeral_1h_input_tokens
147
150
  ```
148
151
 
152
+ ## Fable refusal fallback
153
+
154
+ Fable 5 runs upstream safety classifiers. A refusal is a successful HTTP response with
155
+ `stop_reason: "refusal"`, not an HTTP/provider error, and can happen before output or after
156
+ partial streamed output. Claude Code handles the refusal fallback and can retry on Opus 4.8.
157
+ This is distinct from the CLI `--fallback-model` flag, which is for overload on the default
158
+ model. qgrid does not configure either mechanism or add another retry.
159
+
160
+ The stream adapter must preserve all observable routing evidence:
161
+
162
+ - `system.subtype = "model_refusal_fallback"` provides the original/fallback models and refusal details.
163
+ - `assistant.message.model` identifies the model that produced that message.
164
+ - `usage.iterations` contains the original `message` and `fallback_message` when available.
165
+ - Treat `stop_reason`, rather than `stop_details` or empty content, as the authoritative refusal signal. Category and explanation are optional metadata.
166
+ - `model_refusal_no_fallback` or a terminal `stop_reason: "refusal"` is an error, even if the outer result subtype says `success`. A successfully served fallback has a non-refusal terminal reason and a `fallback_message` iteration.
167
+
168
+ `QueryOutput.model` is the actual serving model. `requestedModel` and `modelFallbacks` preserve
169
+ the caller's requested model and routing history. Because qgrid uses a fresh Claude Code process
170
+ for each request, any Claude Code session-sticky fallback state does not carry to the next request;
171
+ each new Fable request may independently refuse and fall back again.
172
+
173
+ Cost and usage must follow the upstream combined result. Prefer Claude Code's `total_cost_usd` and
174
+ preserve `usage.iterations` only as attribution evidence; do not independently price every iteration
175
+ and add them together. An upstream pre-output refusal is not billed, while work completed before a
176
+ mid-stream refusal can be billed. Upstream fallback handling applies fallback credit where applicable,
177
+ so a qgrid-side retry would risk duplicate work and incorrect cache-write billing.
178
+
179
+ Upstream references:
180
+
181
+ - [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback)
182
+ - [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit)
183
+ - [Claude Code CLI `--fallback-model`](https://code.claude.com/docs/en/cli-reference)
184
+
149
185
  ## Structured output
150
186
 
151
187
  qgrid strictifies schemas before provider dispatch. Claude Code `--json-schema` is not grammar-constrained decoding; it is a `StructuredOutput` tool plus post-validation behavior. Keep schemas strict and preserve error signaling for failed structured-output attempts.
@@ -156,8 +192,10 @@ qgrid strictifies schemas before provider dispatch. Claude Code `--json-schema`
156
192
 
157
193
  Model normalization strips provider prefix and `[1m]` for canonical model/cost keys.
158
194
 
159
- qgrid's measured 1M support set currently includes:
195
+ qgrid's exact 1M support set currently includes:
160
196
 
197
+ - `claude-fable-5`
198
+ - `claude-sonnet-5`
161
199
  - `claude-sonnet-4-6`
162
200
  - `claude-opus-4-6`
163
201
  - `claude-opus-4-8`
@@ -29,8 +29,10 @@ Key decisions:
29
29
  - `packages/ai-sdk` is the active public SDK. `packages/sdk` is deprecated and should be read only for legacy context.
30
30
  - qgrid's AI SDK provider follows the AI SDK `LanguageModelV3` contract. Generic usage belongs to AI SDK docs; qgrid-specific docs should focus on qgrid routing, logging, cache/session behavior, and `providerOptions.qgrid`.
31
31
  - qgrid-specific options live under `providerOptions.qgrid`, not under provider-specific namespaces such as `providerOptions.openai`. Consumers should not need to branch on Codex versus Claude internals.
32
+ - AI SDK exposes the outer `providerOptions` as a generic JSON record. `QgridProviderOptions` intentionally types the nested `providerOptions.qgrid` value, and examples should apply `satisfies` there to restore qgrid-specific inference without replacing AI SDK's outer contract.
32
33
  - `projectName` is provider config/payload camelCase. `project_name` is the Sonamu/database request-log column. Prefer `QGRID_PROJECT_NAME` as the project-wide default so request logs and metrics remain filterable at volume.
33
34
  - `fallbackModels` may exist as a typed future option, but it is not a server wire contract until the server implements fallback routing. Do not forward it as behavior just because the type exists.
35
+ - Fable 5 refusal fallback is an upstream Claude Code safety path to Opus 4.8, not qgrid's reserved `fallbackModels` feature or Claude Code's overload-only `--fallback-model` flag. qgrid observes and reports the actual route and provider cost; it must not retry again.
34
36
  - The SDK should stay a thin adapter. Server APIs own request-log lifecycle, provider dispatch, structured-output emulation, and provider runtime details.
35
37
 
36
38
  ## Tool Calling And Request-Log Lifecycle
@@ -97,7 +97,10 @@ Do not double-count cache subfields. This was a real dashboard bug: OpenAI cache
97
97
  - OpenAI cost normally uses qgrid's price table.
98
98
  - OpenAI image generation stores a separate `image_cost_usd` estimate. It assumes Codex's image tool is priced as `gpt-image-2` for the selected/default quality and size, because qgrid does not receive exact image tool usage from Codex.
99
99
  - Anthropic Claude Code may return `total_cost_usd`; qgrid prefers provider cost when present and positive.
100
- - Anthropic cache creation uses 5-minute cache write price in fallback tables.
100
+ - Anthropic responses can split cache creation into `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`; preserve that split and price each TTL separately.
101
+ - Claude Code automatically requests 1-hour cache TTL for subscription OAuth and 5-minute TTL for API-key or usage-credit paths. Only legacy responses without a TTL breakdown use the subscription path's 1-hour rate as qgrid's fallback.
102
+ - Persist per-step `cost_usd`, `cost_source`, and the 5m/1h split. New request logs use that exact stored cost; only legacy rows with no `cost_source` are repriced from the current table.
103
+ - When Fable refuses and Claude Code serves Opus, prefer Claude Code's positive `total_cost_usd`. Persist Fable as the requested model and Opus as the serving model instead of pricing the combined turn as Fable.
101
104
  - OpenAI long-context surcharge applies to the whole request when input exceeds the model threshold, not just the excess tokens.
102
105
 
103
106
  When changing models or prices, verify against current official pricing before editing the table.
@@ -47,14 +47,18 @@ Stored request log usage uses qgrid-standard semantics:
47
47
  - `input_tokens`: total input, including cache read/write.
48
48
  - `cache_read_tokens`: cached input read.
49
49
  - `cache_creation_tokens`: prompt cache write input.
50
+ - `cache_creation_5m_tokens` / `cache_creation_1h_tokens`: nullable Anthropic TTL breakdown.
50
51
  - `output_tokens`: output tokens.
51
52
  - `cost_usd`: integer micro-USD in DB; displayed USD is `cost_usd / 1_000_000`.
53
+ - `cost_source`: `provider`, `pricing_table`, or `mixed`. New rows keep the cost calculated at request time.
54
+ - `requested_model_name` / `model_name`: requested and actual serving models; they differ on Fable refusal fallback.
55
+ - `fallback_count`: number of observed model fallbacks in the run/step.
52
56
  - `image_cost_usd`: integer micro-USD estimate for Codex image generation output.
53
57
  - `image_cost_method`: string such as `assumed:gpt-image-2:medium:1536x1024:png`.
54
58
 
55
59
  OpenAI/Codex: use per-turn `.last` usage from `thread/tokenUsage/updated`.
56
60
 
57
- Anthropic: normalize native mutually exclusive categories by summing input + cache creation + cache read into total input.
61
+ Anthropic: normalize native mutually exclusive categories by summing input + cache creation + cache read into total input. Prefer positive Claude Code `total_cost_usd`; otherwise calculate from the actual serving model and TTL split.
58
62
 
59
63
  For image requests, keep `cost_usd` as the Codex driver model token cost. Image output cost is separate because qgrid observes Codex's `image_generation` result, not the OpenAI Images API usage object. Treat `image_cost_usd` as a price-table estimate that may be inaccurate if Codex changes its underlying image accounting.
60
64
 
@@ -62,7 +66,7 @@ Reference images for image generation are not stored in `request_logs.user_promp
62
66
 
63
67
  ## Legacy normalization
64
68
 
65
- `RequestLogModel` normalizes legacy Anthropic rows where stored `input_tokens` may not include cache read/write. Keep this in mind when changing cost or display logic.
69
+ `RequestLogModel` normalizes legacy Anthropic rows where stored `input_tokens` may not include cache read/write. Rows with `cost_source = NULL` are legacy and can be repriced from the current table; rows with a source retain their exact stored cost across price/promotion changes.
66
70
 
67
71
  ## TTFT
68
72
 
@@ -21,6 +21,19 @@ qgrid dashboard work is usually a Sonamu API/model/generated-client/web change,
21
21
  - Web components under `packages/web/src/components/qgrid` consume generated API shapes.
22
22
  - The main dashboard sidebar version should stay synced to `packages/cli/package.json`. `packages/web/vite.config.ts` reads that package version and exposes it as `__QGRID_CLI_VERSION__`; `Sidebar.tsx` displays it.
23
23
 
24
+ ## Migration workflow
25
+
26
+ Sonamu entity definitions are the source of truth for managed database schema.
27
+
28
+ 1. Change the relevant `*.entity.json` files.
29
+ 2. Run `pnpm --dir packages/api sonamu migrate status` and inspect `preparedCodes`.
30
+ 3. Run `pnpm --dir packages/api sonamu migrate generate`.
31
+ 4. Inspect the generated, table-scoped migration files without rewriting them by hand.
32
+ 5. Apply them through Sonamu's migration target flow or qgrid's startup migration runner.
33
+ 6. Re-run migration status and require `pending: []` and `preparedCodes: []`.
34
+
35
+ Do not hand-author a migration that Sonamu can derive from entity changes. If generation cannot represent the required operation, stop and make the exception explicit before changing migration code or database state. Never delete or edit a migration that has already shipped to another environment.
36
+
24
37
  ## Request log dashboard
25
38
 
26
39
  Request log changes usually touch: