@ultimat3/ai 24.0.0 → 25.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +72 -34
- package/README.md +271 -69
- package/package.json +12 -12
- package/src/agent-facts.ts +9 -2
- package/src/agent-job.ts +41 -2
- package/src/agent.ts +30 -13
- package/src/budget.ts +41 -6
- package/src/content-blocks.ts +227 -0
- package/src/content-errors.ts +67 -0
- package/src/echo-provider.ts +102 -0
- package/src/embeddings.ts +30 -10
- package/src/errors.ts +17 -11
- package/src/eval-errors.ts +3 -1
- package/src/evals.ts +1 -1
- package/src/failure-disclosure.ts +90 -0
- package/src/gateway.ts +40 -16
- package/src/hive-pool.ts +24 -16
- package/src/hive-result.ts +4 -1
- package/src/hive.ts +6 -4
- package/src/index.ts +63 -24
- package/src/llm-cache.ts +1 -1
- package/src/llm.ts +15 -11
- package/src/model-resolve.ts +79 -0
- package/src/models.ts +125 -111
- package/src/openai-body.ts +13 -2
- package/src/openai-messages.ts +86 -15
- package/src/openai-provider.ts +28 -31
- package/src/openai-wire.ts +4 -1
- package/src/pg-vector-sql.ts +11 -7
- package/src/pg-vector.ts +14 -6
- package/src/prompt-fence.ts +128 -0
- package/src/prompt.ts +43 -13
- package/src/provider.ts +75 -112
- package/src/rag.ts +7 -6
- package/src/remote-embedder.ts +10 -5
- package/src/runtime.ts +7 -14
- package/src/scorers.ts +1 -1
- package/src/tool-idempotency.ts +79 -0
- package/src/tools.ts +38 -36
- package/src/vector-scope.ts +2 -2
- package/src/vector.ts +103 -37
- package/src/wire.ts +7 -2
- package/src/fix-line.evals.ts +0 -35
- package/src/fix-line.ts +0 -27
- package/src/openai-models.ts +0 -84
package/src/index.ts
CHANGED
|
@@ -12,29 +12,49 @@ export type { AgentJobOptions } from './agent-job';
|
|
|
12
12
|
export { agentJob } from './agent-job';
|
|
13
13
|
export type {
|
|
14
14
|
BudgetKeys,
|
|
15
|
+
BudgetLedger,
|
|
15
16
|
BudgetLedgerInput,
|
|
16
17
|
BudgetLimits,
|
|
17
18
|
BudgetReport,
|
|
18
19
|
BudgetStore,
|
|
19
20
|
BudgetTake,
|
|
21
|
+
MemoryBudgetStore,
|
|
20
22
|
SpendEstimate,
|
|
21
23
|
} from './budget';
|
|
22
24
|
export {
|
|
23
|
-
BudgetLedger,
|
|
24
25
|
budgetKeysFor,
|
|
25
26
|
currentBudget,
|
|
26
27
|
estimateSpend,
|
|
27
|
-
|
|
28
|
+
memoryBudgetStore,
|
|
28
29
|
withBudget,
|
|
29
30
|
} from './budget';
|
|
30
|
-
export type {
|
|
31
|
+
export type {
|
|
32
|
+
AiDocumentBlock,
|
|
33
|
+
AiDocumentSource,
|
|
34
|
+
AiImageBlock,
|
|
35
|
+
AiImageSource,
|
|
36
|
+
AiMediaBlock,
|
|
37
|
+
ImageMediaType,
|
|
38
|
+
} from './content-blocks';
|
|
39
|
+
export {
|
|
40
|
+
IMAGE_MEDIA_TYPES,
|
|
41
|
+
IMAGE_TOKEN_ESTIMATE,
|
|
42
|
+
isMediaBlock,
|
|
43
|
+
MAX_DOCUMENT_BASE64_CHARS,
|
|
44
|
+
MAX_IMAGE_BASE64_CHARS,
|
|
45
|
+
} from './content-blocks';
|
|
46
|
+
export type { ContentRefusal } from './content-errors';
|
|
47
|
+
export { AiContentUnsupportedError } from './content-errors';
|
|
48
|
+
export type { EchoProvider, EchoProviderInput } from './echo-provider';
|
|
49
|
+
export { echoProvider } from './echo-provider';
|
|
50
|
+
export type { Embedder, HashEmbedder, HashEmbedderInput } from './embeddings';
|
|
31
51
|
export {
|
|
32
52
|
cosine,
|
|
33
53
|
embedBatched,
|
|
34
54
|
embedOne,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
hashEmbedder,
|
|
56
|
+
normalizeVector,
|
|
57
|
+
wordTokens,
|
|
38
58
|
} from './embeddings';
|
|
39
59
|
export type { AiErrorCode } from './errors';
|
|
40
60
|
export {
|
|
@@ -94,8 +114,14 @@ export {
|
|
|
94
114
|
resetEvals,
|
|
95
115
|
} from './evals';
|
|
96
116
|
export type { AiFetch } from './fetch-seam';
|
|
97
|
-
export type {
|
|
98
|
-
export {
|
|
117
|
+
export type { Gateway, GatewayCache, ProviderGatewayInput, RetryPolicy } from './gateway';
|
|
118
|
+
export {
|
|
119
|
+
backoffMs,
|
|
120
|
+
DEFAULT_GATEWAY_RETRY,
|
|
121
|
+
isRetryable,
|
|
122
|
+
promptCacheKey,
|
|
123
|
+
providerGateway,
|
|
124
|
+
} from './gateway';
|
|
99
125
|
export type { HiveDef, HiveSplitArgs } from './hive';
|
|
100
126
|
export { hive } from './hive';
|
|
101
127
|
export { HiveEmptyError } from './hive-errors';
|
|
@@ -104,11 +130,19 @@ export type { LlmAction, LlmBudget, LlmDef, LlmVarsArgs } from './llm';
|
|
|
104
130
|
export { llm } from './llm';
|
|
105
131
|
export type { LlmCache, LlmScopeArgs, LlmSemanticCache } from './llm-cache';
|
|
106
132
|
export type { LlmStreamChunk } from './llm-stream';
|
|
107
|
-
export type {
|
|
133
|
+
export type { ModelSource } from './model-resolve';
|
|
134
|
+
export { AiModelUnresolvedError } from './model-resolve';
|
|
135
|
+
export type {
|
|
136
|
+
ContentKind,
|
|
137
|
+
DeclaredReasoning,
|
|
138
|
+
Effort,
|
|
139
|
+
ModelId,
|
|
140
|
+
ModelReasoning,
|
|
141
|
+
ModelSpec,
|
|
142
|
+
ThinkingMode,
|
|
143
|
+
} from './models';
|
|
108
144
|
export {
|
|
109
|
-
ANTHROPIC_MODEL_IDS,
|
|
110
145
|
assertModel,
|
|
111
|
-
DEFAULT_MODEL,
|
|
112
146
|
EFFORTS,
|
|
113
147
|
isModelRegistered,
|
|
114
148
|
modelIds,
|
|
@@ -118,11 +152,10 @@ export {
|
|
|
118
152
|
registerModel,
|
|
119
153
|
resetModels,
|
|
120
154
|
} from './models';
|
|
121
|
-
export { OPENAI_MODEL_IDS, registerOpenAiModels } from './openai-models';
|
|
122
155
|
export type { OpenAiProviderInput } from './openai-provider';
|
|
123
156
|
export { openAiProvider } from './openai-provider';
|
|
124
|
-
export type {
|
|
125
|
-
export {
|
|
157
|
+
export type { PostgresVectorStore, PostgresVectorStoreInput } from './pg-vector';
|
|
158
|
+
export { postgresVectorStore } from './pg-vector';
|
|
126
159
|
export type {
|
|
127
160
|
PgHybridArgs,
|
|
128
161
|
PgSearchArgs,
|
|
@@ -137,18 +170,19 @@ export {
|
|
|
137
170
|
} from './pg-vector-sql';
|
|
138
171
|
export type { DefinePromptInput, Prompt, PromptVars } from './prompt';
|
|
139
172
|
export {
|
|
140
|
-
contentHash,
|
|
141
173
|
definePrompt,
|
|
142
174
|
describePrompts,
|
|
143
175
|
getPrompt,
|
|
176
|
+
promptHash,
|
|
144
177
|
promptVersions,
|
|
145
178
|
resetPrompts,
|
|
146
179
|
} from './prompt';
|
|
180
|
+
export { promptFences } from './prompt-fence';
|
|
147
181
|
export type {
|
|
148
182
|
AiContentBlock,
|
|
149
183
|
AiMessage,
|
|
184
|
+
AnthropicProvider,
|
|
150
185
|
AnthropicProviderInput,
|
|
151
|
-
EchoProviderInput,
|
|
152
186
|
GenerateRequest,
|
|
153
187
|
GenerateResult,
|
|
154
188
|
Provider,
|
|
@@ -158,9 +192,8 @@ export type {
|
|
|
158
192
|
TokenUsage,
|
|
159
193
|
} from './provider';
|
|
160
194
|
export {
|
|
161
|
-
|
|
195
|
+
anthropicProvider,
|
|
162
196
|
costOf,
|
|
163
|
-
EchoProvider,
|
|
164
197
|
estimateCost,
|
|
165
198
|
estimateInputTokens,
|
|
166
199
|
estimateTextTokens,
|
|
@@ -179,17 +212,22 @@ export type {
|
|
|
179
212
|
Reranker,
|
|
180
213
|
RetrieveInput,
|
|
181
214
|
} from './rag';
|
|
182
|
-
export {
|
|
215
|
+
export {
|
|
216
|
+
assembleContext,
|
|
217
|
+
chunkDocument,
|
|
218
|
+
indexDocument,
|
|
219
|
+
passthroughReranker,
|
|
220
|
+
retrieve,
|
|
221
|
+
} from './rag';
|
|
183
222
|
export { assertNoSecrets } from './redaction';
|
|
184
|
-
export type { RemoteEmbedderInput } from './remote-embedder';
|
|
185
|
-
export {
|
|
223
|
+
export type { RemoteEmbedder, RemoteEmbedderInput } from './remote-embedder';
|
|
224
|
+
export { remoteEmbedder } from './remote-embedder';
|
|
186
225
|
export type { AiRuntimeInput, Redactor } from './runtime';
|
|
187
226
|
export {
|
|
188
227
|
aiEmbedder,
|
|
189
228
|
aiGateway,
|
|
190
229
|
aiRedactor,
|
|
191
230
|
configureAi,
|
|
192
|
-
MAX_SEMANTIC_CACHE_SCOPES,
|
|
193
231
|
semanticCacheFor,
|
|
194
232
|
} from './runtime';
|
|
195
233
|
export type { Scorer } from './scorers';
|
|
@@ -212,6 +250,7 @@ export type {
|
|
|
212
250
|
export { asProjectableAction, runLlmToolCall, toLlmTool, toLlmTools } from './tools';
|
|
213
251
|
export type {
|
|
214
252
|
HybridSearchInput,
|
|
253
|
+
MemoryVectorStore,
|
|
215
254
|
MemoryVectorStoreInput,
|
|
216
255
|
MetadataFilter,
|
|
217
256
|
SearchHit,
|
|
@@ -219,7 +258,7 @@ export type {
|
|
|
219
258
|
VectorRecord,
|
|
220
259
|
VectorStore,
|
|
221
260
|
} from './vector';
|
|
222
|
-
export { fuse,
|
|
261
|
+
export { fuse, memoryVectorStore } from './vector';
|
|
223
262
|
export type { VectorScope } from './vector-scope';
|
|
224
|
-
export {
|
|
263
|
+
export { NO_VECTOR_TENANT, tenantOf, UNSCOPED, VectorUnscopedError } from './vector-scope';
|
|
225
264
|
export type { StreamState } from './wire';
|
package/src/llm-cache.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface PromptCache {
|
|
|
74
74
|
* writing its own `scope`, and the locale has to survive that — so it lives in the unconditional
|
|
75
75
|
* half of the store key, beside the prompt hash.
|
|
76
76
|
*
|
|
77
|
-
* `ctx.actor` is never absent (`
|
|
77
|
+
* `ctx.actor` is never absent (`ctxOf` defaults it to `anonymousActor()`), so every
|
|
78
78
|
* anonymous caller shares one partition — which is what the anonymous actor already means
|
|
79
79
|
* everywhere else in the framework.
|
|
80
80
|
*/
|
package/src/llm.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The eight primitives are the whole vocabulary. A model call is a server-authoritative
|
|
5
5
|
* operation with an input schema, an output schema and a policy, which is the definition of
|
|
6
6
|
* an `action` — so this file is a FACTORY over `action()`, never a new kind of thing. That is
|
|
7
|
-
* why `summarize.
|
|
7
|
+
* why `summarize.openapi()`, `.client()`, `.job()`, `.contract()` and its MCP tool all exist
|
|
8
8
|
* without a line here: the value is an action, so it projects like one, and an app gains an
|
|
9
9
|
* MCP tool backed by a model the moment it declares one.
|
|
10
10
|
*
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* `.stream()` is the same action over a different transport, and it is here rather than beside
|
|
16
16
|
* the gateway for one reason: everything that makes `llm()` worth using — policy, input parse,
|
|
17
|
-
* budget scope, semantic cache, span,
|
|
17
|
+
* budget scope, semantic cache, span, MCP tool — is lost the moment a feature has to reach past
|
|
18
18
|
* it to `aiGateway()` for tokens on a screen. `./llm-stream.ts` holds the plumbing and states the
|
|
19
19
|
* two decisions a stream forces (no repair turn, budget reserved exactly as before); what a
|
|
20
20
|
* streamed answer must satisfy is decided in this file, next to the non-streaming version of the
|
|
@@ -29,7 +29,7 @@ import type { Money } from '@ultimat3/money';
|
|
|
29
29
|
import type { InferInput, InferOutput, StandardSchemaV1 } from '@ultimat3/schema';
|
|
30
30
|
import { formatIssues, validateAsync } from '@ultimat3/schema';
|
|
31
31
|
import type { BudgetLimits } from './budget';
|
|
32
|
-
import { budgetKeysFor, currentBudget, withBudget } from './budget';
|
|
32
|
+
import { budgetKeysFor, currentBudget, screenedMoney, withBudget } from './budget';
|
|
33
33
|
import {
|
|
34
34
|
LlmOutputInvalidError,
|
|
35
35
|
LlmRefusedError,
|
|
@@ -41,8 +41,9 @@ import type { LlmCache } from './llm-cache';
|
|
|
41
41
|
import { openCache } from './llm-cache';
|
|
42
42
|
import type { LlmSink, LlmStreamChunk } from './llm-stream';
|
|
43
43
|
import { currentLlmSink, llmStream, streamOneTurn, withLlmSink } from './llm-stream';
|
|
44
|
+
import { modelForCall } from './model-resolve';
|
|
44
45
|
import type { ModelId } from './models';
|
|
45
|
-
import {
|
|
46
|
+
import { moreCapableThan } from './models';
|
|
46
47
|
import type { Prompt, PromptVars } from './prompt';
|
|
47
48
|
import type { AiMessage, GenerateRequest, GenerateResult } from './provider';
|
|
48
49
|
import { isTruncated } from './provider';
|
|
@@ -101,7 +102,7 @@ export interface LlmDef<
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
|
-
* An `action` with one extra way to be called. Every projection an action has, it has —
|
|
105
|
+
* An `action` with one extra way to be called. Every projection an action has, it has — its MCP tool,
|
|
105
106
|
* `.openapi()`, `.client()`, `.job()`, `.contract()` — plus a transport for the case an action's
|
|
106
107
|
* single return value cannot serve: text on a screen before the answer is finished.
|
|
107
108
|
*/
|
|
@@ -191,7 +192,10 @@ async function generate<
|
|
|
191
192
|
// The prompt ref is the identity every failure here is about, and unlike the action's
|
|
192
193
|
// export name it exists before registration and can never twin.
|
|
193
194
|
const name = prompt.ref;
|
|
194
|
-
|
|
195
|
+
// Screened first: a bad declaration is refused whether or not a gateway exists.
|
|
196
|
+
const limits = limitsOf(def.budget);
|
|
197
|
+
// The declaration, its prompt, then the gateway's `defaultModel` — the app's three places.
|
|
198
|
+
const model = modelForCall('llm', name, def.model, prompt.model);
|
|
195
199
|
// `vars()` is the one declared place a model call loads data, so it is the one place the
|
|
196
200
|
// framework can refuse a `Secret` and the one place an app's redactor can see the row before it
|
|
197
201
|
// leaves the process. Both run here, between the load and the request, and neither is optional
|
|
@@ -249,8 +253,6 @@ async function generate<
|
|
|
249
253
|
// A ledger derived from the ambient one, so a per-call budget can only TIGHTEN the actor
|
|
250
254
|
// and org ceilings this call runs inside, never widen them. The gateway reserves against
|
|
251
255
|
// it before the provider is touched — that is where `X_AI_BUDGET_EXCEEDED` comes from.
|
|
252
|
-
// Screened first, as before: a bad declaration is refused whether or not a gateway exists.
|
|
253
|
-
const limits = limitsOf(def.budget);
|
|
254
256
|
// Rooted in the GATEWAY's own ceilings when no scope is open — an empty root ignored them.
|
|
255
257
|
const gateway = aiGateway(name);
|
|
256
258
|
// Keyed on the CALLER, so the gateway's `actor` / `org` ceilings count this call against them.
|
|
@@ -280,7 +282,7 @@ async function generate<
|
|
|
280
282
|
// The fix names a model the caller can paste, and only ever a MORE capable one:
|
|
281
283
|
// "the first id that differs" answered a refusal on the default model with the next
|
|
282
284
|
// entry down the ladder, which is a retry that cannot succeed.
|
|
283
|
-
alternative: moreCapableThan(result.model),
|
|
285
|
+
alternative: moreCapableThan(result.model, request),
|
|
284
286
|
category: result.stopDetails?.category,
|
|
285
287
|
explanation: result.stopDetails?.explanation,
|
|
286
288
|
});
|
|
@@ -346,7 +348,7 @@ async function streamedAnswer<TOutput extends StandardSchemaV1>(
|
|
|
346
348
|
throw new LlmRefusedError({
|
|
347
349
|
prompt: name,
|
|
348
350
|
model: result.model,
|
|
349
|
-
alternative: moreCapableThan(result.model),
|
|
351
|
+
alternative: moreCapableThan(result.model, request),
|
|
350
352
|
category: result.stopDetails?.category,
|
|
351
353
|
explanation: result.stopDetails?.explanation,
|
|
352
354
|
});
|
|
@@ -392,7 +394,9 @@ function limitsOf(budget: LlmBudget | undefined): BudgetLimits {
|
|
|
392
394
|
...(budget?.tokensIn === undefined
|
|
393
395
|
? {}
|
|
394
396
|
: { tokensIn: finiteCount('llm', 'budget.tokensIn', budget.tokensIn) }),
|
|
395
|
-
...(budget?.costPerCall === undefined
|
|
397
|
+
...(budget?.costPerCall === undefined
|
|
398
|
+
? {}
|
|
399
|
+
: { costPerCall: screenedMoney('llm', 'budget.costPerCall', budget.costPerCall) }),
|
|
396
400
|
};
|
|
397
401
|
}
|
|
398
402
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Which model a call runs on, decided in ONE place. The app names it in one of three places — the
|
|
2
|
+
// declaration's `model`, its prompt's, `providerGateway({ defaultModel })` — and the framework has
|
|
3
|
+
// no fourth: a call that names none is refused here, naming all three, never routed to a vendor.
|
|
4
|
+
|
|
5
|
+
import { UltimateError } from '@ultimat3/core';
|
|
6
|
+
import type { ModelId } from './models';
|
|
7
|
+
import { aiGateway } from './runtime';
|
|
8
|
+
|
|
9
|
+
/** Where a model was resolved. Bounded, and the first word of the refusal's cause. */
|
|
10
|
+
export type ModelSite = 'llm' | 'agent' | 'gateway' | 'provider' | 'echo-provider';
|
|
11
|
+
|
|
12
|
+
/** The three places, spelled once so the refusal and the docs cannot drift apart. */
|
|
13
|
+
const PLACES =
|
|
14
|
+
'model: modelId on the llm()/agent() declaration, model: modelId in its definePrompt, or providerGateway({ …, defaultModel: modelId })';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The first declared candidate, in the caller's precedence order (a declaration, then its prompt,
|
|
18
|
+
* then the gateway's `defaultModel`). None declared is `X_AI_MODEL_UNRESOLVED`: which model, and so
|
|
19
|
+
* which vendor, is the app's call and never the framework's.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveModel(
|
|
22
|
+
site: ModelSite,
|
|
23
|
+
...candidates: readonly (ModelId | undefined)[]
|
|
24
|
+
): ModelId {
|
|
25
|
+
for (const candidate of candidates) if (candidate !== undefined) return candidate;
|
|
26
|
+
throw new AiModelUnresolvedError(site);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A call that named no model: its own code, because the fix is a declaration, not a request. */
|
|
30
|
+
export class AiModelUnresolvedError extends UltimateError {
|
|
31
|
+
constructor(site: ModelSite) {
|
|
32
|
+
super({
|
|
33
|
+
code: 'X_AI_MODEL_UNRESOLVED',
|
|
34
|
+
cause: `${site} resolved no model: no declaration, prompt or providerGateway({ defaultModel }) named one, and the framework chooses none (modelId: an id your app registerModel-ed and a configured provider lists)`,
|
|
35
|
+
fix: `${PLACES} # one of the three`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The model an `llm()` or `agent()` call runs on. The gateway is asked only when neither the
|
|
42
|
+
* declaration nor its prompt names one — and with none installed that is `X_AI_GATEWAY_MISSING`
|
|
43
|
+
* for `prompt`, because the third place does not exist yet: the boot call is the whole fix.
|
|
44
|
+
*/
|
|
45
|
+
export function modelForCall(
|
|
46
|
+
site: 'llm' | 'agent',
|
|
47
|
+
prompt: string,
|
|
48
|
+
declared: ModelId | undefined,
|
|
49
|
+
promptModel: ModelId | undefined,
|
|
50
|
+
): ModelId {
|
|
51
|
+
if (declared !== undefined || promptModel !== undefined) {
|
|
52
|
+
return resolveModel(site, declared, promptModel);
|
|
53
|
+
}
|
|
54
|
+
return resolveModel(site, aiGateway(prompt).defaultModel);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Which of the app's three places a model came from. */
|
|
58
|
+
export type ModelSource = 'declaration' | 'prompt' | 'gateway';
|
|
59
|
+
|
|
60
|
+
const SOURCES: readonly ModelSource[] = ['declaration', 'prompt', 'gateway'];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The same answer as `resolveModel`, over the same three candidates in the same order, WITHOUT
|
|
64
|
+
* refusing — and with where it came from. For a fact published about a declaration
|
|
65
|
+
* (`describeAgents()`): a reader describing an app is not a call, so a declaration that names no
|
|
66
|
+
* model yet (the gateway is installed later) is described as `undefined`, never thrown over.
|
|
67
|
+
*/
|
|
68
|
+
export function describedModel(
|
|
69
|
+
declared: ModelId | undefined,
|
|
70
|
+
prompt: ModelId | undefined,
|
|
71
|
+
gateway: ModelId | undefined,
|
|
72
|
+
): { readonly model: ModelId | undefined; readonly from: ModelSource | undefined } {
|
|
73
|
+
const candidates = [declared, prompt, gateway];
|
|
74
|
+
for (const [at, candidate] of candidates.entries()) {
|
|
75
|
+
const from = SOURCES[at];
|
|
76
|
+
if (candidate !== undefined && from !== undefined) return { model: candidate, from };
|
|
77
|
+
}
|
|
78
|
+
return { model: undefined, from: undefined };
|
|
79
|
+
}
|