@codefionn/llmleaf-client 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -25,6 +25,34 @@ export interface Usage {
25
25
  totalTokens: number;
26
26
  /** llmleaf addition; absent when the model has no known price. */
27
27
  costUsd?: number;
28
+ /**
29
+ * Prompt-cache hit accounting (OpenAI `usage.prompt_tokens_details`). Absent when the
30
+ * upstream reported no caching; {@link cachedTokens} flattens it to a plain count.
31
+ */
32
+ promptTokensDetails?: PromptTokensDetails;
33
+ /**
34
+ * Input tokens written to the provider's prompt cache this request — a cache *write*
35
+ * (creation). An llmleaf extension (Anthropic reports it; OpenAI/OpenRouter do not);
36
+ * absent when there were none.
37
+ */
38
+ cacheCreationTokens?: number;
39
+ }
40
+
41
+ /**
42
+ * Breakdown of {@link Usage.promptTokens}. Today only the cache-read (hit) share is
43
+ * surfaced — the count of prompt tokens served from the provider's cache rather than
44
+ * processed fresh.
45
+ */
46
+ export interface PromptTokensDetails {
47
+ cachedTokens?: number;
48
+ }
49
+
50
+ /**
51
+ * Prompt tokens served from the provider's cache this request — a cache *read* (hit).
52
+ * `0` when the upstream reported no caching.
53
+ */
54
+ export function cachedTokens(usage: Usage): number {
55
+ return usage.promptTokensDetails?.cachedTokens ?? 0;
28
56
  }
29
57
 
30
58
  // ---------------------------------------------------------------------------
@@ -63,6 +91,37 @@ export interface ToolCall {
63
91
  function: FunctionCall;
64
92
  }
65
93
 
