kimi-proxy 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -137,6 +137,15 @@ The API runs on `http://127.0.0.1:8000` and serves the dashboard (built assets)
137
137
 
138
138
  ## Configuration
139
139
 
140
+ ### Dashboard & LiveStore
141
+
142
+ Control LiveStore sync behavior via environment variables:
143
+
144
+ | Variable | Default | Description |
145
+ | ----------------------- | ------- | ----------------------------------------------------------------------------------------------- |
146
+ | `LIVESTORE_BATCH` | 50 | Batch size for dashboard sync (range: 1-500) |
147
+ | `LIVESTORE_MAX_RECORDS` | 500 | Memory sliding window - max records to keep in LiveStore. Set to 0 to disable (not recommended) |
148
+
140
149
  ### Providers
141
150
 
142
151
  Set environment variables in `.env`:
@@ -169,6 +178,15 @@ models:
169
178
 
170
179
  The web dashboard shows request/response logs and metrics. Access it at the root path when running the proxy. LiveStore metadata sync pulls from `/api/livestore/pull` in batches (size controlled by `LIVESTORE_BATCH`) and lazily fetches blobs on expansion. Build the dashboard with `bun run build:all` to serve static assets from the backend.
171
180
 
181
+ ### Performance Features
182
+
183
+ - **Reverse-chronological loading**: Data loads from newest to oldest, providing immediate access to recent logs
184
+ - **Memory-efficient virtualization**: Uses TanStack Virtual to render only visible rows
185
+ - **Configurable sliding window**: Limit browser memory usage by setting `LIVESTORE_MAX_RECORDS` (see `.env.example`)
186
+ - **Automatic garbage collection**: Old records beyond the window limit are automatically purged
187
+
188
+ The dashboard uses reactive queries with TanStack Table and TanStack Virtual for fast, efficient rendering of large datasets.
189
+
172
190
  ## Development
173
191
 
