@aeye/core 0.5.3 → 0.6.0

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 (117) hide show
  1. package/README.md +51 -4
  2. package/aeye-core-schema.md +11 -7
  3. package/dist/common.d.ts +30 -5
  4. package/dist/common.d.ts.map +1 -1
  5. package/dist/common.js +134 -129
  6. package/dist/common.js.map +1 -1
  7. package/dist/delivery.d.ts +39 -0
  8. package/dist/delivery.d.ts.map +1 -0
  9. package/dist/delivery.js +127 -0
  10. package/dist/delivery.js.map +1 -0
  11. package/dist/errors.d.ts +60 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +84 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/prompt.d.ts +101 -9
  20. package/dist/prompt.d.ts.map +1 -1
  21. package/dist/prompt.js +163 -141
  22. package/dist/prompt.js.map +1 -1
  23. package/dist/schema/analyze.d.ts +94 -0
  24. package/dist/schema/analyze.d.ts.map +1 -0
  25. package/dist/schema/analyze.js +221 -0
  26. package/dist/schema/analyze.js.map +1 -0
  27. package/dist/schema/any-value.d.ts +32 -0
  28. package/dist/schema/any-value.d.ts.map +1 -0
  29. package/dist/schema/any-value.js +77 -0
  30. package/dist/schema/any-value.js.map +1 -0
  31. package/dist/schema/budget.d.ts +114 -0
  32. package/dist/schema/budget.d.ts.map +1 -0
  33. package/dist/schema/budget.js +223 -0
  34. package/dist/schema/budget.js.map +1 -0
  35. package/dist/schema/consistency.d.ts +27 -0
  36. package/dist/schema/consistency.d.ts.map +1 -0
  37. package/dist/schema/consistency.js +110 -0
  38. package/dist/schema/consistency.js.map +1 -0
  39. package/dist/schema/cycle-breaker.d.ts +14 -0
  40. package/dist/schema/cycle-breaker.d.ts.map +1 -0
  41. package/dist/schema/cycle-breaker.js +157 -0
  42. package/dist/schema/cycle-breaker.js.map +1 -0
  43. package/dist/schema/decode.d.ts +31 -0
  44. package/dist/schema/decode.d.ts.map +1 -0
  45. package/dist/schema/decode.js +207 -0
  46. package/dist/schema/decode.js.map +1 -0
  47. package/dist/schema/descriptor.d.ts +300 -0
  48. package/dist/schema/descriptor.d.ts.map +1 -0
  49. package/dist/schema/descriptor.js +15 -0
  50. package/dist/schema/descriptor.js.map +1 -0
  51. package/dist/schema/dialects/anthropic.d.ts +34 -0
  52. package/dist/schema/dialects/anthropic.d.ts.map +1 -0
  53. package/dist/schema/dialects/anthropic.js +77 -0
  54. package/dist/schema/dialects/anthropic.js.map +1 -0
  55. package/dist/schema/dialects/google.d.ts +47 -0
  56. package/dist/schema/dialects/google.d.ts.map +1 -0
  57. package/dist/schema/dialects/google.js +91 -0
  58. package/dist/schema/dialects/google.js.map +1 -0
  59. package/dist/schema/dialects/lenient.d.ts +4 -0
  60. package/dist/schema/dialects/lenient.d.ts.map +1 -0
  61. package/dist/schema/dialects/lenient.js +26 -0
  62. package/dist/schema/dialects/lenient.js.map +1 -0
  63. package/dist/schema/dialects/openai.d.ts +40 -0
  64. package/dist/schema/dialects/openai.d.ts.map +1 -0
  65. package/dist/schema/dialects/openai.js +81 -0
  66. package/dist/schema/dialects/openai.js.map +1 -0
  67. package/dist/schema/dialects/xai.d.ts +23 -0
  68. package/dist/schema/dialects/xai.d.ts.map +1 -0
  69. package/dist/schema/dialects/xai.js +53 -0
  70. package/dist/schema/dialects/xai.js.map +1 -0
  71. package/dist/schema/enum-values.d.ts +44 -0
  72. package/dist/schema/enum-values.d.ts.map +1 -0
  73. package/dist/schema/enum-values.js +56 -0
  74. package/dist/schema/enum-values.js.map +1 -0
  75. package/dist/schema/expressible.d.ts +59 -0
  76. package/dist/schema/expressible.d.ts.map +1 -0
  77. package/dist/schema/expressible.js +161 -0
  78. package/dist/schema/expressible.js.map +1 -0
  79. package/dist/schema/index.d.ts +20 -0
  80. package/dist/schema/index.d.ts.map +1 -0
  81. package/dist/schema/index.js +20 -0
  82. package/dist/schema/index.js.map +1 -0
  83. package/dist/schema/json-schema.d.ts +64 -0
  84. package/dist/schema/json-schema.d.ts.map +1 -0
  85. package/dist/schema/json-schema.js +2 -0
  86. package/dist/schema/json-schema.js.map +1 -0
  87. package/dist/schema/registry.d.ts +73 -0
  88. package/dist/schema/registry.d.ts.map +1 -0
  89. package/dist/schema/registry.js +135 -0
  90. package/dist/schema/registry.js.map +1 -0
  91. package/dist/schema/strictify.d.ts +19 -0
  92. package/dist/schema/strictify.d.ts.map +1 -0
  93. package/dist/schema/strictify.js +237 -0
  94. package/dist/schema/strictify.js.map +1 -0
  95. package/dist/schema/to-json-schema.d.ts +29 -0
  96. package/dist/schema/to-json-schema.d.ts.map +1 -0
  97. package/dist/schema/to-json-schema.js +691 -0
  98. package/dist/schema/to-json-schema.js.map +1 -0
  99. package/dist/schema/walk-key.d.ts +24 -0
  100. package/dist/schema/walk-key.d.ts.map +1 -0
  101. package/dist/schema/walk-key.js +33 -0
  102. package/dist/schema/walk-key.js.map +1 -0
  103. package/dist/to.js +1 -1
  104. package/dist/to.js.map +1 -1
  105. package/dist/tool.d.ts +32 -3
  106. package/dist/tool.d.ts.map +1 -1
  107. package/dist/tool.js +41 -0
  108. package/dist/tool.js.map +1 -1
  109. package/dist/types.d.ts +265 -8
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +4 -1
  112. package/dist/types.js.map +1 -1
  113. package/package.json +72 -72
  114. package/dist/schema.d.ts +0 -837
  115. package/dist/schema.d.ts.map +0 -1
  116. package/dist/schema.js +0 -2472
  117. package/dist/schema.js.map +0 -1