94
+ /**
95
+ * One structured reasoning ("thinking") block (OpenRouter `reasoning_details[]`). It
96
+ * expresses both *open* reasoning — visible text, optionally signed — and *hidden*
97
+ * reasoning — an encrypted/redacted blob the provider returns in place of the text.
98
+ * `type` is the wire discriminator and selects which field is set:
99
+ *
100
+ * - `"reasoning.text"` → {@link text} (+ optional {@link signature}) — **open** (visible)
101
+ * - `"reasoning.summary"` → {@link summary} — **open** (a summarised view)
102
+ * - `"reasoning.encrypted"` → {@link data} — **hidden** (redacted / opaque)
103
+ *
104
+ * {@link signature} and {@link data} are opaque to the client and MUST be echoed back
105
+ * verbatim in the next request's `reasoning_details` to continue a signed/encrypted
106
+ * reasoning turn (the upstream rejects an altered or dropped block — e.g. before a tool
107
+ * call). {@link format} tags the provider encoding when known.
108
+ */
109
+ export interface ReasoningDetail {
110
+ type: string;
111
+ /** set for `"reasoning.text"` */
112
+ text?: string;
113
+ /** set for `"reasoning.summary"` */
114
+ summary?: string;
115
+ /** set for `"reasoning.encrypted"` (hidden) */
116
+ data?: string;
117
+ /** opaque, replayed verbatim */
118
+ signature?: string;
119
+ id?: string;
120
+ /** e.g. "anthropic-claude-v1" */
121
+ format?: string;
122
+ index?: number;
123
+ }
124
+
66
125
  export interface ChatMessage {
67
126
  role: Role;
68
127
  content?: MessageContent;
@@ -70,6 +129,17 @@ export interface ChatMessage {
70
129
  toolCalls?: ToolCall[];
71
130
  /** set when role == TOOL */
72
131
  toolCallId?: string;
132
+ /**
133
+ * Open reasoning text the assistant emitted (OpenRouter `reasoning`), if any. The flat,
134
+ * human-readable form; {@link reasoningDetails} is the structured, replay-safe one.
135
+ */
136
+ reasoning?: string;
137
+ /**
138
+ * Structured reasoning blocks (open and hidden, with signatures — see
139
+ * {@link ReasoningDetail}). Echo these back verbatim on the next request to preserve
140
+ * signed reasoning across a turn.
141
+ */
142
+ reasoningDetails?: ReasoningDetail[];
73
143
  }
74
144
 
75
145
  export interface FunctionDef {
@@ -161,6 +231,10 @@ export interface Delta {
161
231
  /** incremental text */
162
232
  content?: string;
163
233
  toolCalls?: ToolCallDelta[];
234
+ /** incremental open reasoning text, if any */
235
+ reasoning?: string;
236
+ /** incremental structured reasoning blocks (open / hidden — see {@link ReasoningDetail}). */
237
+ reasoningDetails?: ReasoningDetail[];
164
238
  }
165
239
 
166
240
  export interface ChunkChoice {
package/src/wire.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  ChatMessage,
17
17
  ContentPart,
18
18
  MessageContent,
19
+ ReasoningDetail,
19
20
  ToolCall,
20
21
  ToolDef,
21
22
  ToolChoice,
@@ -27,6 +28,7 @@ import type {
27
28
  Delta,
28
29
  ToolCallDelta,
29
30
  Usage,
31
+ PromptTokensDetails,
30
32
  EmbeddingRequest,
31
33
  EmbeddingResponse,
32
34
  Embedding,
@@ -124,6 +126,22 @@ function encodeToolCall(tc: ToolCall): Json {
124
126
  };
125
127
  }
126
128
 
129
+ /**
130
+ * Encode one reasoning_details[] entry. `signature` and `data` are opaque and must
131
+ * round-trip verbatim, so they are emitted exactly as given when present.
132
+ */
133
+ function encodeReasoningDetail(d: ReasoningDetail): Json {
134
+ const out: Json = { type: d.type };
135
+ put(out, "text", d.text);
136
+ put(out, "summary", d.summary);
137
+ put(out, "data", d.data);
138
+ put(out, "signature", d.signature);
139
+ put(out, "id", d.id);
140
+ put(out, "format", d.format);
141
+ put(out, "index", d.index);
142
+ return out;
143
+ }
144
+
127
145
  function encodeMessage(m: ChatMessage): Json {
128
146
  const out: Json = { role: roleToWire(m.role) ?? "user" };
129
147
  const content = encodeContent(m.content);
@@ -135,6 +153,12 @@ function encodeMessage(m: ChatMessage): Json {
135
153
  out["tool_calls"] = m.toolCalls.map(encodeToolCall);
136
154
  }
137
155
  put(out, "tool_call_id", m.toolCallId);
156
+ // Reasoning (OpenRouter): echo a prior assistant turn back verbatim to preserve
157
+ // signed/encrypted reasoning across a turn (SPEC.md / ReasoningDetail).
158
+ put(out, "reasoning", m.reasoning);
159
+ if (m.reasoningDetails && m.reasoningDetails.length > 0) {
160
+ out["reasoning_details"] = m.reasoningDetails.map(encodeReasoningDetail);
161
+ }
138
162
  return out;
139
163
  }
140
164
 
@@ -201,6 +225,15 @@ export function encodeChatRequest(req: ChatRequest, forceStream?: boolean): Json
201
225
  // Chat — decode
202
226
  // ===========================================================================
203
227
 
228
+ function decodePromptTokensDetails(v: unknown): PromptTokensDetails | undefined {
229
+ const o = obj(v);
230
+ if (!o) return undefined;
231
+ const details: PromptTokensDetails = {};
232
+ const cached = optNum(o["cached_tokens"]);
233
+ if (cached !== undefined) details.cachedTokens = cached;
234
+ return details;
235
+ }
236
+
204
237
  export function decodeUsage(v: unknown): Usage | undefined {
205
238
  const o = obj(v);
206
239
  if (!o) return undefined;
@@ -211,6 +244,10 @@ export function decodeUsage(v: unknown): Usage | undefined {
211
244
  };
212
245
  const cost = optNum(o["cost_usd"]);
213
246
  if (cost !== undefined) usage.costUsd = cost;
247
+ const ptd = decodePromptTokensDetails(o["prompt_tokens_details"]);
248
+ if (ptd !== undefined) usage.promptTokensDetails = ptd;
249
+ const cacheCreation = optNum(o["cache_creation_tokens"]);
250
+ if (cacheCreation !== undefined) usage.cacheCreationTokens = cacheCreation;
214
251
  return usage;
215
252
  }
216
253
 
@@ -249,6 +286,34 @@ function decodeToolCall(v: unknown): ToolCall | undefined {
249
286
  };
250
287
  }
251
288
 
289
+ function decodeReasoningDetail(v: unknown): ReasoningDetail | undefined {
290
+ const o = obj(v);
291
+ if (!o) return undefined;
292
+ const d: ReasoningDetail = { type: str(o["type"]) };
293
+ // `signature` and `data` are opaque blobs replayed verbatim — preserve them as-is.
294
+ const text = optStr(o["text"]);
295
+ if (text !== undefined) d.text = text;
296
+ const summary = optStr(o["summary"]);
297
+ if (summary !== undefined) d.summary = summary;
298
+ const data = optStr(o["data"]);
299
+ if (data !== undefined) d.data = data;
300
+ const signature = optStr(o["signature"]);
301
+ if (signature !== undefined) d.signature = signature;
302
+ const id = optStr(o["id"]);
303
+ if (id !== undefined) d.id = id;
304
+ const format = optStr(o["format"]);
305
+ if (format !== undefined) d.format = format;
306
+ const index = optNum(o["index"]);
307
+ if (index !== undefined) d.index = index;
308
+ return d;
309
+ }
310
+
311
+ function decodeReasoningDetails(v: unknown): ReasoningDetail[] {
312
+ return arr(v)
313
+ .map(decodeReasoningDetail)
314
+ .filter((x): x is ReasoningDetail => x !== undefined);
315
+ }
316
+
252
317
  export function decodeMessage(v: unknown): ChatMessage {
253
318
  const o = obj(v) ?? {};
254
319
  const msg: ChatMessage = { role: roleFromWire(optStr(o["role"])) };
@@ -262,6 +327,10 @@ export function decodeMessage(v: unknown): ChatMessage {
262
327
  if (tcs.length > 0) msg.toolCalls = tcs;
263
328
  const tcid = optStr(o["tool_call_id"]);
264
329
  if (tcid !== undefined) msg.toolCallId = tcid;
330
+ const reasoning = optStr(o["reasoning"]);
331
+ if (reasoning !== undefined) msg.reasoning = reasoning;
332
+ const rds = decodeReasoningDetails(o["reasoning_details"]);
333
+ if (rds.length > 0) msg.reasoningDetails = rds;
265
334
  return msg;
266
335
  }
267
336
 
@@ -320,6 +389,10 @@ function decodeDelta(v: unknown): Delta {
320
389
  .map(decodeToolCallDelta)
321
390
  .filter((x): x is ToolCallDelta => x !== undefined);
322
391
  if (tcs.length > 0) delta.toolCalls = tcs;
392
+ const reasoning = optStr(o["reasoning"]);
393
+ if (reasoning !== undefined) delta.reasoning = reasoning;
394
+ const rds = decodeReasoningDetails(o["reasoning_details"]);
395
+ if (rds.length > 0) delta.reasoningDetails = rds;
323
396
  return delta;
324
397
  }
325
398