174
192
  ```bash
package/dist/config.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface AppConfig {
17
17
  };
18
18
  livestore: {
19
19
  batchSize: number;
20
+ maxRecords?: number;
20
21
  };
21
22
  providers: {
22
23
  openai?: OpenAIConfig;
@@ -9,7 +9,7 @@ export declare class OpenAIChatClientAdapter implements ClientAdapter {
9
9
  export declare class AnthropicMessagesClientAdapter implements ClientAdapter {
10
10
  clientFormat: ClientFormat;
11
11
  toUlx(body: ClientRequest, headers: Record<string, string>): Request;
12
- fromUlx(ulxResponse: Response): JsonValue;
12
+ fromUlx(ulxResponse: Response, _ulxRequest: Request): JsonValue;
13
13
  }
14
14
  export declare class OpenAIResponsesClientAdapter implements ClientAdapter {
15
15
  clientFormat: ClientFormat;
package/dist/index.js CHANGED
@@ -91,6 +91,7 @@ function loadConfig() {
91
91
  const streamDelay = Number(process.env.STREAM_DELAY ?? "10");
92
92
  const streamChunkSize = Number(process.env.STREAM_CHUNK_SIZE ?? "5");
93
93
  const livestoreBatch = Number(process.env.LIVESTORE_BATCH ?? "50");
94
+ const livestoreMaxRecords = process.env.LIVESTORE_MAX_RECORDS ? Number(process.env.LIVESTORE_MAX_RECORDS) : 500;
94
95
  const openai = resolveOpenAI();
95
96
  const anthropic = resolveAnthropic();
96
97
  const openrouter = resolveOpenRouter();
@@ -116,7 +117,10 @@ function loadConfig() {
116
117
  server: { host, port },
117
118
  logging: { dbPath, blobRoot },
118
119
  streaming: { delay: streamDelay, chunkSize: streamChunkSize },
119
- livestore: { batchSize: Math.max(1, Math.min(500, livestoreBatch)) },
120
+ livestore: {
121
+ batchSize: Math.max(1, Math.min(500, livestoreBatch)),
122
+ maxRecords: livestoreMaxRecords
123
+ },
120
124
  providers: { openai, anthropic, openrouter, vertex },
121
125
  models: modelRegistry
122
126
  };
@@ -768,12 +772,12 @@ class HybridLogStore {
768
772
  const clauses = [];
769
773
  const params = { limit };
770
774
  if (checkpoint.timestamp) {
771
- clauses.push(`(timestamp > @ts OR (timestamp = @ts AND id > @id))`);
775
+ clauses.push(`(timestamp < @ts OR (timestamp = @ts AND id < @id))`);
772
776
  params.ts = checkpoint.timestamp;
773
- params.id = checkpoint.id ?? 0;
777
+ params.id = checkpoint.id ?? Number.MAX_SAFE_INTEGER;
774
778
  }
775
779
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
776
- const rows = this.db.prepare(`SELECT * FROM logs ${where} ORDER BY datetime(timestamp), id LIMIT @limit`).all(params);
780
+ const rows = this.db.prepare(`SELECT * FROM logs ${where} ORDER BY datetime(timestamp) DESC, id DESC LIMIT @limit`).all(params);
777
781
  return { items: rows, total: rows.length, page: 1, pageSize: rows.length };
778
782
  }
779
783
  resolveBlobPath(record, kind) {
@@ -1554,33 +1558,36 @@ class OpenAIChatClientAdapter {
1554
1558
  };
1555
1559
  }
1556
1560
  fromUlx(ulxResponse, _ulxRequest) {
1557
- const contentBlocks = ulxResponse.output.find((entry) => entry.type === "message");
1558
- let content = null;
1559
- let reasoning_content = undefined;
1560
- if (contentBlocks?.type === "message") {
1561
- const textParts = [];
1562
- const reasoningParts = [];
1563
- for (const entry of contentBlocks.content) {
1564
- if (entry.type === "text") {
1565
- textParts.push(entry.text ?? "");
1566
- } else if (entry.type === "reasoning") {
1567
- reasoningParts.push(entry.text ?? "");
1561
+ const textParts = [];
1562
+ const reasoningParts = [];
1563
+ let toolCalls = undefined;
1564
+ for (const block of ulxResponse.output) {
1565
+ if (block.type === "message") {
1566
+ for (const entry of block.content) {
1567
+ if (entry.type === "reasoning") {
1568
+ reasoningParts.push(entry.text ?? "");
1569
+ }
1570
+ }
1571
+ const meaningfulContent = block.content.filter((c) => c.type !== "reasoning");
1572
+ if (meaningfulContent.length > 0) {
1573
+ const mixed = contentToText(meaningfulContent);
1574
+ textParts.push(typeof mixed === "string" ? mixed : JSON.stringify(mixed));
1575
+ }
1576
+ if (block.tool_calls) {
1577
+ toolCalls = [...toolCalls ?? [], ...block.tool_calls];
1578
+ }
1579
+ } else if (block.type === "reasoning") {
1580
+ for (const entry of block.content) {
1581
+ if (entry.type === "reasoning") {
1582
+ reasoningParts.push(entry.text ?? "");
1583
+ }
1568
1584
  }
1569
- }
1570
- if (textParts.length > 0) {
1571
- content = textParts.join("");
1572
- } else if (contentBlocks.content.some((c) => c.type === "image_url" || c.type === "json")) {
1573
- const mixed = contentToText(contentBlocks.content.filter((c) => c.type !== "reasoning"));
1574
- content = typeof mixed === "string" ? mixed : JSON.stringify(mixed);
1575
- } else {
1576
- content = null;
1577
- }
1578
- if (reasoningParts.length > 0) {
1579
- reasoning_content = reasoningParts.join(`
1580
-
1581
- `);
1582
1585
  }
1583
1586
  }
1587
+ const content = textParts.join("") || null;
1588
+ const reasoning_content = reasoningParts.join(`
1589
+
1590
+ `) || undefined;
1584
1591
  return {
1585
1592
  id: ulxResponse.id,
1586
1593
  object: "chat.completion",
@@ -1591,9 +1598,9 @@ class OpenAIChatClientAdapter {
1591
1598
  index: 0,
1592
1599
  message: {
1593
1600
  role: "assistant",
1594
- content,
1595
1601
  ...reasoning_content ? { reasoning_content } : {},
1596
- tool_calls: contentBlocks?.type === "message" && contentBlocks.tool_calls ? contentBlocks.tool_calls.map((tc) => ({
1602
+ content,
1603
+ tool_calls: toolCalls ? toolCalls.map((tc) => ({
1597
1604
  id: tc.id,
1598
1605
  type: "function",
1599
1606
  function: {
@@ -1683,7 +1690,11 @@ class AnthropicMessagesClientAdapter {
1683
1690
  function anthropicBlockToUlxContent(block) {
1684
1691
  const declaredType = typeof block.type === "string" ? block.type : "";
1685
1692
  if (declaredType === "thinking" || declaredType === "redacted_thinking" || typeof block.thinking === "string") {
1686
- return;
1693
+ return {
1694
+ type: "reasoning",
1695
+ text: block.thinking ?? block.text ?? "",
1696
+ data: block.signature ? { signature: block.signature } : declaredType === "redacted_thinking" ? { redacted: true } : undefined
1697
+ };
1687
1698
  }
1688
1699
  if (typeof block.text === "string") {
1689
1700
  return { type: "text", text: block.text };
@@ -1799,37 +1810,57 @@ class AnthropicMessagesClientAdapter {
1799
1810
  metadata: { clientFormat: this.clientFormat, headers }
1800
1811
  };
1801
1812
  }
1802
- fromUlx(ulxResponse) {
1803
- const contentBlocks = ulxResponse.output.find((entry) => entry.type === "message");
1804
- const messageContent = contentBlocks?.type === "message" ? contentToText(contentBlocks.content) : "";
1805
- const content = [];
1806
- if (typeof messageContent === "string") {
1807
- if (messageContent)
1808
- content.push({ type: "text", text: messageContent });
1809
- } else if (Array.isArray(messageContent)) {
1810
- for (const item of messageContent) {
1811
- if (item && typeof item === "object") {
1812
- const block = item;
1813
- if (block.type === "thinking") {
1814
- content.push(block);
1815
- } else if (block.type === "text") {
1816
- content.push(block);
1813
+ fromUlx(ulxResponse, _ulxRequest) {
1814
+ const thinkingBlocks = [];
1815
+ const textBlocks = [];
1816
+ const toolUseBlocks = [];
1817
+ for (const block of ulxResponse.output) {
1818
+ if (block.type === "reasoning") {
1819
+ for (const entry of block.content) {
1820
+ if (entry.type === "reasoning") {
1821
+ thinkingBlocks.push({
1822
+ type: "thinking",
1823
+ thinking: entry.text ?? "",
1824
+ signature: entry.data?.signature
1825
+ });
1826
+ }
1827
+ }
1828
+ } else if (block.type === "message") {
1829
+ const messageContent = contentToText(block.content);
1830
+ if (typeof messageContent === "string") {
1831
+ if (messageContent)
1832
+ textBlocks.push({ type: "text", text: messageContent });
1833
+ } else if (Array.isArray(messageContent)) {
1834
+ for (const item of messageContent) {
1835
+ if (item && typeof item === "object") {
1836
+ const b = item;
1837
+ if (b.type === "thinking") {
1838
+ thinkingBlocks.push(b);
1839
+ } else if (b.type === "text") {
1840
+ textBlocks.push(b);
1841
+ }
1842
+ }
1843
+ }
1844
+ } else if (typeof messageContent === "object" && messageContent !== null && "text" in messageContent) {
1845
+ textBlocks.push(messageContent);
1846
+ }
1847
+ if (block.tool_calls) {
1848
+ for (const tc of block.tool_calls) {
1849
+ toolUseBlocks.push({
1850
+ type: "tool_use",
1851
+ id: tc.id,
1852
+ name: tc.name,
1853
+ input: JSON.parse(tc.arguments)
1854
+ });
1817
1855
  }
1818
1856
  }
1819
- }
1820
- } else if (typeof messageContent === "object" && messageContent !== null && "text" in messageContent) {
1821
- content.push(messageContent);
1822
- }
1823
- if (contentBlocks?.type === "message" && contentBlocks.tool_calls) {
1824
- for (const tc of contentBlocks.tool_calls) {
1825
- content.push({
1826
- type: "tool_use",
1827
- id: tc.id,
1828
- name: tc.name,
1829
- input: JSON.parse(tc.arguments)
1830
- });
1831
1857
  }
1832
1858
  }
1859
+ const content = [
1860
+ ...thinkingBlocks,
1861
+ ...textBlocks,
1862
+ ...toolUseBlocks
1863
+ ];
1833
1864
  return {
1834
1865
  id: ulxResponse.id,
1835
1866
  type: "message",
@@ -2089,18 +2120,19 @@ class OpenAIResponsesClientAdapter {
2089
2120
  }
2090
2121
  fromUlx(ulxResponse, ulxRequest) {
2091
2122
  const outputBlocks = ulxResponse.output;
2092
- const output = [];
2093
2123
  const textParts = [];
2094
2124
  const createdAt = Math.floor(Date.now() / 1000);
2095
2125
  let messageIndex = 0;
2096
2126
  let functionCallIndex = 0;
2097
2127
  let reasoningIndex = 0;
2128
+ const collectedReasoning = [];
2129
+ const collectedMessages = [];
2130
+ const collectedFunctionCalls = [];
2098
2131
  for (const block of outputBlocks) {
2099
2132
  if (block.type === "message") {
2100
2133
  const messageId = `msg_${ulxResponse.id}_${messageIndex++}`;
2101
2134
  const status = block.status === "incomplete" ? "incomplete" : "completed";
2102
2135
  const content = [];
2103
- const reasoningContent = [];
2104
2136
  for (const entry of block.content) {
2105
2137
  if (entry.type === "text") {
2106
2138
  const text = entry.text ?? "";
@@ -2119,22 +2151,16 @@ class OpenAIResponsesClientAdapter {
2119
2151
  }
2120
2152
  content.push({ type: "output_text", text, annotations: [] });
2121
2153
  } else if (entry.type === "reasoning") {
2122
- reasoningContent.push({
2123
- type: "reasoning_text",
2124
- text: entry.text ?? ""
2154
+ collectedReasoning.push({
2155
+ type: "reasoning",
2156
+ id: `rsn_${ulxResponse.id}_${reasoningIndex++}`,
2157
+ status: "completed",
2158
+ content: [{ type: "reasoning_text", text: entry.text ?? "" }],
2159
+ summary: []
2125
2160
  });
2126
2161
  }
2127
2162
  }
2128
- if (reasoningContent.length > 0) {
2129
- output.push({
2130
- type: "reasoning",
2131
- id: `rsn_${ulxResponse.id}_${reasoningIndex++}`,
2132
- status: "completed",
2133
- content: reasoningContent,
2134
- summary: []
2135
- });
2136
- }
2137
- output.push({
2163
+ collectedMessages.push({
2138
2164
  type: "message",
2139
2165
  id: messageId,
2140
2166
  role: "assistant",
@@ -2143,7 +2169,7 @@ class OpenAIResponsesClientAdapter {
2143
2169
  });
2144
2170
  if (block.tool_calls) {
2145
2171
  for (const call of block.tool_calls) {
2146
- output.push({
2172
+ collectedFunctionCalls.push({
2147
2173
  type: "function_call",
2148
2174
  id: `fc_${ulxResponse.id}_${functionCallIndex++}`,
2149
2175
  call_id: call.id,
@@ -2154,7 +2180,7 @@ class OpenAIResponsesClientAdapter {
2154
2180
  }
2155
2181
  }
2156
2182
  } else if (block.type === "tool_call") {
2157
- output.push({
2183
+ collectedFunctionCalls.push({
2158
2184
  type: "function_call",
2159
2185
  id: `fc_${ulxResponse.id}_${functionCallIndex++}`,
2160
2186
  call_id: block.call_id,
@@ -2163,7 +2189,7 @@ class OpenAIResponsesClientAdapter {
2163
2189
  status: block.status === "pending" ? "in_progress" : "completed"
2164
2190
  });
2165
2191
  } else if (block.type === "reasoning") {
2166
- output.push({
2192
+ collectedReasoning.push({
2167
2193
  type: "reasoning",
2168
2194
  id: `rsn_${ulxResponse.id}_${reasoningIndex++}`,
2169
2195
  status: "completed",
@@ -2178,6 +2204,11 @@ class OpenAIResponsesClientAdapter {
2178
2204
  });
2179
2205
  }
2180
2206
  }
2207
+ const output = [
2208
+ ...collectedReasoning,
2209
+ ...collectedMessages,
2210
+ ...collectedFunctionCalls
2211
+ ];
2181
2212
  const inputTokens = ulxResponse.usage?.input_tokens ?? 0;
2182
2213
  const outputTokens = ulxResponse.usage?.output_tokens ?? 0;
2183
2214
  const totalTokens = ulxResponse.usage?.total_tokens ?? inputTokens + outputTokens;
@@ -2387,7 +2418,9 @@ function createCapturingFetch(originalFetch = globalThis.fetch) {
2387
2418
  // src/core/providers/anthropic.ts
2388
2419
  var AnthropicContentSchema = z6.object({
2389
2420
  type: z6.string(),
2390
- text: z6.string().optional()
2421
+ text: z6.string().optional(),
2422
+ thinking: z6.string().optional(),
2423
+ signature: z6.string().optional()
2391
2424
  }).passthrough();
2392
2425
  var AnthropicResponseSchema = z6.object({
2393
2426
  id: z6.string(),
@@ -2428,6 +2461,13 @@ function toAnthropicContent(blocks) {
2428
2461
  return blocks.map((entry) => {
2429
2462
  if (entry.type === "text")
2430
2463
  return { type: "text", text: entry.text ?? "" };
2464
+ if (entry.type === "reasoning") {
2465
+ return {
2466
+ type: "thinking",
2467
+ thinking: entry.text ?? "",
2468
+ signature: entry.data?.signature
2469
+ };
2470
+ }
2431
2471
  if (entry.type === "image_url") {
2432
2472
  if (typeof entry.url === "string") {
2433
2473
  const match = entry.url.match(/^data:([^;]+);base64,(.+)$/);
@@ -2468,7 +2508,8 @@ function anthropicResponseToUlx(body, request) {
2468
2508
  type: "reasoning",
2469
2509
  content: reasoning.map((part) => ({
2470
2510
  type: "reasoning",
2471
- text: part.text ?? ""
2511
+ text: part.thinking ?? part.text ?? "",
2512
+ data: part.signature ? { signature: part.signature } : undefined
2472
2513
  })),
2473
2514
  summary: []
2474
2515
  });
@@ -2626,7 +2667,7 @@ var TOOL_SECTION_END = "<|tool_calls_section_end|>";
2626
2667
  function cleanText(text) {
2627
2668
  if (!text)
2628
2669
  return "";
2629
- return text.replaceAll("(no content)", "").replace(/\n\s*\n\s*\n+/g, `
2670
+ return text.replaceAll("(no content)", "").replace(/<tool_call>[a-zA-Z0-9_:-]+/g, "").replace(/\n\s*\n\s*\n+/g, `
2630
2671
 
2631
2672
  `).trim();
