@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.
- package/dist/packlets/ai-assist/completionClient.js +147 -19
- package/dist/packlets/ai-assist/completionClient.js.map +1 -1
- package/dist/packlets/ai-assist/index.js +2 -1
- package/dist/packlets/ai-assist/index.js.map +1 -1
- package/dist/packlets/ai-assist/jsonCompletion.js +20 -2
- package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/dist/packlets/ai-assist/model.js.map +1 -1
- package/dist/packlets/ai-assist/registry.js +39 -1
- package/dist/packlets/ai-assist/registry.js.map +1 -1
- package/dist/packlets/ai-assist/structuredOutput.js +315 -0
- package/dist/packlets/ai-assist/structuredOutput.js.map +1 -0
- package/dist/packlets/ai-assist/structuredOutputTypes.js +21 -0
- package/dist/packlets/ai-assist/structuredOutputTypes.js.map +1 -0
- package/dist/ts-extras.d.ts +229 -2
- package/lib/packlets/ai-assist/completionClient.d.ts +13 -0
- package/lib/packlets/ai-assist/completionClient.d.ts.map +1 -1
- package/lib/packlets/ai-assist/completionClient.js +146 -18
- package/lib/packlets/ai-assist/completionClient.js.map +1 -1
- package/lib/packlets/ai-assist/index.d.ts +3 -1
- package/lib/packlets/ai-assist/index.d.ts.map +1 -1
- package/lib/packlets/ai-assist/index.js +6 -2
- package/lib/packlets/ai-assist/index.js.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
- package/lib/packlets/ai-assist/jsonCompletion.js +20 -2
- package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
- package/lib/packlets/ai-assist/model.d.ts +38 -2
- package/lib/packlets/ai-assist/model.d.ts.map +1 -1
- package/lib/packlets/ai-assist/model.js.map +1 -1
- package/lib/packlets/ai-assist/registry.d.ts +20 -0
- package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
- package/lib/packlets/ai-assist/registry.js +41 -1
- package/lib/packlets/ai-assist/registry.js.map +1 -1
- package/lib/packlets/ai-assist/structuredOutput.d.ts +88 -0
- package/lib/packlets/ai-assist/structuredOutput.d.ts.map +1 -0
- package/lib/packlets/ai-assist/structuredOutput.js +321 -0
- package/lib/packlets/ai-assist/structuredOutput.js.map +1 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts +142 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.d.ts.map +1 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.js +22 -0
- package/lib/packlets/ai-assist/structuredOutputTypes.js.map +1 -0
- package/package.json +7 -7
package/dist/ts-extras.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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 (
|
|
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
|