@fgv/ts-extras 5.1.0-52 → 5.1.0-53

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 (41) hide show
  1. package/dist/packlets/ai-assist/completionClient.js +147 -19
  2. package/dist/packlets/ai-assist/completionClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/index.js +2 -1
  4. package/dist/packlets/ai-assist/index.js.map +1 -1
  5. package/dist/packlets/ai-assist/jsonCompletion.js +20 -2
  6. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
  7. package/dist/packlets/ai-assist/model.js.map +1 -1
  8. package/dist/packlets/ai-assist/registry.js +39 -1
  9. package/dist/packlets/ai-assist/registry.js.map +1 -1
  10. package/dist/packlets/ai-assist/structuredOutput.js +315 -0
  11. package/dist/packlets/ai-assist/structuredOutput.js.map +1 -0
  12. package/dist/packlets/ai-assist/structuredOutputTypes.js +21 -0
  13. package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -0
  14. package/dist/ts-extras.d.ts +229 -2
  15. package/lib/packlets/ai-assist/completionClient.d.ts +13 -0
  16. package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
  17. package/lib/packlets/ai-assist/completionClient.js +146 -18
  18. package/lib/packlets/ai-assist/completionClient.js.map +1 -1
  19. package/lib/packlets/ai-assist/index.d.ts +3 -1
  20. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  21. package/lib/packlets/ai-assist/index.js +6 -2
  22. package/lib/packlets/ai-assist/index.js.map +1 -1
  23. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
  24. package/lib/packlets/ai-assist/jsonCompletion.js +20 -2
  25. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
  26. package/lib/packlets/ai-assist/model.d.ts +38 -2
  27. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  28. package/lib/packlets/ai-assist/model.js.map +1 -1
  29. package/lib/packlets/ai-assist/registry.d.ts +20 -0
  30. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  31. package/lib/packlets/ai-assist/registry.js +41 -1
  32. package/lib/packlets/ai-assist/registry.js.map +1 -1
  33. package/lib/packlets/ai-assist/structuredOutput.d.ts +88 -0
  34. package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -0
  35. package/lib/packlets/ai-assist/structuredOutput.js +321 -0
  36. package/lib/packlets/ai-assist/structuredOutput.js.map +1 -0
  37. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +142 -0
  38. package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -0
  39. package/lib/packlets/ai-assist/structuredOutputTypes.js +22 -0
  40. package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -0
  41. package/package.json +7 -7