2632
2673
  }
@@ -2752,6 +2793,18 @@ function fixKimiResponse(response, request) {
2752
2793
  const message = choice.message !== undefined && isJsonObject(choice.message) ? choice.message : choice.message = {};
2753
2794
  const rawToolCalls = message.tool_calls;
2754
2795
  let aggregatedToolCalls = Array.isArray(rawToolCalls) ? [...rawToolCalls] : [];
2796
+ if (typeof message.reasoning === "string" && !message.reasoning_content) {
2797
+ message.reasoning_content = message.reasoning;
2798
+ }
2799
+ if (Array.isArray(message.reasoning_details) && !message.reasoning_content) {
2800
+ const details = message.reasoning_details;
2801
+ const text = details.filter((d) => d.type === "reasoning.text" && typeof d.text === "string").map((d) => d.text).join(`
2802
+
2803
+ `);
2804
+ if (text) {
2805
+ message.reasoning_content = text;
2806
+ }
2807
+ }
2755
2808
  if (typeof message.reasoning_content === "string") {
2756
2809
  const original = message.reasoning_content;
2757
2810
  const { cleanedText, extracted } = extractToolCallSections(original);
@@ -2953,32 +3006,31 @@ function toOpenAITool(tool) {
2953
3006
  }
2954
3007
  function toOpenAIMessages(messages) {
2955
3008
  return messages.map((msg) => {
3009
+ const reasoningBlocks = msg.content.filter((c) => c.type === "reasoning");
3010
+ const nonReasoningBlocks = msg.content.filter((c) => c.type !== "reasoning");
3011
+ const res = {
3012
+ role: msg.role === "assistant" ? "assistant" : msg.role,
3013
+ content: toOpenAIContent(nonReasoningBlocks)
3014
+ };
3015
+ if (reasoningBlocks.length > 0) {
3016
+ res.reasoning_content = reasoningBlocks.map((b) => b.text).join(`
3017
+
3018
+ `);
3019
+ }
2956
3020
  if (msg.role === "tool") {
2957
- return {
2958
- role: "tool",
2959
- tool_call_id: msg.tool_call_id,
2960
- content: toOpenAIContent(msg.content)
2961
- };
3021
+ res.tool_call_id = msg.tool_call_id;
2962
3022
  }
2963
3023
  if (msg.tool_calls) {
2964
- return {
2965
- role: msg.role,
2966
- content: toOpenAIContent(msg.content),
2967
- tool_calls: msg.tool_calls.map((call) => ({
2968
- id: call.id,
2969
- type: "function",
2970
- function: {
2971
- name: call.name,
2972
- arguments: call.arguments
2973
- }
2974
- }))
2975
- };
3024
+ res.tool_calls = msg.tool_calls.map((call) => ({
3025
+ id: call.id,
3026
+ type: "function",
3027
+ function: {
3028
+ name: call.name,
3029
+ arguments: call.arguments
3030
+ }
3031
+ }));
2976
3032
  }
2977
- return {
2978
- role: msg.role === "assistant" ? "assistant" : msg.role,
2979
- content: toOpenAIContent(msg.content),
2980
- name: undefined
2981
- };
3033
+ return res;
2982
3034
  });
2983
3035
  }
2984
3036
  function normalizeOpenAIProviderResponse(payload, request) {
@@ -3817,13 +3869,53 @@ class VertexProviderAdapter {
3817
3869
  toVertexTools(tools) {
3818
3870
  if (!tools?.length)
3819
3871
  return;
3872
+ function flattenJsonSchema(schema) {
3873
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
3874
+ return schema;
3875
+ }
3876
+ const obj = { ...schema };
3877
+ const definitions = obj.definitions;
3878
+ delete obj.definitions;
3879
+ function resolveRef(target, path4) {
3880
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
3881
+ return target;
3882
+ }
3883
+ const copy = { ...target };
3884
+ for (const key in copy) {
3885
+ const value = copy[key];
3886
+ if (key === "$ref" && typeof value === "string" && value.startsWith("#/definitions/")) {
3887
+ const defName = value.slice(14);
3888
+ const definition = definitions?.[defName];
3889
+ if (definition) {
3890
+ delete copy.$ref;
3891
+ const resolved = resolveRef(definition, [...path4, defName]);
3892
+ if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
3893
+ Object.assign(copy, resolved);
3894
+ }
3895
+ }
3896
+ } else if (key === "$ref" && typeof value === "string") {
3897
+ delete copy.$ref;
3898
+ } else {
3899
+ copy[key] = resolveRef(value, path4);
3900
+ }
3901
+ }
3902
+ return copy;
3903
+ }
3904
+ return resolveRef(obj, []);
3905
+ }
3820
3906
  return [
3821
3907
  {
3822
- functionDeclarations: tools.map((tool) => ({
3823
- name: tool.name,
3824
- description: tool.description,
3825
- parameters: tool.parameters
3826
- }))
3908
+ functionDeclarations: tools.map((tool) => {
3909
+ let parameters = tool.parameters;
3910
+ if (parameters && typeof parameters === "object" && !Array.isArray(parameters)) {
3911
+ parameters = flattenJsonSchema(parameters);
3912
+ }
3913
+ return {
3914
+ name: tool.name,
3915
+ description: tool.description,
3916
+ parameters
3917
+ };
3918
+ })
3827
3919
  }
3828
3920
  ];
3829
3921
  }
@@ -5471,16 +5563,16 @@ async function createLiveStoreRuntime(options) {
5471
5563
  const clauses = [];
5472
5564
  const params = { limit };
5473
5565
  if (checkpoint.timestamp) {
5474
- clauses.push("(timestamp > $ts OR (timestamp = $ts AND numeric_id > $id))");
5566
+ clauses.push("(timestamp < $ts OR (timestamp = $ts AND numeric_id < $id))");
5475
5567
  params.ts = checkpoint.timestamp;
5476
- params.id = checkpoint.id ?? 0;
5568
+ params.id = checkpoint.id ?? Number.MAX_SAFE_INTEGER;
5477
5569
  }
5478
5570
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
5479
5571
  const rows = store.query({
5480
5572
  query: `
5481
5573
  SELECT * FROM logs
5482
5574
  ${where}
5483
- ORDER BY timestamp, numeric_id
5575
+ ORDER BY timestamp DESC, numeric_id DESC
5484
5576
  LIMIT $limit
5485
5577
  `,
5486
5578
  bindValues: params
@@ -5501,6 +5593,29 @@ async function createLiveStoreRuntime(options) {
5501
5593
  return;
5502
5594
  return { timestamp: latest.timestamp, id: latest.numeric_id };
5503
5595
  },
5596
+ async trim(maxRecords) {
5597
+ const countResult = store.query({
5598
+ query: `SELECT COUNT(*) as count FROM logs`,
5599
+ bindValues: {}
5600
+ });
5601
+ const currentCount = countResult[0]?.count ?? 0;
5602
+ if (currentCount <= maxRecords)
5603
+ return 0;
5604
+ const toDelete = currentCount - maxRecords;
5605
+ const deleteResult = store.query({
5606
+ query: `
5607
+ DELETE FROM logs
5608
+ WHERE id IN (
5609
+ SELECT id FROM logs
5610
+ ORDER BY timestamp ASC, numeric_id ASC
5611
+ LIMIT @limit
5612
+ )
5613
+ RETURNING COUNT(*) as deleted
5614
+ `,
5615
+ bindValues: { limit: toDelete }
5616
+ });
5617
+ return deleteResult[0]?.deleted ?? 0;
5618
+ },
5504
5619
  async close() {
5505
5620
  await store.shutdown();
5506
5621
  }
@@ -5548,6 +5663,21 @@ async function createServer(config) {
5548
5663
  batchSize: config.livestore.batchSize
5549
5664
  });
5550
5665
  logger.info({ seeded }, "Seeded LiveStore log mirror");
5666
+ if (config.livestore.maxRecords && config.livestore.maxRecords > 0) {
5667
+ const trimInterval = setInterval(async () => {
5668
+ try {
5669
+ const deleted = await liveStoreRuntime.trim(config.livestore.maxRecords);
5670
+ if (deleted > 0) {
5671
+ logger.debug({ deleted, maxRecords: config.livestore.maxRecords }, "LiveStore trimmed old records");
5672
+ }
5673
+ } catch (error) {
5674
+ logger.error({ err: error }, "Failed to trim LiveStore records");
5675
+ }
5676
+ }, 30000);
5677
+ server.addHook("onClose", async () => {
5678
+ clearInterval(trimInterval);
5679
+ });
5680
+ }
5551
5681
  server.addHook("onClose", async () => {
5552
5682
  await liveStoreRuntime.close();
5553
5683
  });
@@ -5774,6 +5904,31 @@ async function createServer(config) {
5774
5904
  timestamp: meta.timestamp
5775
5905
  });
5776
5906
  });