package/README.md CHANGED
@@ -408,6 +408,42 @@ const adaptivePrompt = new Prompt({
408
408
  });
409
409
  ```
410
410
 
411
+ ## Schema Fallbacks
412
+
413
+ A provider that rejects a schema throws `SchemaRejectedError` with a `target` naming the output or the tool. The prompt then steps down its `schemaFallbacks`, by default `SCHEMA_FALLBACKS`: the rejected schema goes non-strict, then the output moves to JSON mode with its schema in the prompt (`schemaDelivery: 'json'`), then to plain text with its schema in the prompt (`'prompt'`). Steps stay on the request for the rest of the run, and each emits a `schemaFallback` event.
414
+
415
+ ```typescript
416
+ import { Prompt, SCHEMA_FALLBACKS } from '@aeye/core';
417
+
418
+ const prompt = new Prompt({
419
+ name: 'extract',
420
+ description: 'Extracts fields',
421
+ content: 'Extract the fields from: {{text}}',
422
+ schema,
423
+ schemaFallbacks: SCHEMA_FALLBACKS.filter(step => step.name !== 'json'),
424
+ });
425
+ ```
426
+
427
+ Providers use `schemaRejection(request, error)` to classify a bad request and `deliverSchema(request)` to send a `json` or `prompt` schema without changing the prompt's request.
428
+
429
+ ## Error Kinds
430
+
431
+ Every prompt failure has `kind: 'model' | 'tool'`. `ToolInputError` (an unknown tool or bad arguments) and `OutputValidationError` (output that isn't JSON or fails the schema or `validate`) are model mistakes, fed back to the model for another try. `ToolExecutionError` is a tool's own throw. They appear on the `toolError` event's `failure`, on tool result messages' `failure`, and on the `textReset` event's `error`. A prompt that gives up throws a `PromptError` whose `cause` is the last failure.
432
+
433
+ ## Review
434
+
435
+ `review` judges the parsed, validated output before it's returned. Feedback becomes a user message and the model answers again, up to `reviewRetries` times; after that the prompt throws a `PromptError` caused by a `ReviewRejectedError` with the last output.
436
+
437
+ ```typescript
438
+ const answer = new Prompt({
439
+ name: 'answer',
440
+ description: 'Answers briefly',
441
+ content: 'Answer in under 20 words: {{question}}',
442
+ input: (input: { question: string }) => input,
443
+ review: (output) => output.split(' ').length < 20 ? { pass: true } : { feedback: 'Use fewer than 20 words.' },
444
+ });
445
+ ```
446
+
411
447
  ## Model Capabilities
412
448
 
413
449
  @aeye uses a capability system for automatic model selection. Models advertise their capabilities and the library selects the best match for each request:
@@ -421,6 +457,8 @@ const adaptivePrompt = new Prompt({
421
457
  | `audio` | Text-to-speech |
422
458
  | `hearing` | Speech-to-text |
423
459
  | `embedding` | Text embeddings |
460
+ | `rerank` | Ordering documents by relevance |
461
+ | `decision` | Typed answers (probabilities, choices, scores) from decision models |
424
462
  | `tools` | Function/tool calling |
425
463
  | `json` | JSON output mode |
426
464
  | `structured` | Structured outputs with schemas |
@@ -541,7 +579,10 @@ class Prompt<TContext, TMetadata, TName extends string, TInput extends object, T
541
579
  - `content: string` - Handlebars template for the system/user message
542
580
  - `input?: (input: TInput, ctx: Context<TContext, TMetadata>) => Record<string, any>` - Maps raw input to template variables
543
581
  - `schema?: ZodType<TOutput> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => ZodType<TOutput> | false)` - Output schema; `false` disables structured output
544
- - `strict?: boolean` - Enforce strict Zod schema (default: `true`)
582
+ - `strict?: boolean | number` - Strict tool inputs: `true` requires, `false` disables, a number is a preference priority (default: `1`)
583
+ - `format?: ToolTextFormat` - Take free text (`{ type: 'text' }`) or grammar-constrained text (`{ type: 'grammar', syntax: 'lark' | 'regex', definition }`) as `input` instead of JSON; use `TEXT_TOOL_SCHEMA` as the schema
584
+ - `defer?: boolean` - Load only when the model searches for it (OpenAI tool search)
585
+ - `async?: boolean` - The model keeps working while the tool runs (OpenAI Responses)
545
586
  - `config?: Partial<Request> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => Partial<Request> | false)` - AI request overrides
546
587
  - `tools?: TTools` - Tools the prompt may call
547
588
  - `toolExecution?: 'sequential' | 'parallel' | 'immediate'` - Tool execution mode
@@ -549,8 +590,12 @@ class Prompt<TContext, TMetadata, TName extends string, TInput extends object, T
549
590
  - `toolIterations?: number` - Max tool-calling iterations (default: 3)
550
591
  - `toolsMax?: number` - Max successful tool calls
551
592
  - `outputRetries?: number` - Retry count for invalid structured outputs
593
+ - `schemaDelivery?: 'auto' | 'structured' | 'json' | 'prompt'` - How the output schema reaches the model (default: `'auto'`)
594
+ - `schemaFallbacks?: readonly SchemaFallback[]` - Steps taken when a provider rejects a schema (default: the context's, then `SCHEMA_FALLBACKS`)
552
595
  - `metadata?: TMetadata` - Static metadata for this prompt
553
596
  - `validate?: (output: TOutput, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
597
+ - `review?: (output: TDecoded, context: PromptReviewContext) => PromptReview | Promise<PromptReview>` - Review the valid output: `{ pass: true }` or `{ feedback }` for another round
598
+ - `reviewRetries?: number` - Rounds the model gets to answer review feedback (default: 1)
554
599
  - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
555
600
 
556
601
  **Methods:**
@@ -626,6 +671,8 @@ interface CoreContextFields {
626
671
  outputRetries?: number;
627
672
  // Number of context-trimming retries (default: 1)
628
673
  forgetRetries?: number;
674
+ // Steps a prompt takes when a provider rejects a schema (default: SCHEMA_FALLBACKS)
675
+ schemaFallbacks?: readonly SchemaFallback[];
629
676
 
630
677
  // Abort signal for cancellation
631
678
  signal?: AbortSignal;
@@ -644,9 +691,9 @@ interface Request {
644
691
  maxTokens?: number;
645
692
  stop?: string | string[];
646
693
  tools?: ToolDefinition[];
647
- toolChoice?: 'auto' | 'required' | 'none' | { type: 'tool'; name: string };
648
- responseFormat?: 'text' | 'json' | { type: 'json_schema'; schema: object; name: string; strict?: boolean };
649
- reason?: { effort?: 'low' | 'medium' | 'high'; maxTokens?: number };
694
+ toolChoice?: 'auto' | 'required' | 'none' | { tool: string } | { allowed: string[]; required?: boolean };
695
+ responseFormat?: 'text' | 'json' | { type: ZodType; strict: boolean | number };
696
+ reason?: { effort?: ReasoningEffort; maxTokens?: number; summary?: 'auto' | 'concise' | 'detailed'; exclude?: boolean };
650
697
  frequencyPenalty?: number;
651
698
  presencePenalty?: number;
652
699
  seed?: number;
@@ -35,7 +35,7 @@ A descriptor (see `src/schema.ts` line ~26 for the fully documented interface) d
35
35
  | `supportsRecursion` | Whether `$ref` self-reference works. |
36
36
  | `maxStrictTools` / `maxStrictOptionalParams` / `maxStrictUnionTypes` | Per-request slot budgets (`undefined` = no documented limit). |
37
37
  | `schemaSizeLimits` | Per-SCHEMA size ceilings — properties, nesting, total string characters, enum values (`undefined` = no documented limit) — see below. |
38
- | `maxEnumValues` | Opt-in ceiling on how many values ONE emitted `enum` may carry (`undefined` = no cap, and that is what every built-in declares) — see below. |
38
+ | `maxEnumValues` | Opt-in ceiling on how many values ONE emitted `enum` may carry (`undefined` = no cap). `GOOGLE_STRICT`/`GOOGLE_NON_STRICT` declare 40, a MEASURED response to a real provider rejection; every other built-in declares none — see below. |
39
39
 
40
40
  ### `schemaSizeLimits` — how BIG one strict schema may be
41
41
 
@@ -60,7 +60,7 @@ Scope is Structured Outputs — a tool carrying `strict: true`, or a `json_schem
60
60
 
61
61
  The measurement is taken from the **Zod** schema rather than from the emitted JSON, so where an encoding is descriptor-dependent it counts the widest one (a record as its `{key, value}` pair form, a tuple as its numeric-key form). Over-counting costs strictness on a schema that would have fitted; under-counting costs an HTTP 400, so the rounding goes up. `$defs` entry names are not counted: they are generated at emit time, few, and short against a 120,000-character budget.
62
62
 
63
- ### `maxEnumValues` — an opt-in dialect ceiling that WIDENS, and why no built-in sets one
63
+ ### `maxEnumValues` — an opt-in dialect ceiling that WIDENS, and why only Google sets one
64
64
 
65
65
  Some providers may reject a schema whose `enum` is simply too long. That is **not** one of the per-request budgets above: those are strict-mode allowances the `SchemaBudget` spends across a whole request, degrading an item that no longer fits to the family's non-strict descriptor, whereas this one would fire whatever the strictness, so it is enforced structurally at the point an `enum` is emitted.
66
66
 
@@ -75,15 +75,19 @@ Truncating would make the values past the cap **unreachable** — the wire schem
75
75
 
76
76
  The trade is enforcement: a widened node no longer rejects a value structurally, so whatever validates the model's output afterwards must. `Tool.parse` does — it runs against the ORIGINAL zod schema, whose `enum` is untouched — so a hallucinated member comes back as a normal, re-promptable validation error.
77
77
 
78
- **No built-in descriptor declares a cap, and that is a measured result.** The field exists because a large `enum` was believed to make Gemini answer `400 INVALID_ARGUMENT`. Measured against `google/gemini-3-flash-preview` through OpenRouter (2026-08-23), it does not: single-parameter enums at 50/64/72/80/96/128/256/512/1024/**2048** values (52 KB of tool schema) all returned HTTP 200 with the expected tool call, under `tool_choice` both `required` (which is what makes Gemini compile a decoding grammar) and `auto` — as did 96 values with 80-character members, 20 tools each carrying a 96-value enum in one request, a 96-value enum next to `strict: true` / `minItems` / `propertyOrdering` / a sibling `anyOf`, and a 2048-value enum inside a `response_format` schema. Reliability was compared too, since a cap's whole cost is losing the enum's can't-hallucinate guarantee: 10 trials each of `enum` vs plain-string-with-full-list, over confusable near-duplicate names, at 96 and again at 512 values — both picked the correct member 10/10 with zero off-list answers.
78
+ **`GOOGLE_STRICT`/`GOOGLE_NON_STRICT` declare `maxEnumValues: 40`. Every other built-in declares no cap.** The field exists because a large `enum` was believed to make Gemini answer `400 INVALID_ARGUMENT`. An initial round of measurement against `google/gemini-3-flash-preview` through OpenRouter (2026-08-23) looked like it disproved that: single-parameter enums at 50/64/72/80/96/128/256/512/1024/**2048** SYNTHETIC values (`name_0`, `name_1`, …) all returned HTTP 200, as did an eleven-tool roster modelled on a real kind-authoring agent with a 96-value enum on one tool.
79
79
 
80
- A second pass answered the obvious objection — that an isolated probe is not what a real session sends. Same date and model, upstream **pinned to Google AI Studio** with fallbacks off (the upstream that served the production failures), an eleven-tool roster emitted through this library for a Google model and modelled on a real kind-authoring agent (`fn_load` / `fn_search` / `type_load` / `api_signature` / `api_set` / …, real descriptions, real system prompt): a 96-value enum on one of those tools returned 200 with the expected tool call under `tool_choice` `required` and `auto` alike, as did the same roster at 8 values and a tool carrying a bare typeless `{}` property.
80
+ **That conclusion was wrong, because none of it used the CONTENT of a real session.** A third round replayed the exact wire bytes of an actual product request — `fn_load`'s `names` array, enum-constrained to a real session's 98 known component/fn names (`default_renderer`, `QueryWidget`, `ConfirmPrompt`, `list_widget_types`, …) — and it 400'd, reproducibly, twice, byte-for-byte identical to what was actually sent. Bisecting that real value list: 90 values passes, 93 fails. Bisecting what differs from the earlier synthetic probes: 98 SHORT synthetic values (284 total characters) pass; 206 short values matching the real list's total character count (714 characters) also pass; only the real, longer, identifier-shaped values fail, and only past ~92 of them. So the limit tracks neither raw member count nor total character/byte budget — most likely actual tokenized grammar-state count, which short repetitive synthetic strings compress far below what real identifier text does at the same count or character budget. This is why the first two rounds' synthetic and lightly-realistic probes never tripped it.
81
81
 
82
- The caveat that keeps that from being proof: the **control** — the recursive `$defs/Any` shape documented elsewhere in this file as 100% reproducible HTTP 400 — also returned 200, pinned and unpinned, forced and automatic, both hand-built and as the product's real 32 KB gin `api_set` schema. That failure class is not visible from this route today, so treat the numbers as evidence, not a verdict.
82
+ The widen-to-string fallback this field triggers was verified against the exact failing real schema: dropping `enum` and putting the same 98 values into the `description` of a plain string field returned HTTP 200 with the expected tool call — confirming the fallback mechanism actually fixes the failure it exists for, not just a synthetic stand-in.
83
83
 
84
- One trap for whoever measures this next: at `max_tokens: 64` the `$defs/Any` tool answered `finish_reason: "length"` with no tool call and empty content, which reads exactly like a grammar that produced nothing. It is not — this model spends reasoning tokens first, and at 400 the identical request emitted a correct call in 28 output tokens. Give a forced-tool probe room before calling an empty completion a failure.
84
+ `40` is not a measured edge — it is a conservative floor with roughly 2x margin under the real one (90 passes / 93 fails) measured on this one real value set. Given the failure tracks content, not just count, a different session's actual name list could plausibly fail at a different count; 40 was also the cap the product that surfaced this ran under in production for months, which is corroborating evidence it holds in practice, not just in this one measurement.
85
85
 
86
- So: **set this only against a measured provider rejection, and set it as high as the measurement allows.** An `enum` that fits is the more correct encoding, because it is the one the model cannot answer outside of.
86
+ The reliability comparison from the earlier rounds still stands and is why a cap is preferable to leaving every enum uncapped "to be safe" at a much lower number: `enum` vs plain-string-with-full-list, 10 trials each over confusable near-duplicate names at 96 and 512 values, scored 10/10 on-target with zero off-list answers for BOTH encodings. So there is no reliability cost to raising this cap later if a higher safe value is measured — only downside risk to leaving it too high.
87
+
88
+ One trap for whoever measures this next: with `max_tokens` at 64, a forced-tool probe can come back `finish_reason: "length"` with no tool call and empty content, which reads exactly like a grammar that produced nothing. It usually is not — this model spends reasoning tokens first. Give a forced-tool probe room before calling an empty completion a failure.
89
+
90
+ So: **set this only where a provider is MEASURED to reject a large enum using the SESSION'S OWN real content, not synthetic stand-ins, and set it conservatively below the measured edge until more value sets are tested.** An `enum` that fits is the more correct encoding, because it is the one the model cannot answer outside of.
87
91
 
88
92
  ### `anyEncoding` — and what each mode requires
89
93
 
package/dist/common.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Chunk, Events, Model, ModelInput, Reasoning, Response, Runner, Usage } from "./types";
1
+ import { Chunk, Events, GeneratedAudio, Model, ModelInput, Reasoning, Response, Runner, Usage } from "./types";
2
2
  /**
3
3
  * A flexible function type that can be:
4
4
  * - A direct value of type R
@@ -145,6 +145,14 @@ export declare function getModel(input: ModelInput | undefined): Model | undefin
145
145
  * @returns The aggregated Response object.
146
146
  */
147
147
  export declare function getResponseFromChunks(chunks: Chunk[], model?: ModelInput): Response;
148
+ /**
149
+ * Appends a partial audio chunk onto accumulated audio.
150
+ *
151
+ * @param target - The audio accumulated so far.
152
+ * @param add - The partial audio to append.
153
+ * @returns The combined audio.
154
+ */
155
+ export declare function accumulateAudio(target: GeneratedAudio | undefined, add: GeneratedAudio): GeneratedAudio;
148
156
  /**
149
157
  * Generates a plain text representation of reasoning data.
150
158
  *
@@ -153,13 +161,30 @@ export declare function getResponseFromChunks(chunks: Chunk[], model?: ModelInpu
153
161
  */
154
162
  export declare function getReasoningText(reasoning: Reasoning | undefined): string;
155
163
  /**
156
- * Accumulates reasoning data by merging an additional Reasoning object into a target Reasoning object.
164
+ * Accumulates streamed reasoning: the reasoning so far with the next part
165
+ * appended, as a new object that shares nothing mutable with either input.
166
+ *
167
+ * Providers stream reasoning as deltas, never repeating what was sent, so
168
+ * `content` and each detail's `text`, `summary` and `data` concatenate. A
169
+ * detail continues an earlier one of the same type with the same id (when
170
+ * both have one) or index: OpenRouter sends a block's text and later its
171
+ * signature at one index, and Gemini's text and encrypted reasoning share
172
+ * index 0 under different types. The provider is whichever part names it.
157
173
  *
158
- * @param target - The target Reasoning object to accumulate into.
159
- * @param add - The Reasoning object to add from.
160
- * @returns - The updated Reasoning object.
174
+ * @param target - The reasoning accumulated so far.
175
+ * @param add - The reasoning to append.
176
+ * @returns - The accumulated reasoning.
161
177
  */
162
178
  export declare function accumulateReasoning(target: Reasoning | undefined, add?: Reasoning): Reasoning | undefined;
179
+ /**
180
+ * Whether reasoning goes back to a provider: it came from there, or names no
181
+ * provider. Signed and encrypted reasoning only verifies where it was made.
182
+ *
183
+ * @param reasoning - The reasoning of an earlier assistant turn.
184
+ * @param provider - The provider the conversation goes to.
185
+ * @returns - Whether to replay the reasoning.
186
+ */
187
+ export declare function replaysTo(reasoning: Reasoning | undefined, provider: string): reasoning is Reasoning;
163
188
  /**
164
189
  * Converts a Response object into an array of Chunk objects.
165
190
  *
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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"}
package/dist/common.js CHANGED
@@ -127,6 +127,22 @@ export async function consumeAll(gen) {
127
127
  }
128
128
  return results;
129
129
  }
130
+ /**
131
+ * Adds every count in `add` onto `target`, treating a missing count as zero.
132
+ *
133
+ * @param target - The counts to add into (mutated).
134
+ * @param add - The counts to add.
135
+ * @returns The target.
136
+ */
137
+ function sumCounts(target, add) {
138
+ for (const key in add) {
139
+ const value = add[key];
140
+ if (value !== undefined) {
141
+ target[key] = (target[key] ?? 0) + value;
142
+ }
143
+ }
144
+ return target;
145
+ }
130
146
  /**
131
147
  * Accumulates usage statistics by adding values from one Usage object to another.
132
148
  * Handles the nested structure matching ModelPricing.
@@ -139,92 +155,40 @@ export function accumulateUsage(target, add) {
139
155
  if (!add) {
140
156
  return;
141
157
  }
142
- // Accumulate text usage
143
- if (add.text) {
144
- if (!target.text) {
145
- target.text = {};
146
- }
147
- if (add.text.input !== undefined) {
148
- target.text.input = (target.text.input || 0) + add.text.input;
149
- }
150
- if (add.text.output !== undefined) {
151
- target.text.output = (target.text.output || 0) + add.text.output;
152
- }
153
- if (add.text.cached !== undefined) {
154
- target.text.cached = (target.text.cached || 0) + add.text.cached;
155
- }
156
- }
157
- // Accumulate audio usage
158
- if (add.audio) {
159
- if (!target.audio) {
160
- target.audio = {};
161
- }
162
- if (add.audio.input !== undefined) {
163
- target.audio.input = (target.audio.input || 0) + add.audio.input;
164
- }
165
- if (add.audio.output !== undefined) {
166
- target.audio.output = (target.audio.output || 0) + add.audio.output;
167
- }
168
- if (add.audio.seconds !== undefined) {
169
- target.audio.seconds = (target.audio.seconds || 0) + add.audio.seconds;
170
- }
171
- }
172
- // Accumulate image usage
158
+ if (add.text)
159
+ target.text = sumCounts(target.text ?? {}, add.text);
160
+ if (add.audio)
161
+ target.audio = sumCounts(target.audio ?? {}, add.audio);
162
+ if (add.reasoning)
163
+ target.reasoning = sumCounts(target.reasoning ?? {}, add.reasoning);
164
+ if (add.embeddings)
165
+ target.embeddings = sumCounts(target.embeddings ?? {}, add.embeddings);
166
+ if (add.tools)
167
+ target.tools = sumCounts(target.tools ?? {}, add.tools);
168
+ // Image input is a count; image output merges by quality and size
173
169
  if (add.image) {
174
- if (!target.image) {
175
- target.image = { output: [] };
176
- }
170
+ const image = target.image ?? (target.image = {});
177
171
  if (add.image.input !== undefined) {
178
- target.image.input = (target.image.input || 0) + add.image.input;
172
+ image.input = (image.input ?? 0) + add.image.input;
173
+ }
174
+ if (add.image.tokens !== undefined) {
175
+ image.tokens = (image.tokens ?? 0) + add.image.tokens;
179
176
  }
180
- if (add.image.output) {
181
- if (!target.image.output) {
182
- target.image.output = [];
177
+ for (const addOutput of add.image.output ?? []) {
178
+ const outputs = image.output ?? (image.output = []);
179
+ const existing = outputs.find(o => o.quality === addOutput.quality &&
180
+ o.size.width === addOutput.size.width &&
181
+ o.size.height === addOutput.size.height);
182
+ if (existing) {
183
+ existing.count += addOutput.count;
183
184
  }
184
- // Merge image outputs by quality and size
185
- for (const addOutput of add.image.output) {
186
- const existing = target.image.output.find(o => o.quality === addOutput.quality &&
187
- o.size.width === addOutput.size.width &&
188
- o.size.height === addOutput.size.height);
189
- if (existing) {
190
- existing.count += addOutput.count;
191
- }
192
- else {
193
- target.image.output.push({ ...addOutput });
194
- }
185
+ else {
186
+ outputs.push({ ...addOutput });
195
187
  }
196
188
  }
197
189
  }
198
- // Accumulate reasoning usage
199
- if (add.reasoning) {
200
- if (!target.reasoning) {
201
- target.reasoning = {};
202
- }
203
- if (add.reasoning.input !== undefined) {
204
- target.reasoning.input = (target.reasoning.input || 0) + add.reasoning.input;
205
- }
206
- if (add.reasoning.output !== undefined) {
207
- target.reasoning.output = (target.reasoning.output || 0) + add.reasoning.output;
208
- }
209
- if (add.reasoning.cached !== undefined) {
210
- target.reasoning.cached = (target.reasoning.cached || 0) + add.reasoning.cached;
211
- }
212
- }
213
- // Accumulate embeddings usage
214
- if (add.embeddings) {
215
- if (!target.embeddings) {
216
- target.embeddings = {};
217
- }
218
- if (add.embeddings.count !== undefined) {
219
- target.embeddings.count = (target.embeddings.count || 0) + add.embeddings.count;
220
- }
221
- if (add.embeddings.tokens !== undefined) {
222
- target.embeddings.tokens = (target.embeddings.tokens || 0) + add.embeddings.tokens;
223
- }
224
- }
225
- // Accumulate cost
226
190
  if (add.cost !== undefined) {
227
- target.cost = (target.cost || 0) + add.cost;
191
+ target.cost = (target.cost ?? 0) + add.cost;
228
192
  }
229
193
  }
230
194
  /**
@@ -309,7 +273,7 @@ export function getResponseFromChunks(chunks, model = 'unknown') {
309
273
  resp.model = chunk.model;
310
274
  }
311
275
  if (chunk.refusal) {
312
- resp.refusal = chunk.refusal;
276
+ resp.refusal = (resp.refusal ?? '') + chunk.refusal;
313
277
  }
314
278
  if (chunk.toolCall) {
315
279
  resp.toolCalls = resp.toolCalls || [];
@@ -319,9 +283,48 @@ export function getResponseFromChunks(chunks, model = 'unknown') {
319
283
  resp.usage = resp.usage || {};
320
284
  accumulateUsage(resp.usage, chunk.usage);
321
285
  }
286
+ if (chunk.citations) {
287
+ resp.citations = [...resp.citations ?? [], ...chunk.citations];
288
+ }
289
+ if (chunk.images) {
290
+ resp.images = [...resp.images ?? [], ...chunk.images];
291
+ }
292
+ if (chunk.audio) {
293
+ resp.audio = accumulateAudio(resp.audio, chunk.audio);
294
+ }
295
+ if (chunk.moderation) {
296
+ resp.moderation = { ...resp.moderation, ...chunk.moderation };
297
+ }
298
+ if (chunk.serviceTier) {
299
+ resp.serviceTier = chunk.serviceTier;
300
+ }
322
301
  }
323
302
  return resp;
324
303
  }
304
+ /**
305
+ * The base64 encoding of two base64 strings' bytes joined. Each streamed part
306
+ * is encoded on its own, so a padded last block of the first is re-encoded
307
+ * with the bytes that follow.
308
+ */
309
+ function joinBase64(x, y) {
310
+ return x.endsWith('=') ? x.slice(0, -4) + btoa(atob(x.slice(-4)) + atob(y)) : x + y;
311
+ }
312
+ /**
313
+ * Appends a partial audio chunk onto accumulated audio.
314
+ *
315
+ * @param target - The audio accumulated so far.
316
+ * @param add - The partial audio to append.
317
+ * @returns The combined audio.
318
+ */
319
+ export function accumulateAudio(target, add) {
320
+ const join = (x, y, by) => x === undefined || y === undefined ? x ?? y : by(x, y);
321
+ return {
322
+ id: add.id ?? target?.id,
323
+ format: add.format ?? target?.format,
324
+ data: join(target?.data, add.data, joinBase64),
325
+ transcript: join(target?.transcript, add.transcript, (x, y) => x + y),
326
+ };
327
+ }
325
328
  /**
326
329
  * Generates a plain text representation of reasoning data.
327
330
  *
@@ -348,66 +351,63 @@ export function getReasoningText(reasoning) {
348
351
  }
349
352
  return parts.join('\n\n');
350
353
  }
351
- // Only appends y to x if neither contains the other
352
- function appendMaybe(x, y) {
353
- if (!x) {
354
- return y;
355
- }
356
- if (!y) {
357
- return x;
358
- }
359
- if (!x.includes(y) && !y.includes(x)) {
360
- return x + y;
361
- }
362
- return x;
354
+ /** Two streamed pieces of text joined, either of which may be missing. */
355
+ function concat(x, y) {
356
+ return x && y ? x + y : x || y || undefined;
363
357
  }
364
358
  /**
365
- * Accumulates reasoning data by merging an additional Reasoning object into a target Reasoning object.
359
+ * Accumulates streamed reasoning: the reasoning so far with the next part
360
+ * appended, as a new object that shares nothing mutable with either input.
361
+ *
362
+ * Providers stream reasoning as deltas, never repeating what was sent, so
363
+ * `content` and each detail's `text`, `summary` and `data` concatenate. A
364
+ * detail continues an earlier one of the same type with the same id (when
365
+ * both have one) or index: OpenRouter sends a block's text and later its
366
+ * signature at one index, and Gemini's text and encrypted reasoning share
367
+ * index 0 under different types. The provider is whichever part names it.
366
368
  *
367
- * @param target - The target Reasoning object to accumulate into.
368
- * @param add - The Reasoning object to add from.
369
- * @returns - The updated Reasoning object.
369
+ * @param target - The reasoning accumulated so far.
370
+ * @param add - The reasoning to append.
371
+ * @returns - The accumulated reasoning.
370
372
  */
371
373
  export function accumulateReasoning(target, add) {
372
- if (!target) {
373
- return add;
374
- }
375
374
  if (!add) {
376
375
  return target;
377
376
  }
378
- if (add.content) {
379
- target.content = appendMaybe(target.content || undefined, add.content);
380
- }
381
- if (add.details?.length) {
382
- target.details = target.details || [];
383
- for (const detail of add.details) {
384
- let existing = target.details.find(d => (detail.id && d.id && detail.id === d.id) ||
385
- (detail.index !== undefined && d.index !== undefined && detail.index === d.index));
386
- const missingIdentifiers = add.details.every(d => !d.id && d.index === undefined) &&
387
- target.details.every(d => !d.id && d.index === undefined);
388
- if (missingIdentifiers) {
389
- existing = target.details.find(d => d.type === detail.type);
390
- }
391
- if (existing) {
392
- if (detail.data) {
393
- existing.data = appendMaybe(existing.data, detail.data);
394
- }
395
- if (detail.text) {
396
- existing.text = appendMaybe(existing.text, detail.text);
397
- if (detail.signature) {
398
- existing.signature = detail.signature;
399
- }
400
- }
401
- if (detail.summary) {
402
- existing.summary = appendMaybe(existing.summary, detail.summary);
403
- }
404
- }
405
- else {
406
- target.details.push(detail);
407
- }
377
+ const details = [...target?.details ?? []];
378
+ for (const detail of add.details ?? []) {
379
+ const at = details.findIndex(d => d.type === detail.type && (d.id && detail.id ? d.id === detail.id : d.index === detail.index));
380
+ if (at === -1) {
381
+ details.push(detail);
382
+ }
383
+ else {
384
+ const existing = details[at];
385
+ details[at] = {
386
+ ...existing,
387
+ id: existing.id || detail.id,
388
+ text: concat(existing.text, detail.text),
389
+ summary: concat(existing.summary, detail.summary),
390
+ data: concat(existing.data, detail.data),
391
+ signature: detail.signature ?? existing.signature,
392
+ };
408
393
  }
409
394
  }
410
- return target;
395
+ return {
396
+ content: concat(target?.content, add.content),
397
+ details: details.length ? details : undefined,
398
+ provider: target?.provider ?? add.provider,
399
+ };
400
+ }
401
+ /**
402
+ * Whether reasoning goes back to a provider: it came from there, or names no
403
+ * provider. Signed and encrypted reasoning only verifies where it was made.
404
+ *
405
+ * @param reasoning - The reasoning of an earlier assistant turn.
406
+ * @param provider - The provider the conversation goes to.
407
+ * @returns - Whether to replay the reasoning.
408
+ */
409
+ export function replaysTo(reasoning, provider) {
410
+ return !!reasoning && (!reasoning.provider || reasoning.provider === provider);
411
411
  }
412
412
  /**
413
413
  * Converts a Response object into an array of Chunk objects.
@@ -431,6 +431,11 @@ export function getChunksFromResponse(response) {
431
431
  finishReason: response.finishReason,
432
432
  usage: response.usage,
433
433
  model: response.model,
434
+ citations: response.citations,
435
+ images: response.images,
436
+ audio: response.audio,
437
+ moderation: response.moderation,
438
+ serviceTier: response.serviceTier,
434
439
  });
435
440
  return chunks;
436
441
  }