@@ -181,6 +181,8 @@ declare namespace AiAssist {
181
181
  supportsImageGeneration,
182
182
  resolveEmbeddingCapability,
183
183
  supportsEmbedding,
184
+ resolveStructuredOutputCapability,
185
+ supportsStructuredOutput,
184
186
  DEFAULT_MODEL_CAPABILITY_CONFIG,
185
187
  callProviderCompletion,
186
188
  callProxiedCompletion,
@@ -213,6 +215,14 @@ declare namespace AiAssist {
213
215
  modelSpecKey,
214
216
  modelSpec,
215
217
  resolveEffectiveTools,
218
+ ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,
219
+ AiStructuredOutputFormat,
220
+ IAiStructuredOutputCapability,
221
+ IJsonObjectStructuredOutputRequest,
222
+ ISchemaStructuredOutputRequest,
223
+ StructuredOutputEnforcement,
224
+ StructuredOutputFallback,
225
+ StructuredOutputRequest,
216
226
  classifyJsonParseFailure,
217
227
  extractJsonText,
218
228
  fencedStringifiedJson,
@@ -393,6 +403,20 @@ declare type AiServerToolType = 'web_search';
393
403
  */
394
404
  declare const aiServerToolType: Converter<AiServerToolType>;
395
405
 
406
+ /**
407
+ * Wire format a provider uses to express a structured-output constraint.
408
+ *
409
+ * @remarks
410
+ * Four shapes, not one, and they differ in more than field names: the OpenAI
411
+ * pair carry the schema in the request body, Gemini carries it inside
412
+ * `generationConfig`, and Anthropic has no response-format field at all —
413
+ * its mechanism is forced tool use, which is why `'tool-forced'` is a distinct
414
+ * {@link AiAssist.StructuredOutputEnforcement} value rather than a spelling of
415
+ * `'schema'`.
416
+ * @public
417
+ */
418
+ declare type AiStructuredOutputFormat = 'openai-json-schema' | 'openai-responses-format' | 'gemini-response-schema' | 'anthropic-tool-forced';
419
+
396
420
  /**
397
421
  * Thinking/reasoning mode support for a provider.
398
422
  * @public
@@ -550,6 +574,19 @@ declare const allProviderIds: ReadonlyArray<AiProviderId>;
550
574
  */
551
575
  declare const ALWAYS_STRIPPED_HEADERS: ReadonlyArray<string>;
552
576
 
577
+ /**
578
+ * The name the Anthropic forced-tool path gives its synthetic tool.
579
+ *
580
+ * @remarks
581
+ * Anthropic has no `response_format`; its structured-output mechanism is forced
582
+ * tool use, so a tool must exist to be forced. The name is fgv-owned and never
583
+ * reaches the caller — the structured-output resolver re-serializes the tool's
584
+ * `input` back into `IAiCompletionResponse.content`, so a caller's converter sees
585
+ * a JSON string exactly as it does on every other provider.
586
+ * @public
587
+ */
588
+ declare const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME: string;
589
+
553
590
  /**
554
591
  * Maps Anthropic effort level to the `thinking.budget_tokens` integer that the
555
592
  * Anthropic API requires when `thinking.type === 'enabled'`.
@@ -2596,11 +2633,29 @@ declare interface IAiCompletionResponse {
2596
2633
  readonly content: string;
2597
2634
  /** Whether the response was truncated due to token limits */
2598
2635
  readonly truncated: boolean;
2636
+ /**
2637
+ * Which structured-output constraint the provider was **asked** to apply.
2638
+ *
2639
+ * @remarks
2640
+ * **Required, not optional, and that is the point.** An optional field would
2641
+ * make absence three-ways ambiguous — no capability / not requested / a build
2642
+ * predating the feature — and disambiguating exactly that is what this field
2643
+ * exists for. `'none'` already expresses *"no constraint sent"*, so
2644
+ * always-present costs nothing and removes the ambiguity by construction. The
2645
+ * same remedy as `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, applied to the
2646
+ * same defect.
2647
+ *
2648
+ * It reports what was *sent*, never whether **this** response conforms — that
2649
+ * is the caller's converter's answer and re-deriving it here would be a second
2650
+ * source of truth. See `StructuredOutputEnforcement` for the three-question
2651
+ * split.
2652
+ */
2653
+ readonly structuredOutput: StructuredOutputEnforcement;
2599
2654
  }
2600
2655
 
2601
2656
  /**
2602
2657
  * Embedding capability for a model family within a provider. Used as an entry
2603
- * in {@link IAiProviderDescriptor.embedding}.
2658
+ * in `embedding`.
2604
2659
  *
2605
2660
  * @public
2606
2661
  */
@@ -2823,7 +2878,7 @@ declare interface IAiImageGenerationResponse {
2823
2878
 
2824
2879
  /**
2825
2880
  * Image-generation capability for a model family within a provider. Used as
2826
- * an entry in {@link IAiProviderDescriptor.imageGeneration}.
2881
+ * an entry in `imageGeneration`.
2827
2882
  *
2828
2883
  * @public
2829
2884
  */
@@ -3006,6 +3061,23 @@ declare interface IAiProviderDescriptor {
3006
3061
  * caller supplies the embedding model via `modelOverride`.
3007
3062
  */
3008
3063
  readonly embedding?: ReadonlyArray<IAiEmbeddingModelCapability>;
3064
+ /**
3065
+ * Per-model-family structured-output capability, longest-prefix matched against
3066
+ * the **resolved** completion model id. Absent (or no matching entry) means the
3067
+ * model can enforce nothing, and a request against it reports `'none'`.
3068
+ *
3069
+ * @remarks
3070
+ * Same declaration idiom as `imageGeneration` and
3071
+ * `embedding`, resolved through the same
3072
+ * alias-first helper — a capability lookup on an unresolved alias is the defect
3073
+ * `resolveImageCapability` once had, where a catch-all `modelPrefix: ''` turned
3074
+ * an unknown alias into a confidently wrong answer.
3075
+ *
3076
+ * Note this declares which wire format a model *family* supports, not which
3077
+ * OpenAI endpoint a given call will take — that also depends on whether the
3078
+ * call carries server tools, so the dispatcher supplies it.
3079
+ */
3080
+ readonly structuredOutput?: ReadonlyArray<IAiStructuredOutputCapability>;
3009
3081
  /**
3010
3082
  * Concrete model ids (prefix-matched) that must be invoked via the OpenAI
3011
3083
  * Responses API rather than chat completions — e.g. `gpt-5.5-pro`. Non-OpenAI
@@ -3180,6 +3252,28 @@ declare interface IAiStreamToolUseStart {
3180
3252
  readonly callId?: string;
3181
3253
  }
3182
3254
 
3255
+ /**
3256
+ * Structured-output capability for a model family within a provider. Used as an
3257
+ * entry in `IAiProviderDescriptor.structuredOutput`.
3258
+ *
3259
+ * @remarks
3260
+ * Deliberately thinner than its `imageGeneration` / `embedding` siblings: it
3261
+ * carries no `supportsX` flags, because what each format can enforce is a
3262
+ * property of the provider's **API surface** rather than of any one model, and a
3263
+ * per-entry declaration of it could only ever disagree with the one in code.
3264
+ * @public
3265
+ */
3266
+ declare interface IAiStructuredOutputCapability {
3267
+ /**
3268
+ * Prefix matched against the resolved completion model id. The empty string is
3269
+ * the catch-all and matches every model. When multiple rules' prefixes match a
3270
+ * model id, the longest prefix wins; ties are broken by first-encountered.
3271
+ */
3272
+ readonly modelPrefix: string;
3273
+ /** Wire format used to express the constraint for matching models. */
3274
+ readonly format: AiStructuredOutputFormat;
3275
+ }
3276
+
3183
3277
  /**
3184
3278
  * Behavior annotations for a client-defined tool.
3185
3279
  *
@@ -4543,6 +4637,22 @@ declare interface IImportSecretOptions extends IAddSecretOptions {
4543
4637
  readonly replace?: boolean;
4544
4638
  }
4545
4639
 
4640
+ /**
4641
+ * Ask the provider for syntactically valid JSON of arbitrary shape.
4642
+ *
4643
+ * @remarks
4644
+ * The weaker floor, and worth having on its own: the failure that motivated this
4645
+ * surface (`Expected ',' or '}' after property value` — an unescaped quote closing
4646
+ * a string early) is **syntactic**, so a JSON-mode guarantee removes it. Schema
4647
+ * constraint is what additionally buys shape. It is also the only mode some
4648
+ * model/provider pairs support.
4649
+ * @public
4650
+ */
4651
+ declare interface IJsonObjectStructuredOutputRequest {
4652
+ readonly mode: 'json-object';
4653
+ readonly onUnsupported?: StructuredOutputFallback;
4654
+ }
4655
+
4546
4656
  /**
4547
4657
  * Key derivation parameters stored in encrypted files.
4548
4658
  * Discriminated union on `kdf` field: `'pbkdf2'` or `'argon2id'`.
@@ -5239,6 +5349,18 @@ declare interface IProviderCompletionParams extends IChatRequest {
5239
5349
  * Messages API requires the field, so it falls back to `DEFAULT_ANTHROPIC_MAX_TOKENS`.
5240
5350
  */
5241
5351
  readonly maxTokens?: number;
5352
+ /**
5353
+ * Ask the provider to constrain its output — to a schema, or to syntactically
5354
+ * valid JSON of arbitrary shape.
5355
+ *
5356
+ * @remarks
5357
+ * **The caller supplies intent; the response reports outcome.** A caller cannot
5358
+ * know up front which concrete model will serve the request (a `tier` request
5359
+ * cascades, and aliases resolve at call time), so it never has to: whatever was
5360
+ * actually enforced comes back on
5361
+ * `IAiCompletionResponse.structuredOutput`.
5362
+ */
5363
+ readonly structuredOutput?: StructuredOutputRequest;
5242
5364
  }
5243
5365
 
5244
5366
  /**
@@ -5820,6 +5942,25 @@ declare interface ISaferFetchResponseHead {
5820
5942
  readonly contentLength?: number;
5821
5943
  }
5822
5944
 
5945
+ /**
5946
+ * Ask the provider for JSON constrained to a schema.
5947
+ * @public
5948
+ */
5949
+ declare interface ISchemaStructuredOutputRequest {
5950
+ readonly mode: 'schema';
5951
+ /**
5952
+ * The schema to constrain generation to — **the same object you validate the
5953
+ * reply with**, so the wire schema and the check cannot drift.
5954
+ *
5955
+ * @remarks
5956
+ * Author it with `JsonSchema.object({...})` from `@fgv/ts-json-base`. This is
5957
+ * the property `@fgv/ts-extras-ollama`'s `chatStructured` already has; this
5958
+ * surface is its cloud sibling.
5959
+ */
5960
+ readonly schema: JsonSchema.ISchemaValidator<unknown>;
5961
+ readonly onUnsupported?: StructuredOutputFallback;
5962
+ }
5963
+
5823
5964
  /**
5824
5965
  * Checks if a JSON object appears to be an encrypted file.
5825
5966
  * Uses the format field as a discriminator.
@@ -7897,6 +8038,21 @@ declare function resolveModelAlias(descriptor: IAiProviderDescriptor, model: str
7897
8038
  */
7898
8039
  declare function resolveProviderModel(descriptor: IAiProviderDescriptor, modelOverride: ModelSpec | undefined, context?: ModelSpecKey): Result<string>;
7899
8040
 
8041
+ /**
8042
+ * The structured-output capability for `modelId` under `descriptor`, or
8043
+ * `undefined` when the model can enforce nothing.
8044
+ *
8045
+ * @remarks
8046
+ * Alias-first, exactly like its `imageGeneration` / `embedding` siblings — an
8047
+ * unresolved alias returns `undefined` rather than prefix-matching a catch-all
8048
+ * `modelPrefix: ''`, which is the defect this helper was written to prevent.
8049
+ *
8050
+ * @param descriptor - The provider descriptor.
8051
+ * @param modelId - A concrete model id or an `@provider:role` alias.
8052
+ * @public
8053
+ */
8054
+ declare function resolveStructuredOutputCapability(descriptor: IAiProviderDescriptor, modelId: string): IAiStructuredOutputCapability | undefined;
8055
+
7900
8056
  /**
7901
8057
  * Statuses whose `Retry-After` header is honored.
7902
8058
  *
@@ -8163,6 +8319,71 @@ declare const SMART_JSON_PROMPT_HINT: string;
8163
8319
  */
8164
8320
  declare function spkiToRawX25519(spki: Uint8Array): Result<Uint8Array>;
8165
8321
 
8322
+ /**
8323
+ * Which constraint the provider was **asked** to apply to this response.
8324
+ *
8325
+ * @remarks
8326
+ * Three questions hide inside *"did it honour my schema"*, and they have different
8327
+ * owners:
8328
+ *
8329
+ * | question | answerable by |
8330
+ * |---|---|
8331
+ * | did we send a constraint? | this client, at request-build time |
8332
+ * | which constraint did the provider apply? | this client, from the resolved model's capability |
8333
+ * | does *this response* conform to my shape? | the caller's converter, and nothing else |
8334
+ *
8335
+ * This type answers the first two and deliberately not the third. Reporting
8336
+ * conformance would mean re-validating against the caller's own schema to
8337
+ * re-derive an answer the caller already holds.
8338
+ *
8339
+ * - `'none'` — nothing was sent; the resolved model declares no capability.
8340
+ * - `'json-mode'` — syntactically valid JSON is guaranteed; the shape is not.
8341
+ * - `'schema'` — generation was constrained to the supplied schema.
8342
+ * - `'tool-forced'` — Anthropic-style forced tool use; the shape comes from the
8343
+ * forced tool's input schema, and `content` is the re-serialized tool input.
8344
+ * @public
8345
+ */
8346
+ declare type StructuredOutputEnforcement = 'none' | 'json-mode' | 'schema' | 'tool-forced';
8347
+
8348
+ /**
8349
+ * What to do when the resolved model cannot apply the requested constraint.
8350
+ *
8351
+ * @remarks
8352
+ * `'degrade'` is the default, and it is only safe **because
8353
+ * `IAiCompletionResponse.structuredOutput` is required** rather than
8354
+ * optional. Degrade-and-tell-me is safe; degrade-silently is the failure this
8355
+ * whole surface exists to remove — so the two decisions are one decision, not
8356
+ * two independent ones.
8357
+ *
8358
+ * Reach for `'fail'` when the output is persisted or put on a wire, where an
8359
+ * unconstrained generation that happens to parse is worse than an error because
8360
+ * it is wrong quietly. Leave it at `'degrade'` on paths that are *designed* to
8361
+ * degrade — an extractor that may return nothing, a segmenter that floors to a
8362
+ * mechanical chunker — where a hard failure would make this library less safe
8363
+ * than the code it replaces.
8364
+ * @public
8365
+ */
8366
+ declare type StructuredOutputFallback = 'degrade' | 'fail';
8367
+
8368
+ /**
8369
+ * A caller's structured-output intent.
8370
+ *
8371
+ * @remarks
8372
+ * A discriminated union rather than an optional `schema` whose absence means
8373
+ * *"json-object please"* — an absence that means something is the shape this repo
8374
+ * has been burned by (see `MemoryEmbedOutcome` in `@fgv/ts-agent-memory`, which
8375
+ * exists because a three-ways-ambiguous absence could not be read).
8376
+ *
8377
+ * **The caller supplies intent; the response reports outcome.** A request never
8378
+ * needs to know whether the constraint will be honoured, because
8379
+ * `resolveProviderModel` resolves aliases and tiers at *call* time — a `tier`
8380
+ * request can cascade — so the concrete model that will serve a request is not
8381
+ * knowable to the caller up front. Requiring it to know would be unsound, which
8382
+ * is why the report rides on the response rather than being a lookup.
8383
+ * @public
8384
+ */
8385
+ declare type StructuredOutputRequest = ISchemaStructuredOutputRequest | IJsonObjectStructuredOutputRequest;
8386
+
8166
8387
  /**
8167
8388
  * URL schemes this primitive will ever request.
8168
8389
  *
@@ -8195,6 +8416,12 @@ declare function supportsEmbedding(descriptor: IAiProviderDescriptor): boolean;
8195
8416
  */
8196
8417
  declare function supportsImageGeneration(descriptor: IAiProviderDescriptor): boolean;
8197
8418
 
8419
+ /**
8420
+ * Whether `descriptor` declares any structured-output capability at all.
8421
+ * @public
8422
+ */
8423
+ declare function supportsStructuredOutput(descriptor: IAiProviderDescriptor): boolean;
8424
+
8198
8425
  /**
8199
8426
  * Helper function to create a `StringConverter` which converts
8200
8427
  * `unknown` to `string`, applying template conversions supplied at construction time or at
@@ -1,5 +1,6 @@
1
1
  import { type Logging, Result } from '@fgv/ts-utils';
2
2
  import { type AiServerToolConfig, type IAiCompletionResponse, type IAiProviderDescriptor, type IChatRequest, type IThinkingConfig, type ModelSpec } from './model';
3
+ import type { StructuredOutputRequest } from './structuredOutputTypes';
3
4
  /**
4
5
  * Parameters for a provider completion request. Carries the unified
5
6
  * {@link AiAssist.IChatRequest} shape (`system?` + ordered `messages`, last =
@@ -52,6 +53,18 @@ export interface IProviderCompletionParams extends IChatRequest {
52
53
  * Messages API requires the field, so it falls back to `DEFAULT_ANTHROPIC_MAX_TOKENS`.
53
54
  */
54
55
  readonly maxTokens?: number;
56
+ /**
57
+ * Ask the provider to constrain its output — to a schema, or to syntactically
58
+ * valid JSON of arbitrary shape.
59
+ *
60
+ * @remarks
61
+ * **The caller supplies intent; the response reports outcome.** A caller cannot
62
+ * know up front which concrete model will serve the request (a `tier` request
63
+ * cascades, and aliases resolve at call time), so it never has to: whatever was
64
+ * actually enforced comes back on
65
+ * `IAiCompletionResponse.structuredOutput`.
66
+ */
67
+ readonly structuredOutput?: StructuredOutputRequest;
55
68
  }
56
69
  /**
57
70
  * Calls the appropriate chat completion API for a given provider. Routes by
@@ -1 +1 @@
1
- {"version":3,"file":"completionClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/completionClient.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,EAAuC,MAAM,eAAe,CAAC;AAEhG,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE1B,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,SAAS,EAMf,MAAM,SAAS,CAAC;AA8BjB;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,YAAY;IAC7D,8BAA8B;IAC9B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAClC,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACnD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AA0aD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAqIxC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAwExC"}
1
+ {"version":3,"file":"completionClient.d.ts","sourceRoot":"","sources":["../../../src/packlets/ai-assist/completionClient.ts"],"names":[],"mappings":"AA8BA,OAAO,EAGL,KAAK,OAAO,EACZ,MAAM,EAIP,MAAM,eAAe,CAAC;AAEvB,OAAO,EAEL,KAAK,kBAAkB,EAEvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE1B,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,SAAS,EAMf,MAAM,SAAS,CAAC;AAiCjB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAMvE;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,YAAY;IAC7D,8BAA8B;IAC9B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAClC,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACnD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;CACrD;AAigBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAiLxC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAiHxC"}
@@ -28,6 +28,8 @@ const chatRequestBuilders_1 = require("./chatRequestBuilders");
28
28
  const endpoint_1 = require("./endpoint");
29
29
  const http_1 = require("./http");
30
30
  const toolFormats_1 = require("./toolFormats");
31
+ const structuredOutput_1 = require("./structuredOutput");
32
+ const registry_1 = require("./registry");
31
33
  const openAiMessage = ts_utils_1.Validators.object({
32
34
  content: ts_utils_1.Validators.string
33
35
  });
@@ -73,7 +75,7 @@ const geminiResponse = ts_utils_1.Validators.object({
73
75
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
74
76
  * @internal
75
77
  */
76
- async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false) {
78
+ async function callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, useMaxCompletionTokensField = false, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
77
79
  var _a;
78
80
  const url = `${config.baseUrl}/chat/completions`;
79
81
  const messages = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiChatUserContent)(prompt), {
@@ -85,6 +87,7 @@ async function callOpenAiCompletion(config, prompt, head, temperature, logger, s
85
87
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
86
88
  Object.assign(body, resolvedThinking.otherParams);
87
89
  }
90
+ Object.assign(body, structured.wire);
88
91
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
89
92
  /* c8 ignore next 1 - optional logger */
90
93
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
@@ -99,7 +102,8 @@ async function callOpenAiCompletion(config, prompt, head, temperature, logger, s
99
102
  const choice = response.choices[0];
100
103
  return (0, ts_utils_1.succeed)({
101
104
  content: choice.message.content,
102
- truncated: choice.finish_reason === 'length'
105
+ truncated: choice.finish_reason === 'length',
106
+ structuredOutput: structured.enforcement
103
107
  });
104
108
  });
