@larkup/workspace-server 0.0.0-stage → 0.2.1
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/LICENSE +176 -0
- package/README.md +2 -2
- package/dist-types/app/api/chat/feedback/route.d.ts +4 -0
- package/dist-types/app/api/chat/feedback/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/route.d.ts +3 -0
- package/dist-types/app/api/chat/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/tools.d.ts +67 -0
- package/dist-types/app/api/chat/tools.d.ts.map +1 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts +3 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts.map +1 -0
- package/dist-types/app/api/config/verify/route.d.ts +3 -0
- package/dist-types/app/api/config/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/describe-image/route.d.ts +4 -0
- package/dist-types/app/api/describe-image/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts +9 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts +2 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts.map +1 -0
- package/dist-types/app/api/document/sign/route.d.ts +2 -0
- package/dist-types/app/api/document/sign/route.d.ts.map +1 -0
- package/dist-types/app/api/files/remote/route.d.ts +5 -0
- package/dist-types/app/api/files/remote/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts +9 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts +8 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/route.d.ts +4 -0
- package/dist-types/app/api/mcp/route.d.ts.map +1 -0
- package/dist-types/app/api/parse-file/route.d.ts +6 -0
- package/dist-types/app/api/parse-file/route.d.ts.map +1 -0
- package/dist-types/app/api/pdf-page/route.d.ts +4 -0
- package/dist-types/app/api/pdf-page/route.d.ts.map +1 -0
- package/dist-types/app/api/proxy/verify/route.d.ts +4 -0
- package/dist-types/app/api/proxy/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/search/bing/route.d.ts +5 -0
- package/dist-types/app/api/search/bing/route.d.ts.map +1 -0
- package/dist-types/app/api/search/brave/route.d.ts +5 -0
- package/dist-types/app/api/search/brave/route.d.ts.map +1 -0
- package/dist-types/app/api/search/exa/route.d.ts +5 -0
- package/dist-types/app/api/search/exa/route.d.ts.map +1 -0
- package/dist-types/app/api/search/google/route.d.ts +30 -0
- package/dist-types/app/api/search/google/route.d.ts.map +1 -0
- package/dist-types/app/api/search/route.d.ts +7 -0
- package/dist-types/app/api/search/route.d.ts.map +1 -0
- package/dist-types/app/api/search/tavily/route.d.ts +5 -0
- package/dist-types/app/api/search/tavily/route.d.ts.map +1 -0
- package/dist-types/app/api/search/verify/route.d.ts +4 -0
- package/dist-types/app/api/search/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/tabular/route.d.ts +9 -0
- package/dist-types/app/api/tabular/route.d.ts.map +1 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts +10 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts.map +1 -0
- package/dist-types/lib/chat/conversation-memory.d.ts +80 -0
- package/dist-types/lib/chat/conversation-memory.d.ts.map +1 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts +7 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts.map +1 -0
- package/dist-types/lib/chat/image-description.d.ts +10 -0
- package/dist-types/lib/chat/image-description.d.ts.map +1 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts +14 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/media-source-routing.d.ts +53 -0
- package/dist-types/lib/chat/media-source-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/message-input.d.ts +8 -0
- package/dist-types/lib/chat/message-input.d.ts.map +1 -0
- package/dist-types/lib/chat/model-provider.d.ts +23 -0
- package/dist-types/lib/chat/model-provider.d.ts.map +1 -0
- package/dist-types/lib/chat/response-style.d.ts +2 -0
- package/dist-types/lib/chat/response-style.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts +32 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts +39 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts +15 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts +12 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-routing.d.ts +41 -0
- package/dist-types/lib/chat/tabular-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts +33 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts +31 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts +11 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-context.d.ts +72 -0
- package/dist-types/lib/chat/tool-context.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-registry.d.ts +3 -0
- package/dist-types/lib/chat/tool-registry.d.ts.map +1 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts +33 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/visual-routing.d.ts +53 -0
- package/dist-types/lib/chat/visual-routing.d.ts.map +1 -0
- package/dist-types/lib/enterprise-client.d.ts +31 -0
- package/dist-types/lib/enterprise-client.d.ts.map +1 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts +7 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts.map +1 -0
- package/dist-types/lib/media/knowledge.d.ts +47 -0
- package/dist-types/lib/media/knowledge.d.ts.map +1 -0
- package/dist-types/lib/media/pdf-inspection.d.ts +30 -0
- package/dist-types/lib/media/pdf-inspection.d.ts.map +1 -0
- package/dist-types/lib/media/source-identity.d.ts +12 -0
- package/dist-types/lib/media/source-identity.d.ts.map +1 -0
- package/dist-types/lib/media/source-utils.d.ts +157 -0
- package/dist-types/lib/media/source-utils.d.ts.map +1 -0
- package/dist-types/lib/media/video/quick-look.d.ts +60 -0
- package/dist-types/lib/media/video/quick-look.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts +521 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts +32 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts.map +1 -0
- package/dist-types/lib/remote-files.d.ts +12 -0
- package/dist-types/lib/remote-files.d.ts.map +1 -0
- package/dist-types/lib/search/provider-verification.d.ts +5 -0
- package/dist-types/lib/search/provider-verification.d.ts.map +1 -0
- package/dist-types/runtime-host.d.ts +20 -0
- package/dist-types/runtime-host.d.ts.map +1 -0
- package/package.json +338 -3
- package/src/app/api/chat/feedback/route.ts +77 -0
- package/src/app/api/chat/route.ts +2277 -0
- package/src/app/api/chat/tools.ts +3563 -0
- package/src/app/api/config/test-embedding/route.ts +39 -0
- package/src/app/api/config/verify/route.ts +309 -0
- package/src/app/api/describe-image/route.ts +108 -0
- package/src/app/api/doc-editor/edit/route.ts +87 -0
- package/src/app/api/doc-editor/export/route.ts +32 -0
- package/src/app/api/doc-editor/parse/route.ts +74 -0
- package/src/app/api/doc-editor/restore/route.ts +70 -0
- package/src/app/api/document/sign/route.ts +55 -0
- package/src/app/api/files/remote/route.ts +32 -0
- package/src/app/api/mcp/[connectionId]/route.ts +29 -0
- package/src/app/api/mcp/[connectionId]/test/route.ts +13 -0
- package/src/app/api/mcp/route.ts +22 -0
- package/src/app/api/parse-file/route.ts +86 -0
- package/src/app/api/pdf-page/route.ts +40 -0
- package/src/app/api/proxy/verify/route.ts +78 -0
- package/src/app/api/search/bing/route.ts +86 -0
- package/src/app/api/search/brave/route.ts +88 -0
- package/src/app/api/search/exa/route.ts +100 -0
- package/src/app/api/search/google/route.ts +147 -0
- package/src/app/api/search/route.ts +28 -0
- package/src/app/api/search/tavily/route.ts +93 -0
- package/src/app/api/search/verify/route.ts +27 -0
- package/src/app/api/tabular/route.ts +68 -0
- package/src/lib/chat/assistant-source-scope.ts +32 -0
- package/src/lib/chat/conversation-memory.ts +621 -0
- package/src/lib/chat/gateway-fallbacks.ts +12 -0
- package/src/lib/chat/image-description.ts +21 -0
- package/src/lib/chat/media-retrieval-routing.ts +37 -0
- package/src/lib/chat/media-source-routing.ts +100 -0
- package/src/lib/chat/message-input.ts +48 -0
- package/src/lib/chat/model-provider.ts +157 -0
- package/src/lib/chat/response-style.ts +3 -0
- package/src/lib/chat/retrieval-ranking.ts +126 -0
- package/src/lib/chat/retrieval-routing.ts +221 -0
- package/src/lib/chat/tabular-dataset-routing.ts +93 -0
- package/src/lib/chat/tabular-query-plan.ts +195 -0
- package/src/lib/chat/tabular-routing.ts +122 -0
- package/src/lib/chat/tabular-sandbox-files.ts +78 -0
- package/src/lib/chat/tabular-visualization.ts +165 -0
- package/src/lib/chat/tool-call-repair.ts +84 -0
- package/src/lib/chat/tool-context.ts +1222 -0
- package/src/lib/chat/tool-registry.ts +6 -0
- package/src/lib/chat/video-rag-routing.ts +149 -0
- package/src/lib/chat/visual-routing.ts +225 -0
- package/src/lib/enterprise-client.ts +96 -0
- package/src/lib/marketplace/tool-runtime-config.ts +155 -0
- package/src/lib/media/knowledge.ts +489 -0
- package/src/lib/media/pdf-inspection.ts +280 -0
- package/src/lib/media/source-identity.ts +50 -0
- package/src/lib/media/source-utils.ts +1156 -0
- package/src/lib/media/video/quick-look.ts +368 -0
- package/src/lib/media/video-intelligence-adapter.ts +1946 -0
- package/src/lib/media/video-intelligence-connection.ts +151 -0
- package/src/lib/remote-files.ts +182 -0
- package/src/lib/search/provider-verification.ts +95 -0
- package/src/runtime-host.ts +31 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Remove optional capabilities that are unavailable in the current runtime. */
|
|
2
|
+
export function executableTools<T extends Record<string, unknown>>(candidates: T) {
|
|
3
|
+
return Object.fromEntries(
|
|
4
|
+
Object.entries(candidates).filter(([, definition]) => definition != null),
|
|
5
|
+
) as { [K in keyof T as T[K] extends null | undefined ? never : K]: NonNullable<T[K]> };
|
|
6
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export type VideoEvidenceVerificationStatus =
|
|
2
|
+
'supported' | 'conflicted' | 'insufficient' | 'needs_inspection';
|
|
3
|
+
|
|
4
|
+
export interface IndexedVideoAnswerSignal {
|
|
5
|
+
modality: string;
|
|
6
|
+
text: string;
|
|
7
|
+
confidenceScore: number;
|
|
8
|
+
startSecs: number;
|
|
9
|
+
endSecs: number;
|
|
10
|
+
conflict?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Concise inventory records average well under a paragraph. This ceiling is
|
|
14
|
+
// high enough for long recordings while still bounding the answer model's
|
|
15
|
+
// context and the browser payload.
|
|
16
|
+
export const MAX_EXHAUSTIVE_EVIDENCE_ITEMS = 800;
|
|
17
|
+
// Structured inventories are rendered by the browser's paginated data table,
|
|
18
|
+
// never supplied to the answer model. They therefore deserve a much higher
|
|
19
|
+
// transport limit than prose evidence while still protecting a chat response
|
|
20
|
+
// from an unbounded malformed source.
|
|
21
|
+
export const MAX_EXHAUSTIVE_TABLE_ROWS = 10_000;
|
|
22
|
+
|
|
23
|
+
/** Follow a tool-owned chronological cursor without relying on model tool use. */
|
|
24
|
+
export async function collectExhaustiveVideoEvidencePages(
|
|
25
|
+
execute: (input: Record<string, unknown>, options: { toolCallId: string }) => Promise<unknown>,
|
|
26
|
+
input: { mediaAssetId: string; query: string },
|
|
27
|
+
firstPage: unknown,
|
|
28
|
+
toolCallId: string,
|
|
29
|
+
) {
|
|
30
|
+
if (!firstPage || typeof firstPage !== 'object' || Array.isArray(firstPage)) return firstPage;
|
|
31
|
+
const first = firstPage as Record<string, any>;
|
|
32
|
+
if (first.continuation?.exhaustive !== true || first.continuation?.hasMore !== true) {
|
|
33
|
+
return firstPage;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const evidence = Array.isArray(first.evidence) ? [...first.evidence] : [];
|
|
37
|
+
const seen = new Set(evidence.map((item: any) => item?.id ?? item?.evidenceId).filter(Boolean));
|
|
38
|
+
const rows = Array.isArray(first.rows) ? [...first.rows] : [];
|
|
39
|
+
const seenRows = new Set(
|
|
40
|
+
rows.map((row: any) => row?.id ?? row?._rowId).filter((id: unknown) => id != null),
|
|
41
|
+
);
|
|
42
|
+
const hasTable = Array.isArray(first.rows);
|
|
43
|
+
let continuation = first.continuation;
|
|
44
|
+
let lastPage = first;
|
|
45
|
+
|
|
46
|
+
while (
|
|
47
|
+
continuation?.hasMore === true &&
|
|
48
|
+
Number.isFinite(continuation.nextCursor) &&
|
|
49
|
+
(hasTable
|
|
50
|
+
? rows.length < MAX_EXHAUSTIVE_TABLE_ROWS
|
|
51
|
+
: evidence.length < MAX_EXHAUSTIVE_EVIDENCE_ITEMS)
|
|
52
|
+
) {
|
|
53
|
+
const cursor = Number(continuation.nextCursor);
|
|
54
|
+
const next = await execute({ ...input, exhaustive: true, cursor, limit: 48 }, { toolCallId });
|
|
55
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) break;
|
|
56
|
+
const page = next as Record<string, any>;
|
|
57
|
+
for (const item of Array.isArray(page.evidence) ? page.evidence : []) {
|
|
58
|
+
const id = item?.id ?? item?.evidenceId;
|
|
59
|
+
if (id && seen.has(id)) continue;
|
|
60
|
+
if (id) seen.add(id);
|
|
61
|
+
evidence.push(item);
|
|
62
|
+
if (evidence.length >= MAX_EXHAUSTIVE_EVIDENCE_ITEMS) break;
|
|
63
|
+
}
|
|
64
|
+
if (hasTable) {
|
|
65
|
+
for (const row of Array.isArray(page.rows) ? page.rows : []) {
|
|
66
|
+
const id = row?.id ?? row?._rowId;
|
|
67
|
+
if (id != null && seenRows.has(id)) continue;
|
|
68
|
+
if (id != null) seenRows.add(id);
|
|
69
|
+
rows.push(row);
|
|
70
|
+
if (rows.length >= MAX_EXHAUSTIVE_TABLE_ROWS) break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
lastPage = page;
|
|
74
|
+
continuation = page.continuation;
|
|
75
|
+
if (!continuation || Number(continuation.nextCursor) <= cursor) break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...first,
|
|
80
|
+
evidence,
|
|
81
|
+
...(hasTable
|
|
82
|
+
? {
|
|
83
|
+
columns: Array.isArray(first.columns) ? first.columns : [],
|
|
84
|
+
rows,
|
|
85
|
+
totalRows: rows.length,
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
continuation: {
|
|
89
|
+
...(lastPage.continuation ?? continuation ?? first.continuation),
|
|
90
|
+
aggregatedItems: hasTable ? rows.length : evidence.length,
|
|
91
|
+
contextLimitReached:
|
|
92
|
+
(hasTable
|
|
93
|
+
? rows.length >= MAX_EXHAUSTIVE_TABLE_ROWS
|
|
94
|
+
: evidence.length >= MAX_EXHAUSTIVE_EVIDENCE_ITEMS) && continuation?.hasMore === true,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Decide whether chat can answer from the immutable index before paying for a
|
|
101
|
+
* live source read. This gate is intentionally content-agnostic: it evaluates
|
|
102
|
+
* provenance, confidence, temporal coverage, and retrieval agreement rather
|
|
103
|
+
* than recognizing any particular kind of recording.
|
|
104
|
+
*/
|
|
105
|
+
export function indexedVideoEvidenceIsSufficient(input: {
|
|
106
|
+
verificationStatus: VideoEvidenceVerificationStatus;
|
|
107
|
+
questionKinds: string[];
|
|
108
|
+
evidence: IndexedVideoAnswerSignal[];
|
|
109
|
+
focusSources?: string[];
|
|
110
|
+
durationSecs?: number;
|
|
111
|
+
hierarchyRanges?: number;
|
|
112
|
+
}) {
|
|
113
|
+
if (input.verificationStatus !== 'supported') return false;
|
|
114
|
+
const usable = input.evidence.filter(
|
|
115
|
+
(item) => !item.conflict && item.confidenceScore >= 0.5 && item.text.trim().length > 0,
|
|
116
|
+
);
|
|
117
|
+
if (usable.some((item) => /Claim verdict:\s*direct/i.test(item.text))) return true;
|
|
118
|
+
|
|
119
|
+
const descriptive = usable.filter((item) => ['visual', 'computed'].includes(item.modality));
|
|
120
|
+
const nonLexicalAgreement = (input.focusSources ?? []).some((source) => source !== 'lexical');
|
|
121
|
+
|
|
122
|
+
// A reconciled computed account is created from timestamped source evidence,
|
|
123
|
+
// not a remembered answer. It is the index's strongest reusable result.
|
|
124
|
+
if (descriptive.some((item) => item.modality === 'computed')) return true;
|
|
125
|
+
|
|
126
|
+
if (input.questionKinds.includes('state-change') || input.questionKinds.includes('outcome')) {
|
|
127
|
+
const duration = input.durationSecs;
|
|
128
|
+
const moments = new Set(descriptive.map((item) => Math.floor(item.startSecs / 15))).size;
|
|
129
|
+
const buckets = duration && duration > 0 ? 5 : 1;
|
|
130
|
+
const coveredBuckets = new Set(
|
|
131
|
+
descriptive.map((item) =>
|
|
132
|
+
duration && duration > 0
|
|
133
|
+
? Math.min(buckets - 1, Math.floor((item.startSecs / duration) * buckets))
|
|
134
|
+
: 0,
|
|
135
|
+
),
|
|
136
|
+
).size;
|
|
137
|
+
|
|
138
|
+
// For outcome questions, if we have broad temporal coverage of the video,
|
|
139
|
+
// the index is sufficient without needing to rewatch the final 10 seconds blindly.
|
|
140
|
+
if (moments >= 3 && coveredBuckets / buckets >= 0.6 && (input.hierarchyRanges ?? 0) > 0) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A semantically located visual account can answer a focused appearance,
|
|
146
|
+
// comparison, count, or outcome question immediately. OCR/transcript-only
|
|
147
|
+
// neighbours do not pass this gate and therefore still trigger a close read.
|
|
148
|
+
return descriptive.length > 0 && nonLexicalAgreement;
|
|
149
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { searchTerms } from './retrieval-ranking';
|
|
2
|
+
|
|
3
|
+
type ImageDocument = {
|
|
4
|
+
metadata?: { images?: any[]; imageUrl?: unknown; index?: unknown; pageNumber?: unknown };
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type RetrievedPdfSource = {
|
|
8
|
+
documentId: string;
|
|
9
|
+
pageNumber?: number;
|
|
10
|
+
title?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// A numbered object reference is stronger than a page number attached to a
|
|
14
|
+
// vector hit. That hit may be an incidental mention, whereas this is an
|
|
15
|
+
// explicit request for an object that the local PDF index can locate by its
|
|
16
|
+
// own page text and caption.
|
|
17
|
+
const NUMBERED_DOCUMENT_REFERENCE =
|
|
18
|
+
/\b(?:fig(?:ure)?|table|eq(?:uation)?|section|chapter|appendix)\.?\s*(?:no\.?\s*)?\(?\d+(?:\.\d+)+\b/iu;
|
|
19
|
+
|
|
20
|
+
/** Resolve both current parent-document images and legacy standalone image records. */
|
|
21
|
+
export function findIndexedImageSource<T extends ImageDocument>(documents: T[], imageUrl: string) {
|
|
22
|
+
const source = documents.find(
|
|
23
|
+
(document) =>
|
|
24
|
+
(Array.isArray(document.metadata?.images) &&
|
|
25
|
+
document.metadata.images.some((image: any) => image?.imageUrl === imageUrl)) ||
|
|
26
|
+
document.metadata?.imageUrl === imageUrl,
|
|
27
|
+
);
|
|
28
|
+
const image =
|
|
29
|
+
source?.metadata?.images?.find((candidate: any) => candidate?.imageUrl === imageUrl) ??
|
|
30
|
+
(source?.metadata?.imageUrl === imageUrl
|
|
31
|
+
? {
|
|
32
|
+
imageUrl,
|
|
33
|
+
index: source.metadata.index,
|
|
34
|
+
pageNumber: source.metadata.pageNumber,
|
|
35
|
+
}
|
|
36
|
+
: undefined);
|
|
37
|
+
return image ? { source, image } : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function indexedImageDescriptions(value: unknown): string[] {
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
try {
|
|
43
|
+
return indexedImageDescriptions(JSON.parse(value));
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) return value.flatMap(indexedImageDescriptions);
|
|
49
|
+
if (!value || typeof value !== 'object') return [];
|
|
50
|
+
const record = value as Record<string, unknown>;
|
|
51
|
+
const here = Array.isArray(record.images)
|
|
52
|
+
? record.images.flatMap((image) => {
|
|
53
|
+
if (!image || typeof image !== 'object') return [];
|
|
54
|
+
const description = (image as { description?: unknown }).description;
|
|
55
|
+
return typeof description === 'string' && description.trim() ? [description] : [];
|
|
56
|
+
})
|
|
57
|
+
: [];
|
|
58
|
+
return [...here, ...Object.values(record).flatMap(indexedImageDescriptions)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Recognizes image-bearing retrieval results, including compact JSON envelopes. */
|
|
62
|
+
export function hasRetrievedImageEvidence(value: unknown): boolean {
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
try {
|
|
65
|
+
return hasRetrievedImageEvidence(JSON.parse(value));
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) return value.some(hasRetrievedImageEvidence);
|
|
71
|
+
if (!value || typeof value !== 'object') return false;
|
|
72
|
+
const record = value as Record<string, unknown>;
|
|
73
|
+
if (Array.isArray(record.images) && record.images.length > 0) return true;
|
|
74
|
+
return Object.values(record).some(hasRetrievedImageEvidence);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Finds a PDF source returned by retrieval, even when it has no pre-indexed images. */
|
|
78
|
+
export function hasRetrievedPdfEvidence(value: unknown): boolean {
|
|
79
|
+
if (findRetrievedPdfSource(value)) return true;
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
try {
|
|
82
|
+
return hasRetrievedPdfEvidence(JSON.parse(value));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) return value.some(hasRetrievedPdfEvidence);
|
|
88
|
+
if (!value || typeof value !== 'object') return false;
|
|
89
|
+
const record = value as Record<string, unknown>;
|
|
90
|
+
const metadata =
|
|
91
|
+
record.metadata && typeof record.metadata === 'object'
|
|
92
|
+
? (record.metadata as Record<string, unknown>)
|
|
93
|
+
: undefined;
|
|
94
|
+
return (
|
|
95
|
+
(typeof record.url === 'string' && /\.pdf(?:$|[?#])/i.test(record.url)) ||
|
|
96
|
+
(typeof record.title === 'string' && /\.pdf(?:$|[?#])/i.test(record.title)) ||
|
|
97
|
+
record.mimeType === 'application/pdf' ||
|
|
98
|
+
metadata?.mimeType === 'application/pdf' ||
|
|
99
|
+
metadata?.fileKind === 'pdf' ||
|
|
100
|
+
(typeof metadata?.originalFile === 'string' &&
|
|
101
|
+
/\.pdf(?:$|[?#])/i.test(metadata.originalFile)) ||
|
|
102
|
+
Object.values(record).some(hasRetrievedPdfEvidence)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function positivePageNumber(value: unknown): number | undefined {
|
|
107
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Whether the request names a stable, numbered object inside a document. */
|
|
111
|
+
export function hasNumberedDocumentReference(text: string): boolean {
|
|
112
|
+
return NUMBERED_DOCUMENT_REFERENCE.test(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Return the nearest named document object from a chronological text history. */
|
|
116
|
+
export function latestNumberedDocumentReferenceText(texts: readonly string[]): string | undefined {
|
|
117
|
+
for (let index = texts.length - 1; index >= 0; index -= 1) {
|
|
118
|
+
if (hasNumberedDocumentReference(texts[index])) return texts[index];
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* An explicit document reference must be ranked against the original PDF.
|
|
125
|
+
* Otherwise, a page attached to the retrieval hit is an efficient exact hint.
|
|
126
|
+
*/
|
|
127
|
+
export function preferredPdfPagesForInspection(
|
|
128
|
+
retrievedPageNumber: number | undefined,
|
|
129
|
+
question: string,
|
|
130
|
+
): number[] | undefined {
|
|
131
|
+
if (hasNumberedDocumentReference(question)) return undefined;
|
|
132
|
+
return retrievedPageNumber ? [retrievedPageNumber] : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pdfSourceFromRecord(value: unknown): RetrievedPdfSource | undefined {
|
|
136
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
137
|
+
const record = value as Record<string, any>;
|
|
138
|
+
if (typeof record.documentId !== 'string' || !record.documentId) return undefined;
|
|
139
|
+
const metadata = record.metadata && typeof record.metadata === 'object' ? record.metadata : {};
|
|
140
|
+
const isPdf =
|
|
141
|
+
(typeof record.url === 'string' && /\.pdf(?:$|[?#])/i.test(record.url)) ||
|
|
142
|
+
(typeof record.title === 'string' && /\.pdf(?:$|[?#])/i.test(record.title)) ||
|
|
143
|
+
record.mimeType === 'application/pdf' ||
|
|
144
|
+
metadata.mimeType === 'application/pdf' ||
|
|
145
|
+
metadata.fileKind === 'pdf' ||
|
|
146
|
+
(typeof metadata.originalFile === 'string' && /\.pdf(?:$|[?#])/i.test(metadata.originalFile));
|
|
147
|
+
if (!isPdf) return undefined;
|
|
148
|
+
return {
|
|
149
|
+
documentId: record.documentId,
|
|
150
|
+
pageNumber: positivePageNumber(record.pageNumber) ?? positivePageNumber(metadata.pageNumber),
|
|
151
|
+
title: typeof record.title === 'string' ? record.title : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Select the leading PDF source returned by retrieval. A direct visual chunk
|
|
157
|
+
* carries its page number, so it is preferred over a document-level text
|
|
158
|
+
* chunk that would otherwise make a preview fall back to page one.
|
|
159
|
+
*/
|
|
160
|
+
export function findRetrievedPdfSource(value: unknown): RetrievedPdfSource | undefined {
|
|
161
|
+
if (typeof value === 'string') {
|
|
162
|
+
try {
|
|
163
|
+
return findRetrievedPdfSource(JSON.parse(value));
|
|
164
|
+
} catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
169
|
+
const record = value as Record<string, unknown>;
|
|
170
|
+
const hits = Array.isArray(record.hits) ? record.hits : [];
|
|
171
|
+
const sources = hits.map(pdfSourceFromRecord).filter(Boolean) as RetrievedPdfSource[];
|
|
172
|
+
return sources.find((source) => source.pageNumber !== undefined) ?? sources[0];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reuse relevant descriptions produced during image indexing, and reserve a
|
|
177
|
+
* fresh vision pass for details those descriptions do not cover.
|
|
178
|
+
*/
|
|
179
|
+
export function shouldInspectRetrievedImage(text: string, evidence: unknown): boolean {
|
|
180
|
+
if (!text.trim() || !hasRetrievedImageEvidence(evidence) || requestsImagePresentation(text)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Captions are useful for orientation, but they cannot prove an exhaustive
|
|
185
|
+
// visual request. A diagram question that asks for every label, a count, or
|
|
186
|
+
// an exact name must receive one bounded image read; otherwise a partial
|
|
187
|
+
// caption can make the assistant confidently omit the information the user
|
|
188
|
+
// actually asked it to enumerate.
|
|
189
|
+
if (
|
|
190
|
+
/\b(?:every|all|each|list|enumerate|count|how many|number of)\b|\b(?:view|routine|table|column|field|label|name)s?\b/i.test(
|
|
191
|
+
text,
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const descriptions = indexedImageDescriptions(evidence).join(' ').toLocaleLowerCase();
|
|
198
|
+
if (!descriptions) return true;
|
|
199
|
+
const terms = searchTerms(text);
|
|
200
|
+
const matched = terms.filter((term) => descriptions.includes(term)).length;
|
|
201
|
+
|
|
202
|
+
// Image indexing already performed the expensive visual read. Reuse it when
|
|
203
|
+
// the stored description substantially covers this question; only reopen a
|
|
204
|
+
// vision call when the retrieval note does not address the requested detail.
|
|
205
|
+
return terms.length > 0 && matched / terms.length < 0.35;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Generic presentation intent, shared by PDFs, screenshots, scans, and media frames. */
|
|
209
|
+
export function requestsImagePresentation(text: string): boolean {
|
|
210
|
+
return /\b(?:show|preview|display|open|view|see|need|give|provide|render)\b[\s\S]{0,40}\b(?:image|picture|diagram|page|visual|figure|it)\b|\b(?:image|picture|diagram|page|visual|figure)\s+(?:preview|view)\b/i.test(
|
|
211
|
+
text,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Page text is enough for ordinary prose. These requests explicitly depend on
|
|
217
|
+
* page layout, notation, or a visual object, so a rendered-page read is the
|
|
218
|
+
* higher-fidelity source of truth. The language is document-generic rather
|
|
219
|
+
* than tied to any particular paper or diagram format.
|
|
220
|
+
*/
|
|
221
|
+
export function requiresPdfVisualAnalysis(text: string): boolean {
|
|
222
|
+
return /\b(?:figure|fig\.?|diagram|chart|graph|image|picture|visual|table|equation|formula|notation|layout|caption|legend|label|matrix)\b|\\(?:frac|sqrt|sum|alpha|beta|gamma|delta|theta|[()[\]{}])|[∂∑√ᵀ]/i.test(
|
|
223
|
+
text,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
type EnterpriseProfile = {
|
|
2
|
+
dashboardUrl: string;
|
|
3
|
+
clientKey: string;
|
|
4
|
+
managedToolIds: string[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type EnterpriseToolDescriptor = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
ui: 'notice' | 'table' | 'card';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function profile(config: any): EnterpriseProfile | null {
|
|
15
|
+
const enterprise = config?.enterprise as EnterpriseProfile | undefined;
|
|
16
|
+
if (!enterprise?.dashboardUrl || !enterprise.clientKey) return null;
|
|
17
|
+
return enterprise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function enterpriseFetch(config: any, path: string, init?: RequestInit) {
|
|
21
|
+
const enterprise = profile(config);
|
|
22
|
+
if (!enterprise) return null;
|
|
23
|
+
const url = new URL(path, enterprise.dashboardUrl);
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
...init,
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${enterprise.clientKey}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
...(init?.headers || {}),
|
|
30
|
+
},
|
|
31
|
+
signal: AbortSignal.timeout(12_000),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) throw new Error('Enterprise private tool is unavailable.');
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getEnterpriseTools(config: any): Promise<EnterpriseToolDescriptor[]> {
|
|
38
|
+
const enterprise = profile(config);
|
|
39
|
+
if (!enterprise?.managedToolIds.length) return [];
|
|
40
|
+
try {
|
|
41
|
+
const data = (await enterpriseFetch(config, '/api/client/tools')) as {
|
|
42
|
+
tools?: EnterpriseToolDescriptor[];
|
|
43
|
+
} | null;
|
|
44
|
+
return (data?.tools || []).filter((tool) => enterprise.managedToolIds.includes(tool.id));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[enterprise] private tools unavailable:', error);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeEnterpriseTool(config: any, id: string, input: { query?: string }) {
|
|
52
|
+
return enterpriseFetch(config, `/api/client/tools/${encodeURIComponent(id)}/execute`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: JSON.stringify(input),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Do this before model invocation so a paused or exhausted client cannot spend more. */
|
|
59
|
+
export async function authorizeEnterpriseAiRequest(config: any) {
|
|
60
|
+
if (!profile(config)) return;
|
|
61
|
+
await enterpriseFetch(config, '/api/client/authorize', { method: 'POST', body: '{}' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function trackEnterpriseAiUsage(
|
|
65
|
+
config: any,
|
|
66
|
+
event: { modelId: string; inputTokens: number; outputTokens: number; costUsd: number },
|
|
67
|
+
) {
|
|
68
|
+
void enterpriseFetch(config, '/api/client/usage', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
body: JSON.stringify({ service: 'ai', ...event }),
|
|
71
|
+
}).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reports one meter's usage for an installed Marketplace catalog tool. This
|
|
76
|
+
* is the `trackUsage` sink wired into every dynamically-loaded ToolExtension
|
|
77
|
+
* client for an EE-enrolled installation — generic across tools, since the
|
|
78
|
+
* meter id and quantity always come from the tool itself, never hardcoded
|
|
79
|
+
* here. A no-op when the workspace isn't enrolled with Enterprise.
|
|
80
|
+
*/
|
|
81
|
+
export function trackEnterpriseToolUsage(
|
|
82
|
+
config: any,
|
|
83
|
+
toolId: string,
|
|
84
|
+
event: { meter: string; quantity: number; unit: string },
|
|
85
|
+
) {
|
|
86
|
+
void enterpriseFetch(config, '/api/client/usage', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
service: 'tool',
|
|
90
|
+
toolId,
|
|
91
|
+
meterId: event.meter,
|
|
92
|
+
quantity: event.quantity,
|
|
93
|
+
unit: event.unit,
|
|
94
|
+
}),
|
|
95
|
+
}).catch(() => undefined);
|
|
96
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { RagConfig } from '@larkup/core/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supplies a marketplace runtime with the user's selected AI Models settings
|
|
5
|
+
* without making the installed package import the host application's source.
|
|
6
|
+
*/
|
|
7
|
+
export function withGlobalVisionGatewayConfig(
|
|
8
|
+
toolConfig: Record<string, unknown>,
|
|
9
|
+
config: Pick<
|
|
10
|
+
RagConfig,
|
|
11
|
+
| 'visionProvider'
|
|
12
|
+
| 'visionApiKey'
|
|
13
|
+
| 'visionModelId'
|
|
14
|
+
| 'chatProvider'
|
|
15
|
+
| 'chatApiKey'
|
|
16
|
+
| 'chatModelId'
|
|
17
|
+
| 'embeddingProvider'
|
|
18
|
+
| 'embeddingApiKey'
|
|
19
|
+
>,
|
|
20
|
+
): Record<string, unknown> {
|
|
21
|
+
const visionCapableProviders = new Set(['vercel_ai_gateway', 'openai', 'google']);
|
|
22
|
+
const toolConfigWithCurrentDefaults = migrateLegacyVideoIntelligenceDefaults(toolConfig, config);
|
|
23
|
+
const configuredGatewayKey =
|
|
24
|
+
(config.visionProvider === 'vercel_ai_gateway' ? config.visionApiKey : undefined) ||
|
|
25
|
+
(config.chatProvider === 'vercel_ai_gateway' ? config.chatApiKey : undefined) ||
|
|
26
|
+
(config.embeddingProvider === 'vercel_ai_gateway' ? config.embeddingApiKey : undefined);
|
|
27
|
+
// Model credentials must come from the user's saved project configuration.
|
|
28
|
+
// A server/developer environment key is intentionally never inherited.
|
|
29
|
+
const explicitVisionProvider =
|
|
30
|
+
config.visionProvider &&
|
|
31
|
+
config.visionApiKey &&
|
|
32
|
+
visionCapableProviders.has(config.visionProvider)
|
|
33
|
+
? config.visionProvider
|
|
34
|
+
: undefined;
|
|
35
|
+
const inheritedChatProvider =
|
|
36
|
+
config.chatProvider && config.chatApiKey && visionCapableProviders.has(config.chatProvider)
|
|
37
|
+
? config.chatProvider
|
|
38
|
+
: undefined;
|
|
39
|
+
const inheritedEmbeddingProvider =
|
|
40
|
+
config.embeddingProvider &&
|
|
41
|
+
config.embeddingApiKey &&
|
|
42
|
+
visionCapableProviders.has(config.embeddingProvider)
|
|
43
|
+
? config.embeddingProvider
|
|
44
|
+
: undefined;
|
|
45
|
+
const visionProvider =
|
|
46
|
+
explicitVisionProvider ||
|
|
47
|
+
inheritedChatProvider ||
|
|
48
|
+
inheritedEmbeddingProvider ||
|
|
49
|
+
(configuredGatewayKey ? 'vercel_ai_gateway' : undefined);
|
|
50
|
+
const visionApiKey =
|
|
51
|
+
(explicitVisionProvider ? config.visionApiKey : undefined) ||
|
|
52
|
+
(visionProvider === config.chatProvider ? config.chatApiKey : undefined) ||
|
|
53
|
+
(visionProvider === config.embeddingProvider ? config.embeddingApiKey : undefined) ||
|
|
54
|
+
(visionProvider === 'vercel_ai_gateway' ? configuredGatewayKey : undefined);
|
|
55
|
+
const visionModel =
|
|
56
|
+
(explicitVisionProvider ? config.visionModelId : undefined) ||
|
|
57
|
+
(inheritedChatProvider && inheritedChatProvider === visionProvider
|
|
58
|
+
? config.chatModelId
|
|
59
|
+
: undefined) ||
|
|
60
|
+
(visionProvider === 'vercel_ai_gateway' && config.chatProvider === 'vercel_ai_gateway'
|
|
61
|
+
? config.chatModelId
|
|
62
|
+
: undefined);
|
|
63
|
+
|
|
64
|
+
// Older projects commonly have one OpenAI key saved with the embedding
|
|
65
|
+
// configuration before a separate Chat Model was introduced. Reuse it when
|
|
66
|
+
// it belongs to a provider that can run the video tool's planning pass.
|
|
67
|
+
const agentProvider =
|
|
68
|
+
(config.chatProvider && config.chatApiKey ? config.chatProvider : undefined) ||
|
|
69
|
+
(config.embeddingProvider &&
|
|
70
|
+
config.embeddingApiKey &&
|
|
71
|
+
visionCapableProviders.has(config.embeddingProvider)
|
|
72
|
+
? config.embeddingProvider
|
|
73
|
+
: config.chatProvider);
|
|
74
|
+
const agentApiKey =
|
|
75
|
+
(agentProvider === config.chatProvider ? config.chatApiKey : undefined) ||
|
|
76
|
+
(agentProvider === config.embeddingProvider ? config.embeddingApiKey : undefined) ||
|
|
77
|
+
(agentProvider === 'vercel_ai_gateway' ? configuredGatewayKey : undefined);
|
|
78
|
+
return {
|
|
79
|
+
...toolConfigWithCurrentDefaults,
|
|
80
|
+
larkupVisionProvider: visionProvider,
|
|
81
|
+
larkupVisionApiKey: visionApiKey,
|
|
82
|
+
larkupVisionModel: visionModel,
|
|
83
|
+
larkupAgentProvider: agentProvider,
|
|
84
|
+
larkupAgentApiKey: agentApiKey,
|
|
85
|
+
larkupAgentModel:
|
|
86
|
+
config.chatModelId ||
|
|
87
|
+
(agentProvider === 'google'
|
|
88
|
+
? 'google/gemini-3.5-flash-lite'
|
|
89
|
+
: agentProvider === 'openai'
|
|
90
|
+
? 'openai/gpt-5-mini'
|
|
91
|
+
: undefined),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Versions through 0.2.4 persisted provider-specific values that were meant
|
|
97
|
+
* to be defaults. Those values shadow AI Models forever, so an existing
|
|
98
|
+
* OpenAI setup can look unconfigured after installing Video Intelligence.
|
|
99
|
+
* Treat only the exact old, keyless defaults as automatic. Real overrides and
|
|
100
|
+
* Gateway-based AI Models selections remain untouched.
|
|
101
|
+
*/
|
|
102
|
+
function migrateLegacyVideoIntelligenceDefaults(
|
|
103
|
+
toolConfig: Record<string, unknown>,
|
|
104
|
+
config: Pick<
|
|
105
|
+
RagConfig,
|
|
106
|
+
| 'visionProvider'
|
|
107
|
+
| 'visionApiKey'
|
|
108
|
+
| 'chatProvider'
|
|
109
|
+
| 'chatApiKey'
|
|
110
|
+
| 'embeddingProvider'
|
|
111
|
+
| 'embeddingApiKey'
|
|
112
|
+
>,
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
if (!('videoVisionProvider' in toolConfig) && !('videoAgentProvider' in toolConfig)) {
|
|
115
|
+
return toolConfig;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const value = (key: string) =>
|
|
119
|
+
typeof toolConfig[key] === 'string' ? toolConfig[key].trim() : undefined;
|
|
120
|
+
const hasSavedVisionDefault = Boolean(
|
|
121
|
+
(config.visionProvider && config.visionApiKey) ||
|
|
122
|
+
(config.chatProvider && config.chatApiKey) ||
|
|
123
|
+
(config.embeddingProvider && config.embeddingApiKey),
|
|
124
|
+
);
|
|
125
|
+
const hasSavedAgentDefault = Boolean(
|
|
126
|
+
(config.chatProvider && config.chatApiKey) ||
|
|
127
|
+
(config.embeddingProvider && config.embeddingApiKey),
|
|
128
|
+
);
|
|
129
|
+
const usesSavedGateway =
|
|
130
|
+
config.visionProvider === 'vercel_ai_gateway' || config.chatProvider === 'vercel_ai_gateway';
|
|
131
|
+
const migrated = { ...toolConfig };
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
hasSavedVisionDefault &&
|
|
135
|
+
!usesSavedGateway &&
|
|
136
|
+
value('videoVisionProvider') === 'vercel_ai_gateway' &&
|
|
137
|
+
!value('videoVisionApiKey') &&
|
|
138
|
+
value('semanticVisionModel') === 'google/gemini-3.6-flash'
|
|
139
|
+
) {
|
|
140
|
+
migrated.videoVisionProvider = 'auto';
|
|
141
|
+
migrated.semanticVisionModel = 'auto';
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
hasSavedAgentDefault &&
|
|
145
|
+
config.chatProvider !== 'vercel_ai_gateway' &&
|
|
146
|
+
config.embeddingProvider !== 'vercel_ai_gateway' &&
|
|
147
|
+
value('videoAgentProvider') === 'vercel_ai_gateway' &&
|
|
148
|
+
!value('videoAgentApiKey') &&
|
|
149
|
+
value('agentModel') === 'openai/gpt-5-mini'
|
|
150
|
+
) {
|
|
151
|
+
migrated.videoAgentProvider = 'auto';
|
|
152
|
+
migrated.agentModel = 'auto';
|
|
153
|
+
}
|
|
154
|
+
return migrated;
|
|
155
|
+
}
|