@vellumai/assistant 0.10.1-staging.2 → 0.10.1-staging.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +67 -0
- package/src/__tests__/card-surface-data.test.ts +60 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +3 -3
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +352 -0
- package/src/__tests__/dynamic-page-surface.test.ts +0 -94
- package/src/__tests__/llm-resolver.test.ts +205 -5
- package/src/api/events/ui-surface-show.ts +8 -3
- package/src/api/index.ts +1 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/surfaces.ts +33 -0
- package/src/config/llm-resolver.ts +151 -14
- package/src/daemon/conversation-surfaces.ts +273 -18
- package/src/daemon/message-types/surfaces.ts +11 -20
- package/src/memory/embedding-gemini.ts +1 -1
- package/src/providers/anthropic/client.ts +31 -0
- package/src/tools/ui-surface/definitions.ts +0 -43
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/node";
|
|
1
2
|
import { v4 as uuid } from "uuid";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
|
|
5
|
+
import { SurfaceActionSchema } from "../api/events/ui-surface-show.js";
|
|
6
|
+
import { CardSurfaceDataSchema } from "../api/surfaces.js";
|
|
3
7
|
import { isActivationSession } from "../memory/activation-session-store.js";
|
|
4
8
|
import {
|
|
5
9
|
addAppConversationId,
|
|
@@ -70,6 +74,16 @@ import type { TrustContext } from "./trust-context.js";
|
|
|
70
74
|
|
|
71
75
|
const log = getLogger("conversation-surfaces");
|
|
72
76
|
|
|
77
|
+
// Tolerant variant of SurfaceActionSchema for parsing raw model output.
|
|
78
|
+
// The canonical schema rejects unknown style values; this one coerces them
|
|
79
|
+
// to "secondary" so a single mistyped style doesn't drop all actions.
|
|
80
|
+
const ModelActionSchema = SurfaceActionSchema.extend({
|
|
81
|
+
style: z
|
|
82
|
+
.enum(["primary", "secondary", "destructive"])
|
|
83
|
+
.catch("secondary")
|
|
84
|
+
.optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
73
87
|
const MAX_UNDO_DEPTH = 10;
|
|
74
88
|
|
|
75
89
|
/**
|
|
@@ -472,6 +486,27 @@ function normalizeDynamicPageShowData(
|
|
|
472
486
|
return normalized as unknown as DynamicPageSurfaceData;
|
|
473
487
|
}
|
|
474
488
|
|
|
489
|
+
/** First entry that is a non-empty (trimmed) string, else undefined. */
|
|
490
|
+
function firstNonEmptyString(values: unknown[]): string | undefined {
|
|
491
|
+
for (const value of values) {
|
|
492
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** All non-empty (trimmed) strings from the values list. */
|
|
500
|
+
function allNonEmptyStrings(values: unknown[]): string[] {
|
|
501
|
+
const result: string[] = [];
|
|
502
|
+
for (const value of values) {
|
|
503
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
504
|
+
result.push(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
475
510
|
function normalizeCardShowData(
|
|
476
511
|
input: Record<string, unknown>,
|
|
477
512
|
rawData: Record<string, unknown>,
|
|
@@ -507,6 +542,113 @@ function normalizeCardShowData(
|
|
|
507
542
|
normalized.body = input.body;
|
|
508
543
|
}
|
|
509
544
|
|
|
545
|
+
// The model sees every surface type's schema in the ui_show tool description,
|
|
546
|
+
// so it frequently borrows keys from sibling surfaces when emitting a card.
|
|
547
|
+
// Recover those into the canonical card fields, checking both data-level and
|
|
548
|
+
// top-level (input) placement. Multiple matches are concatenated (body) or
|
|
549
|
+
// first-wins (title/subtitle); all alias keys are deleted afterward so they
|
|
550
|
+
// don't appear as droppedKeys noise.
|
|
551
|
+
//
|
|
552
|
+
// body aliases: copy_block's `text`, confirmation's `message`, generic
|
|
553
|
+
// `content`, and cross-surface `description` (choice/form/oauth/work_result/
|
|
554
|
+
// dynamic_page — 5 types use it), work_result's `summary`, confirmation's
|
|
555
|
+
// `detail`.
|
|
556
|
+
const bodyAliasKeys = [
|
|
557
|
+
"text",
|
|
558
|
+
"message",
|
|
559
|
+
"content",
|
|
560
|
+
"description",
|
|
561
|
+
"summary",
|
|
562
|
+
"detail",
|
|
563
|
+
] as const;
|
|
564
|
+
if (typeof normalized.body !== "string" || normalized.body.trim() === "") {
|
|
565
|
+
const candidates = allNonEmptyStrings(
|
|
566
|
+
bodyAliasKeys.map((k) => {
|
|
567
|
+
const dataVal = normalized[k];
|
|
568
|
+
if (typeof dataVal === "string" && dataVal.trim().length > 0)
|
|
569
|
+
return dataVal;
|
|
570
|
+
return input[k];
|
|
571
|
+
}),
|
|
572
|
+
);
|
|
573
|
+
if (candidates.length > 0) {
|
|
574
|
+
// Temporary: concatenate all matching aliases so no content is lost.
|
|
575
|
+
// A future pass should define per-alias semantic roles (e.g. summary
|
|
576
|
+
// as a subtitle, detail as supplementary) once production telemetry
|
|
577
|
+
// reveals which combinations actually occur.
|
|
578
|
+
normalized.body = candidates.join("\n\n");
|
|
579
|
+
const usedAliases = bodyAliasKeys.filter(
|
|
580
|
+
(k) =>
|
|
581
|
+
(typeof normalized[k] === "string" &&
|
|
582
|
+
(normalized[k] as string).trim().length > 0) ||
|
|
583
|
+
(typeof input[k] === "string" &&
|
|
584
|
+
(input[k] as string).trim().length > 0),
|
|
585
|
+
);
|
|
586
|
+
Sentry.addBreadcrumb({
|
|
587
|
+
category: "card-normalization",
|
|
588
|
+
message: `alias recovery: ${usedAliases.join(", ")} → body`,
|
|
589
|
+
level: "info",
|
|
590
|
+
data: { usedAliases, candidateCount: candidates.length },
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const key of bodyAliasKeys) {
|
|
595
|
+
delete normalized[key];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// title aliases: natural synonyms the model reaches for when it doesn't
|
|
599
|
+
// use `title` verbatim.
|
|
600
|
+
const titleAliasKeys = ["heading", "header", "name"] as const;
|
|
601
|
+
if (typeof normalized.title !== "string" || normalized.title.trim() === "") {
|
|
602
|
+
const aliased = firstNonEmptyString([
|
|
603
|
+
...titleAliasKeys.map((k) => normalized[k]),
|
|
604
|
+
...titleAliasKeys.map((k) => input[k]),
|
|
605
|
+
]);
|
|
606
|
+
if (aliased !== undefined) {
|
|
607
|
+
normalized.title = aliased;
|
|
608
|
+
Sentry.addBreadcrumb({
|
|
609
|
+
category: "card-normalization",
|
|
610
|
+
message: `alias recovery: title`,
|
|
611
|
+
level: "info",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const key of titleAliasKeys) {
|
|
616
|
+
delete normalized[key];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// subtitle aliases: table's `caption`, natural synonym `subheading`.
|
|
620
|
+
if (
|
|
621
|
+
typeof normalized.subtitle !== "string" &&
|
|
622
|
+
typeof input.subtitle === "string"
|
|
623
|
+
) {
|
|
624
|
+
normalized.subtitle = input.subtitle;
|
|
625
|
+
}
|
|
626
|
+
const subtitleAliasKeys = ["subheading", "caption"] as const;
|
|
627
|
+
if (
|
|
628
|
+
typeof normalized.subtitle !== "string" ||
|
|
629
|
+
normalized.subtitle.trim() === ""
|
|
630
|
+
) {
|
|
631
|
+
const aliased = firstNonEmptyString([
|
|
632
|
+
...subtitleAliasKeys.map((k) => normalized[k]),
|
|
633
|
+
...subtitleAliasKeys.map((k) => input[k]),
|
|
634
|
+
]);
|
|
635
|
+
if (aliased !== undefined) {
|
|
636
|
+
normalized.subtitle = aliased;
|
|
637
|
+
Sentry.addBreadcrumb({
|
|
638
|
+
category: "card-normalization",
|
|
639
|
+
message: `alias recovery: subtitle`,
|
|
640
|
+
level: "info",
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const key of subtitleAliasKeys) {
|
|
645
|
+
delete normalized[key];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!Array.isArray(normalized.metadata) && Array.isArray(input.metadata)) {
|
|
649
|
+
normalized.metadata = input.metadata;
|
|
650
|
+
}
|
|
651
|
+
|
|
510
652
|
// task_progress cards: additional fallbacks for title from templateData.
|
|
511
653
|
if (
|
|
512
654
|
normalized.template === "task_progress" &&
|
|
@@ -533,7 +675,40 @@ function normalizeCardShowData(
|
|
|
533
675
|
ensureTaskProgressTemplateData(normalized);
|
|
534
676
|
}
|
|
535
677
|
|
|
536
|
-
|
|
678
|
+
// Parse, don't assert. The old `as unknown as CardSurfaceData` accepted any
|
|
679
|
+
// shape, so anything the model nested under an unmodelled key was carried
|
|
680
|
+
// through unread. Parsing draws the boundary; the dropped-key log surfaces
|
|
681
|
+
// the shapes we still don't recover, so the recovery list above can grow from
|
|
682
|
+
// real traffic rather than guesswork.
|
|
683
|
+
const droppedKeys = Object.keys(normalized).filter(
|
|
684
|
+
(key) => !(key in CardSurfaceDataSchema.shape),
|
|
685
|
+
);
|
|
686
|
+
if (droppedKeys.length > 0) {
|
|
687
|
+
log.warn(
|
|
688
|
+
{ droppedKeys },
|
|
689
|
+
"ui_show card data carried keys the card contract does not model; their content will not render",
|
|
690
|
+
);
|
|
691
|
+
Sentry.addBreadcrumb({
|
|
692
|
+
category: "card-normalization",
|
|
693
|
+
message: `dropped keys: ${droppedKeys.join(", ")}`,
|
|
694
|
+
level: "warning",
|
|
695
|
+
data: { droppedKeys },
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const parsed = CardSurfaceDataSchema.safeParse(normalized);
|
|
699
|
+
if (parsed.success) {
|
|
700
|
+
return parsed.data;
|
|
701
|
+
}
|
|
702
|
+
log.warn(
|
|
703
|
+
{ issues: parsed.error.issues },
|
|
704
|
+
"ui_show card data failed CardSurfaceDataSchema; rendering only the fields that validated",
|
|
705
|
+
);
|
|
706
|
+
return CardSurfaceDataSchema.parse({
|
|
707
|
+
title: typeof normalized.title === "string" ? normalized.title : undefined,
|
|
708
|
+
subtitle:
|
|
709
|
+
typeof normalized.subtitle === "string" ? normalized.subtitle : undefined,
|
|
710
|
+
body: typeof normalized.body === "string" ? normalized.body : undefined,
|
|
711
|
+
});
|
|
537
712
|
}
|
|
538
713
|
|
|
539
714
|
function normalizeTaskProgressCardPatch(
|
|
@@ -2693,9 +2868,17 @@ export async function surfaceProxyResolver(
|
|
|
2693
2868
|
const surfaceType = input.surface_type as SurfaceType;
|
|
2694
2869
|
const title = typeof input.title === "string" ? input.title : undefined;
|
|
2695
2870
|
const rawData = isPlainObject(input.data) ? input.data : {};
|
|
2696
|
-
|
|
2871
|
+
// Each surface type that has a canonical Zod schema gets parsed through it;
|
|
2872
|
+
// the rest pass through raw until migrated (LUM-2134 scope). The per-type
|
|
2873
|
+
// normalizers validate+recover; the union cast at the end is only for the
|
|
2874
|
+
// unmigrated branches that still return hand-written interfaces.
|
|
2875
|
+
const cardData =
|
|
2697
2876
|
surfaceType === "card"
|
|
2698
2877
|
? normalizeCardShowData(input, rawData)
|
|
2878
|
+
: undefined;
|
|
2879
|
+
const data: SurfaceData =
|
|
2880
|
+
cardData !== undefined
|
|
2881
|
+
? cardData
|
|
2699
2882
|
: surfaceType === "choice"
|
|
2700
2883
|
? normalizeChoiceShowData(rawData)
|
|
2701
2884
|
: surfaceType === "copy_block"
|
|
@@ -2704,21 +2887,43 @@ export async function surfaceProxyResolver(
|
|
|
2704
2887
|
? normalizeOAuthConnectShowData(rawData)
|
|
2705
2888
|
: surfaceType === "dynamic_page"
|
|
2706
2889
|
? normalizeDynamicPageShowData(input, rawData)
|
|
2707
|
-
: rawData
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2890
|
+
: (rawData as SurfaceData);
|
|
2891
|
+
// Parse actions through the schema instead of typecasting raw model output.
|
|
2892
|
+
// The model may place actions inside `data` instead of the top-level
|
|
2893
|
+
// `actions` param — recover them so they aren't silently dropped.
|
|
2894
|
+
const rawActions = Array.isArray(input.actions)
|
|
2895
|
+
? input.actions
|
|
2896
|
+
: Array.isArray(rawData.actions)
|
|
2897
|
+
? rawData.actions
|
|
2898
|
+
: undefined;
|
|
2899
|
+
let inputActions: z.infer<typeof ModelActionSchema>[] | undefined;
|
|
2900
|
+
if (rawActions) {
|
|
2901
|
+
const valid: z.infer<typeof ModelActionSchema>[] = [];
|
|
2902
|
+
for (const raw of rawActions) {
|
|
2903
|
+
const result = ModelActionSchema.safeParse(raw);
|
|
2904
|
+
if (result.success) {
|
|
2905
|
+
valid.push(result.data);
|
|
2906
|
+
} else {
|
|
2907
|
+
Sentry.addBreadcrumb({
|
|
2908
|
+
category: "card-normalization",
|
|
2909
|
+
message: "action parse failure (individual)",
|
|
2910
|
+
level: "warning",
|
|
2911
|
+
data: {
|
|
2912
|
+
issuePaths: result.error.issues.map((i) => i.path.join(".")),
|
|
2913
|
+
keys:
|
|
2914
|
+
typeof raw === "object" && raw !== null
|
|
2915
|
+
? Object.keys(raw)
|
|
2916
|
+
: [typeof raw],
|
|
2917
|
+
},
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
inputActions = valid.length > 0 ? valid : undefined;
|
|
2922
|
+
}
|
|
2717
2923
|
const actions =
|
|
2718
2924
|
surfaceType === "choice"
|
|
2719
2925
|
? buildChoiceActions(data as ChoiceSurfaceData)
|
|
2720
2926
|
: inputActions;
|
|
2721
|
-
// Interactive surfaces default to awaiting user action.
|
|
2722
2927
|
const hasActions = Array.isArray(actions) && actions.length > 0;
|
|
2723
2928
|
if (surfaceType === "choice" && !hasActions) {
|
|
2724
2929
|
return {
|
|
@@ -2727,6 +2932,39 @@ export async function surfaceProxyResolver(
|
|
|
2727
2932
|
isError: true,
|
|
2728
2933
|
};
|
|
2729
2934
|
}
|
|
2935
|
+
if (cardData !== undefined) {
|
|
2936
|
+
const hasTitle =
|
|
2937
|
+
(typeof title === "string" && title.trim().length > 0) ||
|
|
2938
|
+
(typeof cardData.title === "string" &&
|
|
2939
|
+
cardData.title.trim().length > 0);
|
|
2940
|
+
const hasBody =
|
|
2941
|
+
typeof cardData.body === "string" && cardData.body.trim().length > 0;
|
|
2942
|
+
const hasSubtitle =
|
|
2943
|
+
typeof cardData.subtitle === "string" &&
|
|
2944
|
+
cardData.subtitle.trim().length > 0;
|
|
2945
|
+
const hasMetadata =
|
|
2946
|
+
Array.isArray(cardData.metadata) && cardData.metadata.length > 0;
|
|
2947
|
+
const hasTemplate = typeof cardData.template === "string";
|
|
2948
|
+
if (
|
|
2949
|
+
!hasTitle &&
|
|
2950
|
+
!hasBody &&
|
|
2951
|
+
!hasSubtitle &&
|
|
2952
|
+
!hasMetadata &&
|
|
2953
|
+
!hasTemplate &&
|
|
2954
|
+
!hasActions
|
|
2955
|
+
) {
|
|
2956
|
+
Sentry.addBreadcrumb({
|
|
2957
|
+
category: "card-normalization",
|
|
2958
|
+
message: "empty card rejected",
|
|
2959
|
+
level: "warning",
|
|
2960
|
+
});
|
|
2961
|
+
return {
|
|
2962
|
+
content:
|
|
2963
|
+
"Error: ui_show card requires content — provide `data.body`, a `template` (e.g. task_progress with steps), `data.metadata`, `data.subtitle`, a `title`, or `actions`. The surface was not displayed because it carried no renderable content. Resend ui_show with populated card content.",
|
|
2964
|
+
isError: true,
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2730
2968
|
const oauthProviderKey =
|
|
2731
2969
|
surfaceType === "oauth_connect"
|
|
2732
2970
|
? (data as unknown as Record<string, unknown>).providerKey
|
|
@@ -2741,6 +2979,7 @@ export async function surfaceProxyResolver(
|
|
|
2741
2979
|
isError: true,
|
|
2742
2980
|
};
|
|
2743
2981
|
}
|
|
2982
|
+
|
|
2744
2983
|
const isInteractive =
|
|
2745
2984
|
surfaceType === "card"
|
|
2746
2985
|
? hasActions
|
|
@@ -2777,10 +3016,7 @@ export async function surfaceProxyResolver(
|
|
|
2777
3016
|
const mappedActions = actions?.map((a) => ({
|
|
2778
3017
|
id: a.id,
|
|
2779
3018
|
label: a.label,
|
|
2780
|
-
style:
|
|
2781
|
-
| "primary"
|
|
2782
|
-
| "secondary"
|
|
2783
|
-
| "destructive",
|
|
3019
|
+
style: a.style ?? "secondary",
|
|
2784
3020
|
...(a.data ? { data: a.data } : {}),
|
|
2785
3021
|
}));
|
|
2786
3022
|
|
|
@@ -2896,7 +3132,26 @@ export async function surfaceProxyResolver(
|
|
|
2896
3132
|
const currentHtml = (stored.data as DynamicPageSurfaceData).html;
|
|
2897
3133
|
pushUndoState(ctx.surfaceUndoStacks, surfaceId, currentHtml);
|
|
2898
3134
|
}
|
|
2899
|
-
|
|
3135
|
+
const rawMerged = { ...stored.data, ...patch };
|
|
3136
|
+
if (stored.surfaceType === "card") {
|
|
3137
|
+
// Validate the merged card data through the canonical schema so
|
|
3138
|
+
// malformed patches (e.g. metadata as a string) are caught here
|
|
3139
|
+
// instead of crashing the client's safeParse.
|
|
3140
|
+
const parsed = CardSurfaceDataSchema.safeParse(rawMerged);
|
|
3141
|
+
mergedData = parsed.success
|
|
3142
|
+
? parsed.data
|
|
3143
|
+
: (CardSurfaceDataSchema.safeParse(stored.data).data ?? {});
|
|
3144
|
+
if (!parsed.success) {
|
|
3145
|
+
log.warn(
|
|
3146
|
+
{ surfaceId, issues: parsed.error.issues },
|
|
3147
|
+
"ui_update card patch produced invalid merged data; reverting to stored data",
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
} else {
|
|
3151
|
+
// Other surface types lack canonical Zod schemas (LUM-2134 scope).
|
|
3152
|
+
// The raw merge is the best we can do until they're migrated.
|
|
3153
|
+
mergedData = rawMerged as SurfaceData;
|
|
3154
|
+
}
|
|
2900
3155
|
stored.data = mergedData;
|
|
2901
3156
|
} else {
|
|
2902
3157
|
mergedData = patch as unknown as SurfaceData;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Surface types, UI surface lifecycle messages.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type CardSurfaceData,
|
|
5
|
+
CardSurfaceDataSchema,
|
|
6
|
+
} from "../../api/surfaces.js";
|
|
7
|
+
|
|
8
|
+
// Surface `data` shapes are wire payloads owned by `@vellumai/assistant-api`.
|
|
9
|
+
// Card is migrated (canonical Zod schema); the remaining types below are still
|
|
10
|
+
// hand-written interfaces pending migration. Re-exported so the daemon's
|
|
11
|
+
// surface protocol barrel (`message-protocol.ts`) keeps surfacing them to
|
|
12
|
+
// daemon consumers under their canonical names.
|
|
13
|
+
export { type CardSurfaceData, CardSurfaceDataSchema };
|
|
4
14
|
|
|
5
15
|
// === Surface type definitions ===
|
|
6
16
|
|
|
@@ -37,25 +47,6 @@ export interface SurfaceAction {
|
|
|
37
47
|
data?: Record<string, unknown>;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
/**
|
|
41
|
-
* Card surface data. Defined as a Zod schema so the type is derived (not
|
|
42
|
-
* hand-maintained) and the seed-content-block schema can compose it directly
|
|
43
|
-
* instead of treating card `data` as an opaque record.
|
|
44
|
-
*/
|
|
45
|
-
export const CardSurfaceDataSchema = z.object({
|
|
46
|
-
title: z.string(),
|
|
47
|
-
subtitle: z.string().optional(),
|
|
48
|
-
body: z.string(),
|
|
49
|
-
metadata: z
|
|
50
|
-
.array(z.object({ label: z.string(), value: z.string() }))
|
|
51
|
-
.optional(),
|
|
52
|
-
/** Optional template name for specialized rendering (e.g. "weather_forecast"). */
|
|
53
|
-
template: z.string().optional(),
|
|
54
|
-
/** Arbitrary data consumed by the template renderer. Shape depends on template. */
|
|
55
|
-
templateData: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
-
});
|
|
57
|
-
export type CardSurfaceData = z.infer<typeof CardSurfaceDataSchema>;
|
|
58
|
-
|
|
59
50
|
export interface ChoiceOption {
|
|
60
51
|
id: string;
|
|
61
52
|
title: string;
|
|
@@ -40,7 +40,7 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
|
|
|
40
40
|
this.taskType = options?.taskType;
|
|
41
41
|
this.dimensions = options?.dimensions;
|
|
42
42
|
this.managedBaseUrl = options?.managedBaseUrl;
|
|
43
|
-
this.interCallDelayMs = options?.interCallDelayMs ??
|
|
43
|
+
this.interCallDelayMs = options?.interCallDelayMs ?? 100;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** True when requests route through the managed platform proxy. */
|
|
@@ -835,6 +835,11 @@ export class AnthropicProvider implements Provider {
|
|
|
835
835
|
disableCache: _disableCache,
|
|
836
836
|
max_tokens: callerMaxTokens,
|
|
837
837
|
usageAttributionHeaders,
|
|
838
|
+
// Pulled out of `restConfig` so they are forwarded conditionally below:
|
|
839
|
+
// newer models reject them outright (see `deprecatesSamplingParams`).
|
|
840
|
+
temperature: callerTemperature,
|
|
841
|
+
top_p: callerTopP,
|
|
842
|
+
top_k: callerTopK,
|
|
838
843
|
...restConfig
|
|
839
844
|
} = (config ?? {}) as Record<string, unknown> & {
|
|
840
845
|
// "xhigh" is an intermediate tier between "high" and "max" supported
|
|
@@ -847,6 +852,9 @@ export class AnthropicProvider implements Provider {
|
|
|
847
852
|
speed?: "standard" | "fast";
|
|
848
853
|
output_config?: Record<string, unknown>;
|
|
849
854
|
usageAttributionHeaders?: Record<string, string>;
|
|
855
|
+
temperature?: number;
|
|
856
|
+
top_p?: number;
|
|
857
|
+
top_k?: number;
|
|
850
858
|
};
|
|
851
859
|
// Haiku does not support the effort / output_config parameter or
|
|
852
860
|
// extended cache TTL betas.
|
|
@@ -856,6 +864,16 @@ export class AnthropicProvider implements Provider {
|
|
|
856
864
|
(restConfig as Record<string, unknown>).model?.toString() ?? this.model;
|
|
857
865
|
const isHaiku = effectiveModel.includes("haiku");
|
|
858
866
|
const supportsEffort = !isHaiku;
|
|
867
|
+
// opus-4-7 / opus-4-8 reject `temperature` and `top_p` with a 400
|
|
868
|
+
// "`temperature`/`top_p` is deprecated for this model" — model-wide, not
|
|
869
|
+
// effort-conditional (verified 2026-06-23). opus-4-6 / sonnet-4-6 /
|
|
870
|
+
// haiku-4-5 still accept them. fable-5 is included conservatively (a
|
|
871
|
+
// frontier model that could not be verified directly but follows the same
|
|
872
|
+
// deprecation direction). Stripping the params here keeps callers that set
|
|
873
|
+
// them (e.g. the memory-v3 L2 selector's `temperature: 0`) from 400ing.
|
|
874
|
+
const deprecatesSamplingParams =
|
|
875
|
+
/claude-opus-4-[78]\b/.test(effectiveModel) ||
|
|
876
|
+
effectiveModel.startsWith("claude-fable-");
|
|
859
877
|
const mergedOutputConfig = {
|
|
860
878
|
...(output_config ?? {}),
|
|
861
879
|
...(effort && effort !== "none" && supportsEffort
|
|
@@ -883,6 +901,19 @@ export class AnthropicProvider implements Provider {
|
|
|
883
901
|
: 64000,
|
|
884
902
|
messages: sentMessages,
|
|
885
903
|
...restConfig,
|
|
904
|
+
// Forward `temperature` / `top_p` / `top_k` only to models that still
|
|
905
|
+
// accept them; newer models 400 on any of the deprecated sampler params.
|
|
906
|
+
// `temperature: 0` is preserved for accepting models (a `typeof ===
|
|
907
|
+
// "number"` check, not truthiness).
|
|
908
|
+
...(deprecatesSamplingParams
|
|
909
|
+
? {}
|
|
910
|
+
: {
|
|
911
|
+
...(typeof callerTemperature === "number"
|
|
912
|
+
? { temperature: callerTemperature }
|
|
913
|
+
: {}),
|
|
914
|
+
...(typeof callerTopP === "number" ? { top_p: callerTopP } : {}),
|
|
915
|
+
...(typeof callerTopK === "number" ? { top_k: callerTopK } : {}),
|
|
916
|
+
}),
|
|
886
917
|
...(Object.keys(mergedOutputConfig).length > 0
|
|
887
918
|
? { output_config: mergedOutputConfig }
|
|
888
919
|
: {}),
|
|
@@ -45,14 +45,6 @@ function proxyExecute(toolName: string) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (toolName === "ui_show" && isEmptyCard(input)) {
|
|
49
|
-
return {
|
|
50
|
-
content:
|
|
51
|
-
"Error: ui_show card requires content — provide `data.body`, a `template` (e.g. task_progress with steps), `data.metadata`, or `actions`. The surface was not displayed because it carried only a title, which renders as a blank box. Resend ui_show with populated card content.",
|
|
52
|
-
isError: true,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
48
|
if (toolName === "ui_show" && isDynamicPageAppSubstitute(input)) {
|
|
57
49
|
return {
|
|
58
50
|
content:
|
|
@@ -168,41 +160,6 @@ function isEmptyDynamicPage(input: Record<string, unknown>): boolean {
|
|
|
168
160
|
return typeof html !== "string" || html.trim().length === 0;
|
|
169
161
|
}
|
|
170
162
|
|
|
171
|
-
/**
|
|
172
|
-
* A `card` ui_show carrying no renderable content — only a title (or nothing)
|
|
173
|
-
* — renders as a blank bordered box. A declared `template` (task_progress,
|
|
174
|
-
* weather_forecast, …) renders its own shell, and `body`/`subtitle`/`metadata`/
|
|
175
|
-
* `actions` are real content; any of those passes. The model places these
|
|
176
|
-
* either nested in `data` or at the top level, so both are checked. Title is
|
|
177
|
-
* intentionally not content: a title-only card is the blank box.
|
|
178
|
-
*/
|
|
179
|
-
function isEmptyCard(input: Record<string, unknown>): boolean {
|
|
180
|
-
if (input.surface_type !== "card") {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
const data = asRecord(input.data) ?? {};
|
|
184
|
-
|
|
185
|
-
const template =
|
|
186
|
-
nonEmptyString(input.template) ?? nonEmptyString(data.template);
|
|
187
|
-
if (template) {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const hasBody = !!(nonEmptyString(input.body) ?? nonEmptyString(data.body));
|
|
192
|
-
const hasSubtitle = !!nonEmptyString(data.subtitle);
|
|
193
|
-
const hasMetadata = Array.isArray(data.metadata) && data.metadata.length > 0;
|
|
194
|
-
const actions = input.actions ?? data.actions;
|
|
195
|
-
const hasActions = Array.isArray(actions) && actions.length > 0;
|
|
196
|
-
|
|
197
|
-
return !(hasBody || hasSubtitle || hasMetadata || hasActions);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function nonEmptyString(value: unknown): string | undefined {
|
|
201
|
-
return typeof value === "string" && value.trim().length > 0
|
|
202
|
-
? value
|
|
203
|
-
: undefined;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
163
|
function isDynamicPageAppSubstitute(input: Record<string, unknown>): boolean {
|
|
207
164
|
if (input.surface_type !== "dynamic_page") {
|
|
208
165
|
return false;
|