105
109
  }
@@ -127,7 +131,7 @@ function extractResponsesApiText(output) {
127
131
  * Used when tools are configured for an openai-format provider.
128
132
  * @internal
129
133
  */
130
- async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens) {
134
+ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, temperature, logger, signal, resolvedThinking, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
131
135
  var _a;
132
136
  const url = `${config.baseUrl}/responses`;
133
137
  const input = (0, chatRequestBuilders_1.buildMessages)(prompt.system, (0, chatRequestBuilders_1.buildOpenAiResponsesUserContent)(prompt), {
@@ -142,6 +146,7 @@ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, t
142
146
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
143
147
  Object.assign(body, resolvedThinking.otherParams);
144
148
  }
149
+ Object.assign(body, structured.wire);
145
150
  const headers = (0, endpoint_1.bearerAuthHeader)(config.apiKey);
146
151
  /* c8 ignore next 1 - optional logger */
147
152
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
@@ -155,7 +160,8 @@ async function callOpenAiResponsesCompletion(config, prompt, tools = [], head, t
155
160
  .onSuccess((response) => {
156
161
  return extractResponsesApiText(response.output).onSuccess((text) => (0, ts_utils_1.succeed)({
157
162
  content: text,
158
- truncated: response.status === 'incomplete'
163
+ truncated: response.status === 'incomplete',
164
+ structuredOutput: structured.enforcement
159
165
  }));
160
166
  });
161
167
  }
@@ -184,8 +190,45 @@ function extractAnthropicText(content) {
184
190
  }
185
191
  return (0, ts_utils_1.succeed)(textParts.join(''));
186
192
  }
193
+ /**
194
+ * Extracts the forced structured-output tool's input from Anthropic response
195
+ * content blocks and re-serializes it.
196
+ *
197
+ * @remarks
198
+ * Under `'tool-forced'` enforcement the model's answer arrives as a `tool_use`
199
+ * block's `input` — a parsed object — rather than as text. Re-serializing it here
200
+ * keeps `IAiCompletionResponse.content` a JSON **string** on every provider, so a
201
+ * caller's converter is written once and does not branch on which enforcement it
202
+ * got. A useful side effect: the string is produced by `JSON.stringify` rather
203
+ * than by the model, so under this enforcement it is syntactically valid by
204
+ * construction.
205
+ * @internal
206
+ */
207
+ function extractAnthropicStructuredOutput(content) {
208
+ for (const block of content) {
209
+ if (typeof block === 'object' && block !== null && 'type' in block) {
210
+ const typed = block;
211
+ if (typed.type === 'tool_use' && typed.name === structuredOutput_1.ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME) {
212
+ // `JSON.stringify` returns `undefined` — not a string, and not a throw —
213
+ // for `undefined` and for a function or symbol. `captureResult` would wrap
214
+ // that as a Success, putting `undefined` behind a `content: string`
215
+ // contract with nothing to catch it downstream. A `tool_use` block with no
216
+ // `input` is exactly that case.
217
+ return (0, ts_utils_1.captureResult)(() => JSON.stringify(typed.input))
218
+ .withErrorFormat((msg) => `Anthropic API response: structured output could not be serialized: ${msg}`)
219
+ .onSuccess((json) => typeof json === 'string'
220
+ ? (0, ts_utils_1.succeed)(json)
221
+ : (0, ts_utils_1.fail)(`Anthropic API response: forced tool '${structuredOutput_1.ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME}' returned no serializable input`));
222
+ }
223
+ }
224
+ }
225
+ // Loud rather than a silent fall back to text: we forced the tool, so its
226
+ // absence means the request did not do what the response is about to claim it
227
+ // did — and `structuredOutput: 'tool-forced'` would then be a lie.
228
+ return (0, ts_utils_1.fail)(`Anthropic API response: structured output was forced but no '${structuredOutput_1.ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME}' tool_use block was returned`);
229
+ }
187
230
  /** Calls the Anthropic Messages API with optional tool support. @internal */
188
- async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens) {
231
+ async function callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, useAdaptiveThinking = false, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
189
232
  const url = `${config.baseUrl}/messages`;
190
233
  const messages = (0, chatRequestBuilders_1.buildAnthropicMessages)(prompt, { head });
191
234
  const body = Object.assign({ model: config.model, system: prompt.system, messages,
@@ -207,6 +250,22 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
207
250
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
208
251
  Object.assign(body, resolvedThinking.otherParams);
209
252
  }
253
+ // The structured-output wire carries `tools` + `tool_choice` of its own, so the
254
+ // server-tool assignment below would clobber it. `resolveStructuredOutput`
255
+ // refuses that combination up front, which is what makes the two mutually
256
+ // exclusive — but that is an invariant held in a DIFFERENT FILE, and a future
257
+ // second Anthropic capability entry (or a relaxed conflict guard) would
258
+ // reintroduce silent clobbering with nothing failing at this line. So assert it
259
+ // here rather than trusting a comment across a file boundary.
260
+ // Unreachable through the public API — resolveStructuredOutput refuses this
261
+ // combination before dispatch — and unreachable BY DESIGN: it cannot be
262
+ // exercised without first breaking the very thing it guards against.
263
+ /* c8 ignore next 6 - defensive: internal consistency check, see above */
264
+ if (structured.enforcement === 'tool-forced' && tools !== undefined && tools.length > 0) {
265
+ return (0, ts_utils_1.fail)(`Anthropic completion: structured output and server-side tools both claim the tools channel; ` +
266
+ `this combination must be refused before reaching the adapter`);
267
+ }
268
+ Object.assign(body, structured.wire);
210
269
  if (tools && tools.length > 0) {
211
270
  body.tools = (0, toolFormats_1.toAnthropicTools)(tools);
212
271
  /* c8 ignore next 3 - optional logger diagnostic output */
@@ -229,9 +288,13 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
229
288
  if (typeof stopReason !== 'string') {
230
289
  return (0, ts_utils_1.fail)('Anthropic API response: stop_reason is missing or not a string');
231
290
  }
232
- return extractAnthropicText(rawContent).onSuccess((text) => (0, ts_utils_1.succeed)({
291
+ const extracted = structured.enforcement === 'tool-forced'
292
+ ? extractAnthropicStructuredOutput(rawContent)
293
+ : extractAnthropicText(rawContent);
294
+ return extracted.onSuccess((text) => (0, ts_utils_1.succeed)({
233
295
  content: text,
234
- truncated: stopReason === 'max_tokens'
296
+ truncated: stopReason === 'max_tokens',
297
+ structuredOutput: structured.enforcement
235
298
  }));
236
299
  }
237
300
  // ============================================================================
@@ -242,7 +305,7 @@ async function callAnthropicCompletion(config, prompt, head, temperature, logger
242
305
  * When tools are configured, includes Google Search grounding.
243
306
  * @internal
244
307
  */
245
- async function callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens) {
308
+ async function callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens, structured = structuredOutput_1.NO_STRUCTURED_OUTPUT) {
246
309
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
247
310
  const contents = (0, chatRequestBuilders_1.buildGeminiContents)(prompt, { head });
248
311
  // Temperature is sent only when explicitly provided; otherwise Gemini's default applies.
@@ -259,6 +322,8 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
259
322
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
260
323
  Object.assign(generationConfig, resolvedThinking.otherParams);
261
324
  }
325
+ // Gemini nests the constraint INSIDE generationConfig, not on the body.
326
+ Object.assign(generationConfig, structured.wire);
262
327
  const body = {
263
328
  systemInstruction: { parts: [{ text: prompt.system }] },
264
329
  contents,
@@ -284,8 +349,15 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
284
349
  .onSuccess((response) => {
285
350
  const candidate = response.candidates[0];
286
351
  return (0, ts_utils_1.succeed)({
287
- content: candidate.content.parts[0].text,
288
- truncated: candidate.finishReason === 'MAX_TOKENS'
352
+ // ALL parts, not `parts[0]`. Gemini may split one reply across several
353
+ // text parts, and reading only the first silently discards the rest —
354
+ // yielding a truncated document that often still parses, which is the
355
+ // worst way to be wrong. The streaming adapter has always concatenated
356
+ // (`fullText += part.text`); this path did not, so the same response gave
357
+ // different text depending on which one you called.
358
+ content: candidate.content.parts.map((part) => part.text).join(''),
359
+ truncated: candidate.finishReason === 'MAX_TOKENS',
360
+ structuredOutput: structured.enforcement
289
361
  });
290
362
  });
291
363
  }
@@ -299,7 +371,7 @@ async function callGeminiCompletion(config, prompt, head, temperature, logger, t
299
371
  * @public
300
372
  */
301
373
  async function callProviderCompletion(params) {
302
- const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens } = params;
374
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, tier, logger, tools, signal, endpoint, thinking, maxTokens, structuredOutput } = params;
303
375
  const splitResult = (0, chatRequestBuilders_1.splitChatRequest)(system, messages);
304
376
  if (splitResult.isFailure()) {
305
377
  return (0, ts_utils_1.fail)(splitResult.message);
@@ -337,6 +409,32 @@ async function callProviderCompletion(params) {
337
409
  }
338
410
  }
339
411
  }
412
+ // Resolved against the CONCRETE model, after resolveProviderModel — passing an
413
+ // alias here is the defect resolveImageCapability once had.
414
+ // The OpenAI route depends on tools AND the model, so it is computed here (once,
415
+ // beside the switch that uses it) and handed to the resolver — a capability keyed
416
+ // on the model alone cannot know which of the two OpenAI wire shapes applies.
417
+ const usesResponsesApi = descriptor.apiFormat === 'openai' && (hasTools || (0, model_1.isResponsesOnlyModel)(descriptor, model));
418
+ const structuredResult = (0, structuredOutput_1.resolveStructuredOutput)(descriptor, model, structuredOutput, tools, usesResponsesApi, registry_1.resolveStructuredOutputCapability);
419
+ if (structuredResult.isFailure()) {
420
+ return (0, ts_utils_1.fail)(structuredResult.message);
421
+ }
422
+ const resolvedStructured = structuredResult.value;
423
+ // OpenAI rejects `response_format: { type: 'json_object' }` with a 400 unless the
424
+ // conversation mentions JSON somewhere — a documented API rule, and one a caller
425
+ // has no way to discover from a schema-mode request that worked. Pre-empted with a
426
+ // named failure before the wire call, the same treatment the Gemini
427
+ // grounding-plus-function-calling conflict already gets. `generateJsonCompletion`
428
+ // satisfies it for free via its prompt hint; a direct caller may not.
429
+ if (resolvedStructured.enforcement === 'json-mode' && descriptor.apiFormat === 'openai') {
430
+ const mentionsJson = (system !== null && system !== void 0 ? system : '').toLowerCase().includes('json') ||
431
+ messages.some((m) => m.content.toLowerCase().includes('json'));
432
+ if (!mentionsJson) {
433
+ return (0, ts_utils_1.fail)(`provider '${descriptor.id}': json-object structured output requires the word 'json' to ` +
434
+ `appear in the system prompt or a message — OpenAI rejects the request otherwise. Mention ` +
435
+ `it, or use structuredOutput: { mode: 'schema', schema } which carries no such rule`);
436
+ }
437
+ }
340
438
  const config = {
341
439
  baseUrl: baseUrlResult.value,
342
440
  apiKey,
@@ -353,14 +451,14 @@ async function callProviderCompletion(params) {
353
451
  case 'openai':
354
452
  // Responses-API-only models (e.g. gpt-5.5-pro) 400 on /chat/completions, so they route
355
453
  // to the Responses path even with no tools requested — same path the tools case uses.
356
- if (hasTools || (0, model_1.isResponsesOnlyModel)(descriptor, config.model)) {
357
- return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens);
454
+ if (usesResponsesApi) {
455
+ return callOpenAiResponsesCompletion(config, prompt, tools, head, temperature, logger, signal, resolvedThinking, maxTokens, resolvedStructured);
358
456
  }
359
- return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, (0, model_1.usesMaxCompletionTokensField)(descriptor));
457
+ return callOpenAiCompletion(config, prompt, head, temperature, logger, signal, resolvedThinking, maxTokens, (0, model_1.usesMaxCompletionTokensField)(descriptor), resolvedStructured);
360
458
  case 'anthropic':
361
- return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, (0, model_1.isAdaptiveThinkingModel)(descriptor, config.model), maxTokens);
459
+ return callAnthropicCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, (0, model_1.isAdaptiveThinkingModel)(descriptor, config.model), maxTokens, resolvedStructured);
362
460
  case 'gemini':
363
- return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens);
461
+ return callGeminiCompletion(config, prompt, head, temperature, logger, tools, signal, resolvedThinking, maxTokens, resolvedStructured);
364
462
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
365
463
  default: {
366
464
  const _exhaustive = descriptor.apiFormat;
@@ -382,7 +480,7 @@ async function callProviderCompletion(params) {
382
480
  * @public
383
481
  */
384
482
  async function callProxiedCompletion(proxyUrl, params) {
385
- const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens } = params;
483
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking, maxTokens, structuredOutput } = params;
386
484
  const splitResult = (0, chatRequestBuilders_1.splitChatRequest)(system, messages);
387
485
  if (splitResult.isFailure()) {
388
486
  return (0, ts_utils_1.fail)(splitResult.message);
@@ -417,6 +515,18 @@ async function callProxiedCompletion(proxyUrl, params) {
417
515
  if (maxTokens !== undefined) {
418
516
  body.maxTokens = maxTokens;
419
517
  }
518
+ if (structuredOutput !== undefined) {
519
+ // The schema travels as its draft-07 wire form, not as the validator object —
520
+ // an `ISchemaValidator` is not JSON-serializable. A proxy reconstitutes it with
521
+ // `JsonSchema.fromJson(raw)` before calling `callProviderCompletion`.
522
+ body.structuredOutput =
523
+ structuredOutput.mode === 'schema'
524
+ ? Object.assign({ mode: 'schema', schema: structuredOutput.schema.toJson() }, (structuredOutput.onUnsupported !== undefined
525
+ ? { onUnsupported: structuredOutput.onUnsupported }
526
+ : {})) : Object.assign({ mode: 'json-object' }, (structuredOutput.onUnsupported !== undefined
527
+ ? { onUnsupported: structuredOutput.onUnsupported }
528
+ : {}));
529
+ }
420
530
  /* c8 ignore next 1 - optional logger */
421
531
  logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
422
532
  const url = `${proxyUrl}/api/ai/completion`;
@@ -431,9 +541,27 @@ async function callProxiedCompletion(proxyUrl, params) {
431
541
  if (typeof response.content !== 'string') {
432
542
  return (0, ts_utils_1.fail)('proxy returned invalid response: missing content');
433
543
  }
544
+ // A caller who asked for nothing gets `'none'` without the proxy having to say
545
+ // so. A caller who DID ask gets a loud failure when the proxy cannot report,
546
+ // rather than a response claiming an enforcement nobody verified — a proxy
547
+ // predating this feature drops the constraint silently, which is the exact
548
+ // failure this surface exists to remove.
549
+ if (structuredOutput === undefined) {
550
+ return (0, ts_utils_1.succeed)({
551
+ content: response.content,
552
+ truncated: response.truncated === true,
553
+ structuredOutput: 'none'
554
+ });
555
+ }
556
+ if (!(0, structuredOutput_1.isStructuredOutputEnforcement)(response.structuredOutput)) {
557
+ return (0, ts_utils_1.fail)(`proxy did not report which structured-output constraint it applied ` +
558
+ `(got ${JSON.stringify(response.structuredOutput)}); it may predate the feature and have ` +
559
+ `dropped the request silently`);
560
+ }
434
561
  return (0, ts_utils_1.succeed)({
435
562
  content: response.content,
436
- truncated: response.truncated === true
563
+ truncated: response.truncated === true,
564
+ structuredOutput: response.structuredOutput
437
565
  });
438
566
  }
439
567
  //# sourceMappingURL=completionClient.js.map