5907
+ server.get("/api/config", (_req, reply) => {
5908
+ reply.send({
5909
+ blobRoot: config.logging.blobRoot
5910
+ });
5911
+ });
5912
+ server.get("/api/logs/:id/path", (req, reply) => {
5913
+ const { id } = req.params;
5914
+ const meta = logStore.readMetadata(Number(id));
5915
+ if (!meta) {
5916
+ reply.status(404).send({ error: { message: "Log not found" } });
5917
+ return;
5918
+ }
5919
+ const date = new Date(meta.timestamp);
5920
+ const year = date.getUTCFullYear();
5921
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
5922
+ const day = String(date.getUTCDate()).padStart(2, "0");
5923
+ const dirPath = `${config.logging.blobRoot}/${year}/${month}/${day}/${meta.request_id}/`;
5924
+ reply.send({
5925
+ directory: dirPath,
5926
+ request: meta.request_path ? `${config.logging.blobRoot}/${meta.request_path}` : null,
5927
+ response: meta.response_path ? `${config.logging.blobRoot}/${meta.response_path}` : null,
5928
+ providerRequest: meta.provider_request_path ? `${config.logging.blobRoot}/${meta.provider_request_path}` : null,
5929
+ providerResponse: meta.provider_response_path ? `${config.logging.blobRoot}/${meta.provider_response_path}` : null
5930
+ });
5931
+ });
5777
5932
  return server;
5778
5933
  }
5779
5934
  async function handleRequest(req, reply, body, modelRegistry, pipeline, logStore, liveStoreRuntime, config, options) {
@@ -6045,5 +6200,5 @@ async function bootstrap() {
6045
6200
  }
6046
6201
  bootstrap();
6047
6202
 
6048
- //# debugId=49E2D016D6E3465664756E2164756E21
6203
+ //# debugId=BB1E97A73BB51B4864756E2164756E21
6049
6204
  //# sourceMappingURL=index.js.map