tardie 0.26.0-rc.0 → 0.26.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/package.json +9 -8
- package/src/agent/component/compaction/model.ts +32 -0
- package/src/agent/component/compaction.ts +10 -19
- package/src/agent/inference/access.ts +1 -184
- package/src/agent/inference/contract.ts +2 -6
- package/src/agent/inference/error.ts +1 -36
- package/src/agent/inference/machine.ts +2 -2
- package/src/agent/inference/model/continuation.ts +14 -0
- package/src/agent/{binding → inference/model}/index.ts +11 -20
- package/src/agent/{binding → inference/model}/output.ts +4 -9
- package/src/agent/{binding → inference/model}/prompt.ts +3 -5
- package/src/agent/{binding → inference/model}/response.ts +4 -4
- package/src/agent/inference/model/settings.ts +11 -0
- package/src/agent/inference/observer.ts +1 -39
- package/src/agent/inference/reference.ts +1 -18
- package/src/agent/inference/request.ts +2 -2
- package/src/agent/inference/retry.ts +2 -17
- package/src/agent/inference/usage.ts +2 -8
- package/src/agent/output/contract.ts +2 -2
- package/src/agent/projection/cost.ts +56 -0
- package/src/agent/runtime/composition.ts +1 -1
- package/src/agent/testing/durable-inference.ts +33 -0
- package/src/agent/testing/inference.ts +3 -3
- package/src/cli/commands.ts +1 -0
- package/src/cli/init.ts +8 -9
- package/src/client/contract.ts +3 -136
- package/src/cloudflare/assembly.ts +3 -4
- package/src/cloudflare/worker.ts +3 -3
- package/src/model/access.ts +184 -0
- package/src/model/catalog/availability.ts +1 -1
- package/src/model/catalog/index.ts +1 -1
- package/src/model/catalog/metadata.ts +1 -1
- package/src/model/catalog/page.ts +2 -2
- package/src/model/catalog/repository.ts +2 -2
- package/src/model/catalog/schema.ts +138 -0
- package/src/model/config.ts +9 -8
- package/src/model/error.ts +36 -0
- package/src/model/host.ts +21 -18
- package/src/model/index.ts +2 -2
- package/src/model/output.ts +7 -0
- package/src/model/pricing.ts +10 -0
- package/src/model/providers/anthropic.ts +2 -1
- package/src/model/providers/bedrock-transport.ts +1 -1
- package/src/model/providers/bedrock.ts +2 -1
- package/src/model/providers/directory.ts +4 -0
- package/src/model/providers/layer.ts +9 -5
- package/src/model/providers/openai-compat.ts +2 -1
- package/src/model/providers/openai.ts +2 -1
- package/src/model/providers/openrouter.ts +12 -0
- package/src/model/providers/options.ts +24 -21
- package/src/model/providers/usage.ts +5 -0
- package/src/model/reference.ts +26 -0
- package/src/model/selection.ts +5 -4
- package/src/model/{binding/index.ts → services.ts} +12 -9
- package/src/{agent/binding → model}/settings.ts +6 -14
- package/src/{agent/binding/observer.ts → model/stream/delivery.ts} +22 -1
- package/src/model/stream/observer.ts +39 -0
- package/src/model/stream/policy.ts +33 -0
- package/src/{agent/binding → model/stream}/request.ts +2 -2
- package/src/server/model-services.ts +3 -4
- package/src/tardie/projection/cost.ts +1 -0
- package/ui/assets/{index-CRr2FBVb.js → index-DHI0N7wi.js} +1 -1
- package/ui/index.html +1 -1
- package/src/agent/binding/continuation.ts +0 -18
- package/src/agent/binding/policy.ts +0 -13
- package/src/model/binding/continuation.ts +0 -1
- package/src/model/binding/output.ts +0 -1
- package/src/model/binding/prompt.ts +0 -1
- package/src/model/binding/response.ts +0 -1
- package/src/model/inference/observer.ts +0 -1
- package/src/model/inference/policy.ts +0 -1
- package/src/model/inference/request.ts +0 -1
- package/src/model/providers/response.ts +0 -1
- /package/src/{agent/binding → model/stream}/collect.ts +0 -0
- /package/src/model/{binding/usage.ts → usage.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardie",
|
|
3
|
-
"version": "0.26.0
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Clavia, Inc.",
|
|
6
6
|
"description": "Actor, agent, client, and host APIs for Tardigrade.",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"tardigrade": {
|
|
19
|
-
"sourceTree": "
|
|
19
|
+
"sourceTree": "055f74c19df551861b617c0df419d9496438966b"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
"./model/metadata": "./src/model/catalog/metadata.ts",
|
|
98
98
|
"./model/directory": "./src/model/providers/directory.ts",
|
|
99
99
|
"./model/reasoning": "./src/model/providers/options.ts",
|
|
100
|
-
"./model/request-policy": "./src/model/
|
|
101
|
-
"./model/output": "./src/model/
|
|
100
|
+
"./model/request-policy": "./src/model/stream/policy.ts",
|
|
101
|
+
"./model/output": "./src/model/output.ts",
|
|
102
102
|
"./model/*": "./src/model/*.ts"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
@@ -108,16 +108,17 @@
|
|
|
108
108
|
"@effect/sql-sqlite-bun": "4.0.0-rc.115",
|
|
109
109
|
"@effect/sql-sqlite-do": "4.0.0-rc.115",
|
|
110
110
|
"@tardie/ai": "0.0.2",
|
|
111
|
-
"@tardie/ai-anthropic": "4.0.0-rc.113-clavia.
|
|
112
|
-
"@tardie/ai-openai": "4.0.0-rc.113-clavia.
|
|
113
|
-
"@tardie/ai-openai-compat": "4.0.0-rc.113-clavia.
|
|
111
|
+
"@tardie/ai-anthropic": "4.0.0-rc.113-clavia.3",
|
|
112
|
+
"@tardie/ai-openai": "4.0.0-rc.113-clavia.4",
|
|
113
|
+
"@tardie/ai-openai-compat": "4.0.0-rc.113-clavia.4",
|
|
114
|
+
"@tardie/ai-openrouter": "4.0.0-rc.113-clavia.1",
|
|
114
115
|
"effect": "4.0.0-rc.115",
|
|
115
116
|
"jsonc-parser": "3.3.1"
|
|
116
117
|
},
|
|
117
118
|
"peerDependencies": {
|
|
118
119
|
"@smithy/fetch-http-handler": "^5.6.3",
|
|
119
120
|
"@smithy/node-http-handler": "^4.11.2",
|
|
120
|
-
"@tardie/ai-bedrock": "0.0.
|
|
121
|
+
"@tardie/ai-bedrock": "0.0.3"
|
|
121
122
|
},
|
|
122
123
|
"peerDependenciesMeta": {
|
|
123
124
|
"@smithy/fetch-http-handler": {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Effect, Option } from "effect"
|
|
2
|
+
import { Toolkit, type Response } from "effect/unstable/ai"
|
|
3
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
4
|
+
import { collectResponse } from "tardie/model/stream/collect"
|
|
5
|
+
import { observeResponse } from "tardie/model/stream/delivery"
|
|
6
|
+
import { BindingSettings, CurrentModel, ProviderRequestKey } from "tardie/model/settings"
|
|
7
|
+
import type { InferenceIdentity } from "tardie/model/stream/observer"
|
|
8
|
+
import type { ModelRef } from "tardie/model/reference"
|
|
9
|
+
import { unknownModelError } from "tardie/model/error"
|
|
10
|
+
|
|
11
|
+
// summarize accepts a nonempty completed summary without tool calls (component/compaction.test.ts).
|
|
12
|
+
export const summarize = (prompt: string, identity: InferenceIdentity, model?: ModelRef) => Effect.gen(function* () {
|
|
13
|
+
const settings = yield* BindingSettings
|
|
14
|
+
const observer = yield* observeResponse(identity, model ?? { provider: settings.provider, model_id: settings.model }, identity.turn, settings.observer)
|
|
15
|
+
const transport = Option.getOrElse(yield* Effect.serviceOption(FetchHttpClient.RequestInit), () => ({}))
|
|
16
|
+
const fetchOptions = { ...transport, timeout: false }
|
|
17
|
+
const response = yield* collectResponse(prompt, Toolkit.make(), observer.onPart, undefined, settings.policy.timeout).pipe(
|
|
18
|
+
Effect.provideService(CurrentModel, model),
|
|
19
|
+
Effect.provideService(ProviderRequestKey, identity.turn),
|
|
20
|
+
Effect.provideService(FetchHttpClient.RequestInit, fetchOptions),
|
|
21
|
+
Effect.ensuring(observer.finish)
|
|
22
|
+
)
|
|
23
|
+
const parts: ReadonlyArray<Response.AnyPart> = response.parts
|
|
24
|
+
const error = parts.find(part => part.type === "error")
|
|
25
|
+
if (error?.type === "error") return yield* unknownModelError(error.error)
|
|
26
|
+
const finish = parts.findLast(part => part.type === "finish")
|
|
27
|
+
const text = parts.flatMap(part => part.type === "text-delta" ? [part.delta] : []).join("")
|
|
28
|
+
if ((finish?.reason !== "stop" && finish?.reason !== "tool-calls") || parts.some(part => part.type === "tool-call") || text.trim() === "") {
|
|
29
|
+
return yield* unknownModelError("Compaction requires a nonempty completed summary without tool calls")
|
|
30
|
+
}
|
|
31
|
+
return text
|
|
32
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { contextPolicyOf, resolvedContextPolicyOf, checkpointOf, keepFromIndex, type ContextPolicy, type CompactionPolicy } from "./context"
|
|
2
2
|
export { contextPolicyOf, resolvedContextPolicyOf, checkpointOf, keepFromIndex, suffixOf, DEFAULT_COMPACTION_POLICY, type ContextPolicy, type ContextWindowTokens, type CompactionPolicy } from "./context"
|
|
3
|
-
import { replayOf } from "../
|
|
3
|
+
import { replayOf } from "../inference/model/continuation"
|
|
4
4
|
import { renderMessageEntries } from "../projection/messages"
|
|
5
5
|
import { upcastError } from "../log/upcast"
|
|
6
6
|
import { hasUnansweredToolCall, responsesOf } from "../log/response"
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
type TranscriptProjectionState
|
|
26
26
|
} from "../projection/transcript"
|
|
27
27
|
import { LanguageModel } from "effect/unstable/ai"
|
|
28
|
-
import {
|
|
29
|
-
import { BindingSettings, ModelSelection } from "
|
|
28
|
+
import { summarize } from "./compaction/model"
|
|
29
|
+
import { BindingSettings, ModelSelection } from "tardie/model/settings"
|
|
30
30
|
import { modelRefOf, type ModelRef } from "../inference/reference"
|
|
31
31
|
import type { AgentComponent } from "../runtime/composition"
|
|
32
32
|
|
|
@@ -70,8 +70,8 @@ const renderedWeights = (events: ReadonlyArray<Event>, policy: ContextPolicy, mo
|
|
|
70
70
|
for (const { event, message } of renderMessageEntries(events, policy)) {
|
|
71
71
|
const continuation = message.continuation
|
|
72
72
|
const replay = continuation === undefined ? undefined : replayOf(continuation, model === undefined ? continuation : { ...continuation, provider: model.provider, model: model.model_id })
|
|
73
|
-
const chars = replay
|
|
74
|
-
? (message.content?.length ?? 0) + (message.toolCalls ?? []).reduce((sum, call) => sum + call.arguments.length, 0)
|
|
73
|
+
const chars = replay === undefined
|
|
74
|
+
? (message.content?.length ?? 0) + (message.toolCalls ?? []).reduce((sum, call) => sum + call.arguments.length, 0)
|
|
75
75
|
: JSON.stringify(continuation!.payload).length
|
|
76
76
|
weights.set(event, (weights.get(event) ?? 0) + chars)
|
|
77
77
|
}
|
|
@@ -237,22 +237,13 @@ const compactionTransition = (
|
|
|
237
237
|
const summaryModel = input.model === undefined
|
|
238
238
|
? undefined
|
|
239
239
|
: selection.resolve?.(input.model).model ?? input.model
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
...(summaryModel === undefined ? {} : { model: summaryModel }),
|
|
245
|
-
system: "",
|
|
246
|
-
tools: []
|
|
247
|
-
},
|
|
248
|
-
`compact-${input.keepFrom}`
|
|
249
|
-
).pipe(Effect.provideService(BindingSettings, yield* (selection.settings?.(summaryModel) ?? BindingSettings)))
|
|
250
|
-
if (action.kind !== "complete" || action.output.trim() === "") {
|
|
251
|
-
return yield* Effect.die(action.kind === "fail" ? action.error : new Error("Compaction requires a nonempty summary"))
|
|
252
|
-
}
|
|
240
|
+
const summary = yield* summarize(brief, { ...self, turn: `compact-${input.keepFrom}` }, summaryModel).pipe(
|
|
241
|
+
Effect.provideService(BindingSettings, yield* (selection.settings?.(summaryModel) ?? BindingSettings)),
|
|
242
|
+
Effect.orDie
|
|
243
|
+
)
|
|
253
244
|
return [compactionCompleted({
|
|
254
245
|
keepFrom: input.keepFrom,
|
|
255
|
-
summary
|
|
246
|
+
summary,
|
|
256
247
|
contextWindowTokens: input.contextWindowTokens,
|
|
257
248
|
fireTokens: input.fireTokens,
|
|
258
249
|
keepTokens: input.keepTokens,
|
|
@@ -1,184 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { ModelRef, modelRefOf, type ModelRef as ModelRefType } from "./reference"
|
|
3
|
-
|
|
4
|
-
const PolicyName = Schema.String.check(Schema.isTrimmed(), Schema.isNonEmpty())
|
|
5
|
-
|
|
6
|
-
export const ModelSelector = Schema.Struct({
|
|
7
|
-
provider: PolicyName,
|
|
8
|
-
model_ids: Schema.Union([Schema.Literal("*"), Schema.Array(PolicyName).check(Schema.isNonEmpty())])
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
export interface ModelSelector {
|
|
12
|
-
readonly provider: string
|
|
13
|
-
readonly model_ids: "*" | ReadonlyArray<string>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const ModelAllow = Schema.Union([Schema.Literal("*"), Schema.Array(ModelSelector)])
|
|
17
|
-
export type ModelAllow = "*" | ReadonlyArray<ModelSelector>
|
|
18
|
-
|
|
19
|
-
// ModelPolicy carries resolved coordinate authority. Future rule languages lower into this selector form before composition, so intersection remains the authority boundary.
|
|
20
|
-
export const ModelPolicy = Schema.Struct({
|
|
21
|
-
default: Schema.optional(ModelRef),
|
|
22
|
-
allow: ModelAllow
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
export interface ModelPolicy {
|
|
26
|
-
readonly default?: ModelRefType
|
|
27
|
-
readonly allow: ModelAllow
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const ModelPolicyOverride = Schema.Struct({
|
|
31
|
-
default: Schema.optional(ModelRef),
|
|
32
|
-
allow: Schema.optional(ModelAllow)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
export interface ModelPolicyOverride {
|
|
36
|
-
readonly default?: ModelRefType
|
|
37
|
-
readonly allow?: ModelAllow
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// DEFAULT_MODEL_POLICY leaves model authority unchanged and selects no default for an unconfigured host.
|
|
41
|
-
export const DEFAULT_MODEL_POLICY: ModelPolicy = { allow: "*" }
|
|
42
|
-
|
|
43
|
-
// DEFAULT_MODEL_POLICY_OVERRIDE inherits model authority and its default.
|
|
44
|
-
export const DEFAULT_MODEL_POLICY_OVERRIDE: ModelPolicyOverride = {}
|
|
45
|
-
|
|
46
|
-
const recordOf = (value: unknown): Record<string, unknown> | undefined =>
|
|
47
|
-
typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined
|
|
48
|
-
|
|
49
|
-
const textOf = (value: unknown): string | undefined =>
|
|
50
|
-
typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined
|
|
51
|
-
|
|
52
|
-
const selectorOf = (value: unknown, index: number): ModelSelector => {
|
|
53
|
-
const selector = recordOf(value)
|
|
54
|
-
if (selector === undefined) throw new Error(`model selector ${index} must be an object`)
|
|
55
|
-
const unknown = Object.keys(selector).filter((field) => field !== "provider" && field !== "model_ids")
|
|
56
|
-
if (unknown.length > 0) throw new Error(`model selector ${index} contains unknown fields: ${unknown.join(", ")}`)
|
|
57
|
-
const provider = textOf(selector["provider"])
|
|
58
|
-
if (provider === undefined) throw new Error(`model selector ${index} must declare provider`)
|
|
59
|
-
const rawModels = selector["model_ids"]
|
|
60
|
-
if (rawModels === "*") return { provider, model_ids: "*" }
|
|
61
|
-
if (!Array.isArray(rawModels) || rawModels.length === 0) {
|
|
62
|
-
throw new Error(`model selector ${index} model_ids must be "*" or a non-empty array`)
|
|
63
|
-
}
|
|
64
|
-
const modelIds = rawModels.map((value, modelIndex) => {
|
|
65
|
-
const model = textOf(value)
|
|
66
|
-
if (model === undefined) throw new Error(`model selector ${index} model_ids[${modelIndex}] must be a non-empty string`)
|
|
67
|
-
return model
|
|
68
|
-
})
|
|
69
|
-
return { provider, model_ids: [...new Set(modelIds)].sort() }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
type ProviderModels = "*" | Set<string>
|
|
73
|
-
|
|
74
|
-
// selectorMapOf lowers the current selector language into the set representation consumed by policy intersection. Query resolvers add syntax before this boundary and persist the normalized result.
|
|
75
|
-
const selectorMapOf = (selectors: ReadonlyArray<ModelSelector>): Map<string, ProviderModels> => {
|
|
76
|
-
const providers = new Map<string, ProviderModels>()
|
|
77
|
-
for (const selector of selectors) {
|
|
78
|
-
const current = providers.get(selector.provider)
|
|
79
|
-
if (current === "*" || selector.model_ids === "*") {
|
|
80
|
-
providers.set(selector.provider, "*")
|
|
81
|
-
} else {
|
|
82
|
-
providers.set(selector.provider, new Set([...(current ?? []), ...selector.model_ids]))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return providers
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const selectorsOf = (providers: ReadonlyMap<string, ProviderModels>): ReadonlyArray<ModelSelector> =>
|
|
89
|
-
[...providers.entries()]
|
|
90
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
91
|
-
.map(([provider, models]) => ({ provider, model_ids: models === "*" ? "*" : [...models].sort() }))
|
|
92
|
-
|
|
93
|
-
const allowOf = (rawAllow: unknown, required: boolean): ModelAllow | undefined => {
|
|
94
|
-
if (rawAllow === "*") return "*"
|
|
95
|
-
if (Array.isArray(rawAllow)) return selectorsOf(selectorMapOf(rawAllow.map(selectorOf)))
|
|
96
|
-
if (!required && rawAllow === undefined) return undefined
|
|
97
|
-
throw new Error(`model policy allow must be "*" or an array`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// modelAllowedBy reports whether one reference belongs to a policy's selected set.
|
|
101
|
-
export const modelAllowedBy = (policy: ModelPolicy, reference: ModelRefType): boolean =>
|
|
102
|
-
policy.allow === "*" || policy.allow.some((selector) =>
|
|
103
|
-
selector.provider === reference.provider &&
|
|
104
|
-
(selector.model_ids === "*" || selector.model_ids.includes(reference.model_id))
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
// modelPolicyOf validates one policy and gives duplicate selectors a stable representation.
|
|
108
|
-
export const modelPolicyOf = (value: unknown): ModelPolicy => {
|
|
109
|
-
if (value === undefined) return DEFAULT_MODEL_POLICY
|
|
110
|
-
const policy = recordOf(value)
|
|
111
|
-
if (policy === undefined) throw new Error("model policy must be an object")
|
|
112
|
-
const unknown = Object.keys(policy).filter((field) => field !== "default" && field !== "allow")
|
|
113
|
-
if (unknown.length > 0) throw new Error(`model policy contains unknown fields: ${unknown.join(", ")}`)
|
|
114
|
-
if (!("allow" in policy)) throw new Error('model policy must declare allow as "*" or an array')
|
|
115
|
-
const rawDefault = policy["default"]
|
|
116
|
-
const selected = modelRefOf(rawDefault)
|
|
117
|
-
if (rawDefault !== undefined && selected === undefined) throw new Error("model policy default must be { provider, model_id }")
|
|
118
|
-
const allow = allowOf(policy["allow"], true)!
|
|
119
|
-
const normalized: ModelPolicy = { ...(selected === undefined ? {} : { default: selected }), allow }
|
|
120
|
-
if (selected !== undefined && !modelAllowedBy(normalized, selected)) {
|
|
121
|
-
throw new Error(`model policy default ${selected.provider}/${selected.model_id} is excluded by allow`)
|
|
122
|
-
}
|
|
123
|
-
return normalized
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// modelPolicyOverrideOf validates an optional downstream attenuation and default override.
|
|
127
|
-
export const modelPolicyOverrideOf = (value: unknown): ModelPolicyOverride => {
|
|
128
|
-
if (value === undefined) return DEFAULT_MODEL_POLICY_OVERRIDE
|
|
129
|
-
const policy = recordOf(value)
|
|
130
|
-
if (policy === undefined) throw new Error("model policy override must be an object")
|
|
131
|
-
const unknown = Object.keys(policy).filter((field) => field !== "default" && field !== "allow")
|
|
132
|
-
if (unknown.length > 0) throw new Error(`model policy override contains unknown fields: ${unknown.join(", ")}`)
|
|
133
|
-
const rawDefault = policy["default"]
|
|
134
|
-
const selected = modelRefOf(rawDefault)
|
|
135
|
-
if (rawDefault !== undefined && selected === undefined) throw new Error("model policy override default must be { provider, model_id }")
|
|
136
|
-
const allow = allowOf(policy["allow"], false)
|
|
137
|
-
const normalized: ModelPolicyOverride = {
|
|
138
|
-
...(selected === undefined ? {} : { default: selected }),
|
|
139
|
-
...(allow === undefined ? {} : { allow })
|
|
140
|
-
}
|
|
141
|
-
if (selected !== undefined && allow !== undefined && !modelAllowedBy({ allow }, selected)) {
|
|
142
|
-
throw new Error(`model policy override default ${selected.provider}/${selected.model_id} is excluded by allow`)
|
|
143
|
-
}
|
|
144
|
-
return normalized
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const intersectModels = (left: ProviderModels, right: ProviderModels): ProviderModels => {
|
|
148
|
-
if (left === "*") return right === "*" ? "*" : new Set(right)
|
|
149
|
-
if (right === "*") return new Set(left)
|
|
150
|
-
return new Set([...left].filter((model) => right.has(model)))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// intersectModelPolicies returns the authority shared by every layer (packages/core/tla/component/ModelPolicy.tla, ChildCannotWiden; HostCeiling).
|
|
154
|
-
export const intersectModelPolicies = (policies: ReadonlyArray<ModelPolicy>): ModelPolicy => {
|
|
155
|
-
const explicit = policies.filter((policy) => policy.allow !== "*")
|
|
156
|
-
if (explicit.length === 0) return DEFAULT_MODEL_POLICY
|
|
157
|
-
let intersection = selectorMapOf(explicit[0]!.allow as ReadonlyArray<ModelSelector>)
|
|
158
|
-
for (const policy of explicit.slice(1)) {
|
|
159
|
-
const right = selectorMapOf(policy.allow as ReadonlyArray<ModelSelector>)
|
|
160
|
-
const next = new Map<string, ProviderModels>()
|
|
161
|
-
for (const [provider, models] of intersection) {
|
|
162
|
-
const other = right.get(provider)
|
|
163
|
-
if (other === undefined) continue
|
|
164
|
-
const shared = intersectModels(models, other)
|
|
165
|
-
if (shared === "*" || shared.size > 0) next.set(provider, shared)
|
|
166
|
-
}
|
|
167
|
-
intersection = next
|
|
168
|
-
}
|
|
169
|
-
return { allow: selectorsOf(intersection) }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// applyModelPolicy attenuates incoming authority and inherits or overrides its default (packages/core/tla/component/ModelPolicy.tla, ChildCannotWiden; DefaultAllowed).
|
|
173
|
-
export const applyModelPolicy = (incoming: ModelPolicy, override: ModelPolicyOverride): ModelPolicy => {
|
|
174
|
-
const authority = intersectModelPolicies([incoming, { allow: override.allow ?? "*" }])
|
|
175
|
-
const selected = override.default ?? incoming.default
|
|
176
|
-
if (selected !== undefined && !modelAllowedBy(authority, selected)) {
|
|
177
|
-
throw new Error(`effective model policy excludes default ${selected.provider}/${selected.model_id}; supply an allowed default`)
|
|
178
|
-
}
|
|
179
|
-
return { ...authority, ...(selected === undefined ? {} : { default: selected }) }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// modelPolicyScopeOf returns a stable cursor scope for a policy intersection.
|
|
183
|
-
export const modelPolicyScopeOf = (policies: ReadonlyArray<ModelPolicy>): string =>
|
|
184
|
-
JSON.stringify(intersectModelPolicies(policies).allow)
|
|
1
|
+
export * from "tardie/model/access"
|
|
@@ -3,7 +3,7 @@ import type { Event } from "tardie/core/log/event"
|
|
|
3
3
|
import type { ContextPolicy } from "../component/compaction"
|
|
4
4
|
import type { OutputFallback } from "../output/contract"
|
|
5
5
|
import type { ModelRef } from "./reference"
|
|
6
|
-
import { DEFAULT_MODEL_POLICY_OVERRIDE, type
|
|
6
|
+
import { DEFAULT_MODEL_POLICY_OVERRIDE, type ModelPolicyOverride } from "./access"
|
|
7
7
|
import type { InferenceIdentity } from "./observer"
|
|
8
8
|
|
|
9
9
|
// InferPolicy states the process-crash ceiling and model authority applied by the inference machine. Output correction bounds belong to the mounted output component (component/repair.ts, RepairPolicy).
|
|
@@ -26,11 +26,7 @@ export interface InferRequest {
|
|
|
26
26
|
readonly output?: { readonly fallback: OutputFallback; readonly system?: string }
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
30
|
-
readonly model: ModelRef
|
|
31
|
-
// models is the interpreter's current authority for validating this call. It is not recorded.
|
|
32
|
-
readonly models?: ModelPolicy
|
|
33
|
-
}
|
|
29
|
+
export type { ModelResolution } from "tardie/model/reference"
|
|
34
30
|
|
|
35
31
|
// NativeOutputSupport declares support for native structured output beside tools (component/native-output.ts).
|
|
36
32
|
export class NativeOutputSupport extends Context.Service<
|
|
@@ -1,36 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { AiError } from "effect/unstable/ai"
|
|
3
|
-
|
|
4
|
-
export const ModelError = Schema.toCodecJson(AiError.AiError)
|
|
5
|
-
export type ModelError = typeof ModelError.Type
|
|
6
|
-
|
|
7
|
-
// encodeModelError preserves JSON duration encoding and native HTTP redaction (error.test.ts, packages/model/src/binding/errors.test.ts).
|
|
8
|
-
export const encodeModelError = (error: AiError.AiError): Schema.Json => {
|
|
9
|
-
const object = Schema.Record(Schema.String, Schema.Json)
|
|
10
|
-
const encoded = Schema.decodeUnknownSync(object)(Schema.encodeSync(ModelError)(error))
|
|
11
|
-
const reason = encoded.reason
|
|
12
|
-
if ("http" in error.reason && error.reason.http !== undefined && Schema.is(object)(reason)) {
|
|
13
|
-
const http = Schema.decodeUnknownSync(Schema.Json)(
|
|
14
|
-
JSON.parse(Schema.encodeSync(Schema.fromJsonString(AiError.HttpContext))(error.reason.http))
|
|
15
|
-
)
|
|
16
|
-
return { ...encoded, reason: { ...Schema.decodeSync(object)(reason), http } }
|
|
17
|
-
}
|
|
18
|
-
return encoded
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// unknownModelError preserves serializable evidence from unclassified failures.
|
|
22
|
-
export const unknownModelError = (cause: unknown): AiError.AiError => {
|
|
23
|
-
if (AiError.isAiError(cause)) return cause
|
|
24
|
-
const description = cause instanceof Error ? cause.message || cause.name : typeof cause === "string" ? cause : typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string" ? cause.message : "Model inference failed"
|
|
25
|
-
const evidence = cause instanceof Error
|
|
26
|
-
? { ...Object.fromEntries(Object.entries(cause).filter(([, value]) => Schema.is(Schema.Json)(value))), name: cause.name, message: cause.message, ...(cause.stack === undefined ? {} : { stack: cause.stack }) }
|
|
27
|
-
: Schema.is(Schema.Json)(cause) ? cause : String(cause)
|
|
28
|
-
return AiError.make({ module: "Tardigrade", method: "inference", reason: AiError.UnknownError.make({ description, metadata: { tardigrade: { evidence } } }) })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// modelErrorOf decodes persisted native evidence; historical envelopes remain historical.
|
|
32
|
-
export const modelErrorOf = (error: unknown): AiError.AiError | undefined => {
|
|
33
|
-
if (AiError.isAiError(error)) return error
|
|
34
|
-
const decoded = Schema.decodeUnknownOption(ModelError)(error)
|
|
35
|
-
return decoded._tag === "Some" ? decoded.value : undefined
|
|
36
|
-
}
|
|
1
|
+
export * from "tardie/model/error"
|
|
@@ -4,9 +4,9 @@ import { bindTransitionContext } from "tardie/core/transition/transition"
|
|
|
4
4
|
import { eventAt, eventPositionOf } from "tardie/core/event"
|
|
5
5
|
import { RetrySchedule, retryDelayOf } from "./retry"
|
|
6
6
|
import { LanguageModel } from "effect/unstable/ai"
|
|
7
|
-
import { react } from "
|
|
7
|
+
import { react } from "./model/index"
|
|
8
8
|
import { unknownModelError } from "./error"
|
|
9
|
-
import { BindingSettings, ModelSelection } from "
|
|
9
|
+
import { BindingSettings, ModelSelection } from "./model/settings"
|
|
10
10
|
import { Cause, Clock, Effect, Random, Schema } from "effect"
|
|
11
11
|
import { EventLog } from "tardie/core/log"
|
|
12
12
|
import { HashMap, Option } from "effect"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { Prompt } from "effect/unstable/ai"
|
|
3
|
+
import type { ProviderContinuation } from "../continuation"
|
|
4
|
+
|
|
5
|
+
export interface ReplayIdentity {
|
|
6
|
+
readonly provider: string
|
|
7
|
+
readonly protocol: string
|
|
8
|
+
readonly model: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const replayOf = (continuation: ProviderContinuation | undefined, identity: ReplayIdentity): ReadonlyArray<Prompt.Message> | undefined => {
|
|
12
|
+
if (continuation === undefined || continuation.provider !== identity.provider || continuation.protocol !== identity.protocol || continuation.model !== identity.model) return undefined
|
|
13
|
+
return Schema.decodeSync(Prompt.Prompt)(continuation.payload).content
|
|
14
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { historyOf, importSchema } from "./prompt"
|
|
2
2
|
import { actionOf, responseEvidence, errorOf } from "./response"
|
|
3
|
-
import {
|
|
4
|
-
import type { InferDelta } from "../
|
|
5
|
-
import type { InferRequest } from "../
|
|
3
|
+
import { observeResponse } from "tardie/model/stream/delivery"
|
|
4
|
+
import type { InferDelta } from "../observer"
|
|
5
|
+
import type { InferRequest } from "../contract"
|
|
6
6
|
import { FetchHttpClient } from "effect/unstable/http"
|
|
7
7
|
import { Duration, Effect, Option } from "effect"
|
|
8
|
-
import { AiError,
|
|
9
|
-
import { modelRequest } from "../
|
|
10
|
-
import type { Action } from "
|
|
11
|
-
import { collectResponse } from "
|
|
8
|
+
import { AiError, Prompt, Response, Tool, Toolkit } from "effect/unstable/ai"
|
|
9
|
+
import { modelRequest } from "../request"
|
|
10
|
+
import type { Action } from "../../log/events"
|
|
11
|
+
import { collectResponse } from "tardie/model/stream/collect"
|
|
12
12
|
import { BindingSettings, BindingInvocation, CurrentModel, ProviderRequestKey } from "./settings"
|
|
13
13
|
|
|
14
14
|
import { fallbackSystemFor, outputModeOf, outputSchemaFor, outputNameFor } from "./output"
|
|
15
15
|
|
|
16
|
-
import { StreamIncomplete, StreamBoundExceeded, StreamTruncated } from "
|
|
16
|
+
import { StreamIncomplete, StreamBoundExceeded, StreamTruncated } from "tardie/model/stream/request"
|
|
17
17
|
|
|
18
18
|
// react translates one Effect response into an action for the inference machine.
|
|
19
19
|
export const react = (request: InferRequest, key?: string, signal?: AbortSignal, onDelta?: (delta: InferDelta) => void) => Effect.gen(function* () {
|
|
@@ -21,9 +21,8 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
|
|
|
21
21
|
const { provider: providerId, protocol, policy } = options
|
|
22
22
|
const endpoint = { provider: providerId, model: request.model?.model_id ?? options.model }
|
|
23
23
|
const outputPolicy = { ...endpoint, ...(options.output === undefined ? {} : { output: options.output }) }
|
|
24
|
-
const observerPolicy = options.observer === undefined ? undefined : observerPolicyOf(options.observer)
|
|
25
24
|
if (signal?.aborted) return yield* Effect.interrupt
|
|
26
|
-
const delivery =
|
|
25
|
+
const delivery = yield* observeResponse(request.identity, { provider: providerId, model_id: endpoint.model }, key, options.observer, onDelta)
|
|
27
26
|
return yield* Effect.suspend(() => {
|
|
28
27
|
let reportedCostUsd: number | undefined
|
|
29
28
|
let observed: Response.AnyPart[] = []
|
|
@@ -55,18 +54,10 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
|
|
|
55
54
|
const response = yield* Effect.gen(function* () {
|
|
56
55
|
const maxOutputTokens = policy.maxOutputTokens
|
|
57
56
|
observed = []
|
|
58
|
-
let sequence = 0
|
|
59
|
-
let blockIndex = -1
|
|
60
|
-
const physicalAttempt = yield* IdGenerator.defaultIdGenerator.generateId()
|
|
61
57
|
const result = yield* collectResponse(Prompt.setSystem(Prompt.fromMessages(history), system), Toolkit.make(...tools), (part) => {
|
|
62
58
|
observed.push(part)
|
|
63
59
|
if (part.type === "finish") reportedCostUsd = options.reportedCostUsd?.(part)
|
|
64
|
-
|
|
65
|
-
if (part.type === "text-delta" || part.type === "reasoning-delta") {
|
|
66
|
-
const delta: InferDelta = { ...request.identity, logicalAttempt: key ?? request.identity.turn, physicalAttempt, model: { provider: providerId, model_id: endpoint.model }, blockIndex: Math.max(0, blockIndex), sequence: sequence++, text: part.delta, ...(part.type === "reasoning-delta" ? { kind: "reasoning" as const } : {}) }
|
|
67
|
-
onDelta?.(delta)
|
|
68
|
-
return delivery?.offer(delta).pipe(Effect.asVoid)
|
|
69
|
-
}
|
|
60
|
+
return delivery.onPart(part)
|
|
70
61
|
}, responseFormat, policy.timeout)
|
|
71
62
|
if (result.parts.some((part) => part.type === "finish" && part.reason === "length")) return yield* new StreamTruncated({ maxOutputTokens })
|
|
72
63
|
return result
|
|
@@ -108,5 +99,5 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
|
|
|
108
99
|
} satisfies Action
|
|
109
100
|
})),
|
|
110
101
|
Effect.map((action): Action => reportedCostUsd === undefined ? action : { ...action, reportedCostUsd }))
|
|
111
|
-
}).pipe(Effect.ensuring(delivery
|
|
102
|
+
}).pipe(Effect.ensuring(delivery.finish))
|
|
112
103
|
})
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { NATIVE_MODE, outputNameErrors, outputProfileErrors, type OutputMode } from "
|
|
2
|
-
import type { OutputRequest } from "../
|
|
1
|
+
import { NATIVE_MODE, outputNameErrors, outputProfileErrors, type OutputMode } from "../../output/contract"
|
|
2
|
+
import type { OutputRequest } from "../request"
|
|
3
3
|
|
|
4
4
|
// The binding's half of the output contract: what an endpoint promises about a declared schema,
|
|
5
5
|
// what that promise costs a request, and how the schema reaches each wire. A turn that declares a
|
|
6
6
|
// contract the configured endpoint cannot honour fails here, before a socket opens
|
|
7
7
|
// (binding/index.test.ts).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// tool-combination question to answer, and one that promises a native strict schema must say
|
|
12
|
-
// whether that schema may ride the same call as a tool list.
|
|
13
|
-
export type OutputCapability =
|
|
14
|
-
| { readonly guarantee: "none" }
|
|
15
|
-
| { readonly guarantee: "native"; readonly withTools: boolean }
|
|
9
|
+
import type { OutputCapability } from "tardie/model/output"
|
|
10
|
+
export type { OutputCapability } from "tardie/model/output"
|
|
16
11
|
|
|
17
12
|
// UNPROVEN is the message an endpoint that declared nothing earns. It names the two ways out, so
|
|
18
13
|
// an operator reading a failed turn knows what to set rather than which source to read.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JsonSchema, Schema, SchemaRepresentation } from "effect"
|
|
2
2
|
import { Prompt } from "effect/unstable/ai"
|
|
3
|
-
import type { AgentMessage } from "
|
|
3
|
+
import type { AgentMessage } from "../../projection/messages"
|
|
4
4
|
import { replayOf, type ReplayIdentity } from "./continuation"
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_SCHEMA_IMPORT_OPTIONS = { patterns: "apply" } as const satisfies SchemaRepresentation.FromJsonSchemaOptions
|
|
@@ -11,18 +11,16 @@ export const historyOf = (messages: ReadonlyArray<AgentMessage>, identity: Repla
|
|
|
11
11
|
const names = new Map<string, string>()
|
|
12
12
|
return messages.flatMap((message) => {
|
|
13
13
|
for (const call of message.toolCalls ?? []) names.set(call.id, call.name)
|
|
14
|
-
|
|
15
|
-
return replay.messages ?? promptMessage(message, names, replay.reasoning)
|
|
14
|
+
return replayOf(message.continuation, identity) ?? promptMessage(message, names)
|
|
16
15
|
})
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const promptMessage = (message: AgentMessage, names: ReadonlyMap<string, string
|
|
18
|
+
const promptMessage = (message: AgentMessage, names: ReadonlyMap<string, string>): ReadonlyArray<Prompt.Message> => {
|
|
20
19
|
if (message.role === "user") return [Prompt.userMessage({ content: [Prompt.makePart("text", { text: message.content ?? "" })] })]
|
|
21
20
|
if (message.role === "tool") return [Prompt.toolMessage({ content: [Prompt.makePart("tool-result", {
|
|
22
21
|
id: message.toolCallId ?? "", name: names.get(message.toolCallId ?? "") ?? "", result: message.content ?? "", isFailure: message.isFailure ?? false, providerExecuted: false
|
|
23
22
|
})] })]
|
|
24
23
|
return [Prompt.assistantMessage({ content: [
|
|
25
|
-
...reasoning.map((text) => Prompt.makePart("text", { text })),
|
|
26
24
|
...(message.content ? [Prompt.makePart("text", { text: message.content })] : []),
|
|
27
25
|
...(message.toolCalls ?? []).map((call) => Prompt.makePart("tool-call", { id: call.id, name: call.name, params: JSON.parse(call.arguments), providerExecuted: false }))
|
|
28
26
|
] })]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect"
|
|
2
2
|
import { AiError, Response } from "effect/unstable/ai"
|
|
3
|
-
import { type Action, type ToolCall } from "
|
|
4
|
-
import { ToolCallValidationError } from "
|
|
5
|
-
import { unknownModelError } from "../
|
|
3
|
+
import { type Action, type ToolCall } from "../../log/events"
|
|
4
|
+
import { ToolCallValidationError } from "tardie/model/stream/collect"
|
|
5
|
+
import { unknownModelError } from "../error"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* actionOf translates Effect response parts into the agent's Action contract.
|
|
@@ -40,7 +40,7 @@ export const actionOf = (parts: ReadonlyArray<Response.AnyPart>, served: Pick<Ac
|
|
|
40
40
|
const calls: ToolCall[] = []
|
|
41
41
|
const errors: AiError.AiError[] = []
|
|
42
42
|
for (const part of parts) {
|
|
43
|
-
if (part.type === "tool-call") calls.push({ callId: part.id, name: part.name, arguments: part.params })
|
|
43
|
+
if (part.type === "tool-call" && !part.providerExecuted) calls.push({ callId: part.id, name: part.name, arguments: part.params })
|
|
44
44
|
if (part.type === "error") {
|
|
45
45
|
if (!Schema.is(ToolCallValidationError)(part.error)) {
|
|
46
46
|
errors.push(yield* errorOf(part.error))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Context } from "effect"
|
|
2
|
+
import type { InferRequest } from "../contract"
|
|
3
|
+
import type { InferDelta } from "../observer"
|
|
4
|
+
export { BindingSettings, CurrentModel, ModelSelection, ProviderRequestKey, type BindingOptions } from "tardie/model/settings"
|
|
5
|
+
|
|
6
|
+
export const BindingInvocation = Context.Reference<{
|
|
7
|
+
readonly request: InferRequest
|
|
8
|
+
readonly key?: string | undefined
|
|
9
|
+
readonly signal?: AbortSignal | undefined
|
|
10
|
+
readonly onDelta?: ((delta: InferDelta) => void) | undefined
|
|
11
|
+
} | undefined>("tardie/BindingInvocation", { defaultValue: () => undefined })
|
|
@@ -1,39 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ModelRef } from "./reference"
|
|
3
|
-
|
|
4
|
-
// InferenceIdentity identifies the actor turn that opened a logical model attempt (index.test.ts, "root and child inference requests carry their actor identity"). instance names the actor instance, so two sessions of one actor name stay distinguishable at the model seam (index.test.ts, "two host instances of one actor name carry distinct instance identities").
|
|
5
|
-
export interface InferenceIdentity {
|
|
6
|
-
readonly actor: string
|
|
7
|
-
readonly instance: string
|
|
8
|
-
readonly thread: string
|
|
9
|
-
readonly turn: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// InferDelta is ephemeral answer or reasoning text from one physical provider request. Sequence is zero-based within that request, so a consumer can detect a dropped delta (packages/model/src/host.test.ts).
|
|
13
|
-
export interface InferDelta extends InferenceIdentity {
|
|
14
|
-
readonly logicalAttempt: string
|
|
15
|
-
readonly physicalAttempt: string
|
|
16
|
-
readonly model: ModelRef
|
|
17
|
-
readonly blockIndex: number
|
|
18
|
-
readonly sequence: number
|
|
19
|
-
readonly kind?: "text" | "reasoning"
|
|
20
|
-
readonly text: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// InferenceObserver receives ephemeral output outside the durable log. Failure and timeout discard that delivery without changing inference (packages/model/src/inference/observer.test.ts).
|
|
24
|
-
export interface InferenceObserver {
|
|
25
|
-
readonly onDelta: (delta: InferDelta) => Effect.Effect<void, Error>
|
|
26
|
-
readonly policy?: Partial<InferenceObserverPolicy>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// InferenceObserverPolicy bounds pending deliveries and each observer call.
|
|
30
|
-
export interface InferenceObserverPolicy {
|
|
31
|
-
readonly bufferCapacity: number
|
|
32
|
-
readonly deliveryTimeoutMs: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// DEFAULT_INFERENCE_OBSERVER_POLICY bounds best-effort delivery while each observer may override both fields.
|
|
36
|
-
export const DEFAULT_INFERENCE_OBSERVER_POLICY: InferenceObserverPolicy = {
|
|
37
|
-
bufferCapacity: 64,
|
|
38
|
-
deliveryTimeoutMs: 100
|
|
39
|
-
}
|
|
1
|
+
export * from "tardie/model/stream/observer"
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// ModelRef identifies the provider and model an actor requests. The host supplies the provider connection.
|
|
4
|
-
export const ModelRef = Schema.Struct({
|
|
5
|
-
provider: Schema.NonEmptyString,
|
|
6
|
-
model_id: Schema.NonEmptyString
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
export type ModelRef = typeof ModelRef.Type
|
|
10
|
-
|
|
11
|
-
// modelRefOf reads a complete model reference from an untyped message or tool argument.
|
|
12
|
-
export const modelRefOf = (value: unknown): ModelRef | undefined => {
|
|
13
|
-
if (typeof value !== "object" || value === null) return undefined
|
|
14
|
-
const reference = value as { readonly provider?: unknown; readonly model_id?: unknown }
|
|
15
|
-
if (typeof reference.provider !== "string" || reference.provider.trim().length === 0) return undefined
|
|
16
|
-
if (typeof reference.model_id !== "string" || reference.model_id.trim().length === 0) return undefined
|
|
17
|
-
return { provider: reference.provider.trim(), model_id: reference.model_id.trim() }
|
|
18
|
-
}
|
|
1
|
+
export * from "tardie/model/reference"
|