@dbx-tools/appkit-model-gateway 0.9.190 → 0.9.193
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/README.md +6 -0
- package/index.ts +7 -1
- package/lib/index.d.ts +7 -1
- package/lib/index.js +6 -2
- package/lib/src/ai-decide.d.ts +50 -0
- package/lib/src/ai-decide.js +406 -0
- package/lib/src/gateway.d.ts +6 -2
- package/lib/src/gateway.js +72 -16
- package/lib/src/model-context.d.ts +32 -0
- package/lib/src/model-context.js +270 -0
- package/lib/src/models.d.ts +3 -3
- package/lib/src/models.js +55 -8
- package/lib/src/reasoning-adapt.js +2 -5
- package/lib/src/routes.js +18 -2
- package/lib/src/smart-router.d.ts +4 -7
- package/lib/src/smart-router.js +7 -175
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/ai-decide.ts +552 -0
- package/src/gateway.ts +96 -14
- package/src/model-context.ts +306 -0
- package/src/models.ts +66 -4
- package/src/reasoning-adapt.ts +1 -5
- package/src/routes.ts +16 -0
- package/src/smart-router.ts +8 -187
package/src/models.ts
CHANGED
|
@@ -12,10 +12,11 @@ import {
|
|
|
12
12
|
ModelFamily,
|
|
13
13
|
modelFamily,
|
|
14
14
|
modelFamilyRank,
|
|
15
|
+
modelMetadataFor,
|
|
15
16
|
versionTuple,
|
|
16
17
|
} from "@dbx-tools/model";
|
|
17
18
|
import { ModelClass, ReasoningEffort } from "@dbx-tools/shared-model";
|
|
18
|
-
import { SMART_ROUTER_MODEL_ID } from "@dbx-tools/shared-model-gateway";
|
|
19
|
+
import { AI_DECIDE_MODEL_ID, SMART_ROUTER_MODEL_ID } from "@dbx-tools/shared-model-gateway";
|
|
19
20
|
import type {
|
|
20
21
|
CodexModel,
|
|
21
22
|
CodexModelListResponse,
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
const CODEX_BASE_INSTRUCTIONS =
|
|
30
31
|
"You are a coding agent. Follow the user's instructions and use the available tools to work in the current repository.";
|
|
31
32
|
const CODEX_SMART_ROUTER_PRIORITY = 9_000;
|
|
33
|
+
const CODEX_AI_DECIDE_PRIORITY = 9_001;
|
|
32
34
|
const CODEX_MODEL_PRIORITY_BASE = 10_000;
|
|
33
35
|
const CODEX_FAMILY_ORDER = [
|
|
34
36
|
ModelFamily.Gpt,
|
|
@@ -52,6 +54,8 @@ export function listModelsPayload(
|
|
|
52
54
|
preserveOrder?: boolean,
|
|
53
55
|
preferSystemModels?: boolean,
|
|
54
56
|
includeSmartRouter?: boolean,
|
|
57
|
+
includeAiDecide?: boolean,
|
|
58
|
+
includeMetadata?: boolean,
|
|
55
59
|
): CodexModelListResponse;
|
|
56
60
|
export function listModelsPayload(
|
|
57
61
|
targets: readonly ModelTarget[],
|
|
@@ -59,6 +63,8 @@ export function listModelsPayload(
|
|
|
59
63
|
preserveOrder?: boolean,
|
|
60
64
|
preferSystemModels?: boolean,
|
|
61
65
|
includeSmartRouter?: boolean,
|
|
66
|
+
includeAiDecide?: boolean,
|
|
67
|
+
includeMetadata?: boolean,
|
|
62
68
|
): OpenAIModelListResponse;
|
|
63
69
|
export function listModelsPayload(
|
|
64
70
|
targets: readonly ModelTarget[],
|
|
@@ -66,6 +72,8 @@ export function listModelsPayload(
|
|
|
66
72
|
preserveOrder?: boolean,
|
|
67
73
|
preferSystemModels?: boolean,
|
|
68
74
|
includeSmartRouter?: boolean,
|
|
75
|
+
includeAiDecide?: boolean,
|
|
76
|
+
includeMetadata?: boolean,
|
|
69
77
|
): ModelListResponse;
|
|
70
78
|
export function listModelsPayload(
|
|
71
79
|
targets: readonly ModelTarget[],
|
|
@@ -73,6 +81,8 @@ export function listModelsPayload(
|
|
|
73
81
|
preserveOrder = false,
|
|
74
82
|
preferSystemModels = false,
|
|
75
83
|
includeSmartRouter = false,
|
|
84
|
+
includeAiDecide = false,
|
|
85
|
+
includeMetadata = false,
|
|
76
86
|
): ModelListResponse {
|
|
77
87
|
const available = targets
|
|
78
88
|
.filter((target) => target.endpoint?.status?.deprecated !== true)
|
|
@@ -85,6 +95,9 @@ export function listModelsPayload(
|
|
|
85
95
|
);
|
|
86
96
|
}
|
|
87
97
|
const smartRouterCandidates = available.filter((target) => target.capabilities.chat);
|
|
98
|
+
if (includeAiDecide && smartRouterCandidates.length > 0) {
|
|
99
|
+
available.unshift(aiDecideModelTarget(smartRouterCandidates));
|
|
100
|
+
}
|
|
88
101
|
if (includeSmartRouter && smartRouterCandidates.length > 0) {
|
|
89
102
|
available.unshift(smartRouterModelTarget(smartRouterCandidates));
|
|
90
103
|
}
|
|
@@ -96,29 +109,35 @@ export function listModelsPayload(
|
|
|
96
109
|
target,
|
|
97
110
|
target.id === SMART_ROUTER_MODEL_ID
|
|
98
111
|
? CODEX_SMART_ROUTER_PRIORITY
|
|
99
|
-
:
|
|
112
|
+
: target.id === AI_DECIDE_MODEL_ID
|
|
113
|
+
? CODEX_AI_DECIDE_PRIORITY
|
|
114
|
+
: CODEX_MODEL_PRIORITY_BASE + modelIndex++,
|
|
100
115
|
preferSystemModels,
|
|
116
|
+
includeMetadata,
|
|
101
117
|
),
|
|
102
118
|
),
|
|
103
119
|
};
|
|
104
120
|
}
|
|
105
121
|
return {
|
|
106
122
|
object: "list",
|
|
107
|
-
data: available.map(openAiModel),
|
|
123
|
+
data: available.map((target) => openAiModel(target, includeMetadata)),
|
|
108
124
|
};
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
function openAiModel(target: ModelTarget): OpenAIModel {
|
|
127
|
+
function openAiModel(target: ModelTarget, includeMetadata: boolean): OpenAIModel {
|
|
112
128
|
return {
|
|
113
129
|
id: target.id,
|
|
114
130
|
object: "model",
|
|
115
131
|
created: 0,
|
|
116
132
|
owned_by: "databricks",
|
|
117
133
|
name: target.displayName,
|
|
134
|
+
model_source: target.endpoint?.modelSource ?? "custom",
|
|
118
135
|
task: target.endpoint?.task,
|
|
119
136
|
status: target.endpoint?.status ?? { deprecated: false },
|
|
137
|
+
...(includeMetadata ? { metadata: modelMetadataFor(target.endpoint ?? target.id) } : {}),
|
|
120
138
|
capabilities: {
|
|
121
139
|
smart_router: target.id === SMART_ROUTER_MODEL_ID,
|
|
140
|
+
ai_decide: target.id === AI_DECIDE_MODEL_ID,
|
|
122
141
|
tools: target.capabilities.tools,
|
|
123
142
|
reasoning: target.reasoningEfforts,
|
|
124
143
|
responses: target.capabilities.responses,
|
|
@@ -134,6 +153,46 @@ function openAiModel(target: ModelTarget): OpenAIModel {
|
|
|
134
153
|
};
|
|
135
154
|
}
|
|
136
155
|
|
|
156
|
+
function aiDecideModelTarget(candidates: readonly ModelTarget[]): ModelTarget {
|
|
157
|
+
const has = (capability: keyof ModelCapabilities) =>
|
|
158
|
+
candidates.some((target) => target.capabilities[capability]);
|
|
159
|
+
const reasoningEfforts = Object.values(ReasoningEffort).filter((effort) =>
|
|
160
|
+
candidates.some((target) => target.reasoningEfforts.includes(effort)),
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
id: AI_DECIDE_MODEL_ID,
|
|
164
|
+
aliases: [AI_DECIDE_MODEL_ID],
|
|
165
|
+
displayName: "AI Decide",
|
|
166
|
+
endpoint: {
|
|
167
|
+
name: AI_DECIDE_MODEL_ID,
|
|
168
|
+
displayName: "AI Decide",
|
|
169
|
+
description: "Uses Databricks ai_decide to choose a sufficient foundation model.",
|
|
170
|
+
task: "llm/v1/chat",
|
|
171
|
+
class: ModelClass.ChatBalanced,
|
|
172
|
+
modelSource: "foundation",
|
|
173
|
+
status: { deprecated: false },
|
|
174
|
+
},
|
|
175
|
+
capabilities: {
|
|
176
|
+
responses: true,
|
|
177
|
+
openResponses: true,
|
|
178
|
+
chat: true,
|
|
179
|
+
anthropic: true,
|
|
180
|
+
embeddings: false,
|
|
181
|
+
aiGatewayCodex: true,
|
|
182
|
+
tools: has("tools"),
|
|
183
|
+
reasoning: reasoningEfforts.length > 0,
|
|
184
|
+
streaming: true,
|
|
185
|
+
parallelTools: has("parallelTools"),
|
|
186
|
+
customTools: has("customTools"),
|
|
187
|
+
structuredOutput: has("structuredOutput"),
|
|
188
|
+
webSearch: has("webSearch"),
|
|
189
|
+
imageInput: has("imageInput"),
|
|
190
|
+
applyPatch: has("applyPatch"),
|
|
191
|
+
},
|
|
192
|
+
reasoningEfforts,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
137
196
|
function smartRouterModelTarget(candidates: readonly ModelTarget[]): ModelTarget {
|
|
138
197
|
const has = (capability: keyof ModelCapabilities) =>
|
|
139
198
|
candidates.some((target) => target.capabilities[capability]);
|
|
@@ -151,6 +210,7 @@ function smartRouterModelTarget(candidates: readonly ModelTarget[]): ModelTarget
|
|
|
151
210
|
"Uses Databricks task_v3 Smart Router to select a compatible system model for each turn.",
|
|
152
211
|
task: "llm/v1/chat",
|
|
153
212
|
class: ModelClass.ChatBalanced,
|
|
213
|
+
modelSource: "foundation",
|
|
154
214
|
status: { deprecated: false },
|
|
155
215
|
},
|
|
156
216
|
capabilities: {
|
|
@@ -187,6 +247,7 @@ function codexModel(
|
|
|
187
247
|
target: ModelTarget,
|
|
188
248
|
priority: number,
|
|
189
249
|
preferSystemModels: boolean,
|
|
250
|
+
includeMetadata: boolean,
|
|
190
251
|
): CodexModel {
|
|
191
252
|
const efforts = target.reasoningEfforts;
|
|
192
253
|
return {
|
|
@@ -195,6 +256,7 @@ function codexModel(
|
|
|
195
256
|
description: target.endpoint?.description ?? "Databricks model service",
|
|
196
257
|
base_instructions: CODEX_BASE_INSTRUCTIONS,
|
|
197
258
|
status: target.endpoint?.status ?? { deprecated: false },
|
|
259
|
+
...(includeMetadata ? { metadata: modelMetadataFor(target.endpoint ?? target.id) } : {}),
|
|
198
260
|
supported_reasoning_levels: efforts.map((effort) => ({
|
|
199
261
|
effort,
|
|
200
262
|
description: REASONING_DESCRIPTIONS[effort] ?? `Use the ${effort} reasoning budget`,
|
package/src/reasoning-adapt.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
adaptRequestReasoning,
|
|
15
15
|
parseReasoningLevels,
|
|
16
16
|
} from "@dbx-tools/model";
|
|
17
|
-
import { log } from "@dbx-tools/shared-core";
|
|
17
|
+
import { isRecord, log } from "@dbx-tools/shared-core";
|
|
18
18
|
|
|
19
19
|
const logger = log.logger("appkit/model-gateway/reasoning");
|
|
20
20
|
const INVALID_ENCRYPTED_REASONING_PATTERN =
|
|
@@ -123,7 +123,3 @@ function stripRequestReasoning(
|
|
|
123
123
|
delete stripped.reasoning_effort;
|
|
124
124
|
return stripped;
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
128
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
-
}
|
package/src/routes.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { log, toError } from "@dbx-tools/shared-core";
|
|
|
10
10
|
import type { ClientProtocol } from "@dbx-tools/shared-model-gateway";
|
|
11
11
|
import type express from "express";
|
|
12
12
|
|
|
13
|
+
import { AiDecideRouterError } from "./ai-decide.ts";
|
|
13
14
|
import { ModelGateway, ModelGatewayError } from "./gateway.ts";
|
|
14
15
|
import { UnsupportedGatewayFeatureError } from "./router.ts";
|
|
15
16
|
import { SmartRouterError } from "./smart-router.ts";
|
|
@@ -33,6 +34,8 @@ export async function sendModels(
|
|
|
33
34
|
const payload = await gateway.models(
|
|
34
35
|
request.header("originator"),
|
|
35
36
|
optionalSearch(request.query.search),
|
|
37
|
+
optionalModelSource(request.query.model_source ?? request.query.modelSource),
|
|
38
|
+
optionalBoolean(request.query.metadata, "metadata"),
|
|
36
39
|
);
|
|
37
40
|
response.json(payload);
|
|
38
41
|
logger.debug("models response", {
|
|
@@ -130,6 +133,7 @@ export async function sendInference(
|
|
|
130
133
|
function sendError(protocol: ClientProtocol, response: express.Response, error: unknown): void {
|
|
131
134
|
const status =
|
|
132
135
|
error instanceof ModelGatewayError ||
|
|
136
|
+
error instanceof AiDecideRouterError ||
|
|
133
137
|
error instanceof SmartRouterError ||
|
|
134
138
|
error instanceof UnsupportedGatewayFeatureError
|
|
135
139
|
? error.status
|
|
@@ -175,6 +179,18 @@ function optionalSearch(value: unknown): string | undefined {
|
|
|
175
179
|
return value;
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
function optionalModelSource(value: unknown): "foundation" | "custom" | "all" {
|
|
183
|
+
if (value === undefined) return "foundation";
|
|
184
|
+
if (value === "foundation" || value === "custom" || value === "all") return value;
|
|
185
|
+
throw new ModelGatewayError(400, "model_source must be foundation, custom, or all");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function optionalBoolean(value: unknown, name: string): boolean {
|
|
189
|
+
if (value === undefined || value === "false") return false;
|
|
190
|
+
if (value === "true") return true;
|
|
191
|
+
throw new ModelGatewayError(400, `${name} must be true or false`);
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
function requestContext(request: express.Request): Record<string, unknown> {
|
|
179
195
|
return {
|
|
180
196
|
method: request.method,
|
package/src/smart-router.ts
CHANGED
|
@@ -22,10 +22,9 @@ import {
|
|
|
22
22
|
type SmartRouterObservation,
|
|
23
23
|
type SmartRouterRequest,
|
|
24
24
|
} from "@dbx-tools/shared-model-gateway";
|
|
25
|
-
import { Tiktoken } from "js-tiktoken/lite";
|
|
26
|
-
import o200kBase from "js-tiktoken/ranks/o200k_base";
|
|
27
25
|
import { z } from "zod";
|
|
28
26
|
|
|
27
|
+
import { normalizeModelContext, type NormalizedModelContext } from "./model-context.ts";
|
|
29
28
|
import { resolveRoute } from "./router.ts";
|
|
30
29
|
|
|
31
30
|
const logger = log.logger("appkit/model-gateway/smart-router");
|
|
@@ -35,7 +34,6 @@ const PROBE_CACHE_NAMESPACE = "model-gateway-smart-router-probe";
|
|
|
35
34
|
const PROBE_CACHE_VERSION = 1;
|
|
36
35
|
const PROBE_TASK = "Check whether Smart Router can select an available model.";
|
|
37
36
|
const NEGATIVE_PROBE_TTL_MS = 30_000;
|
|
38
|
-
const CONTEXT_ENCODING = new Tiktoken(o200kBase);
|
|
39
37
|
|
|
40
38
|
/** Runtime settings for Smart Router. */
|
|
41
39
|
export interface SmartRouterSettings {
|
|
@@ -109,11 +107,7 @@ export interface SmartRouterResult {
|
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
/** Bounded task context extracted from an arbitrary supported input shape. */
|
|
112
|
-
export
|
|
113
|
-
readonly continuation: boolean;
|
|
114
|
-
readonly logicalTurn: string;
|
|
115
|
-
readonly task: string;
|
|
116
|
-
}
|
|
110
|
+
export type SmartRouterContext = NormalizedModelContext;
|
|
117
111
|
|
|
118
112
|
/** Error carrying an HTTP status for Smart Router validation or availability failures. */
|
|
119
113
|
export class SmartRouterError extends Error {
|
|
@@ -210,7 +204,7 @@ export class SmartRouterService {
|
|
|
210
204
|
candidates: readonly SmartRouterCandidate<T>[],
|
|
211
205
|
options: SmartRouterSelectOptions = {},
|
|
212
206
|
): Promise<SmartRouterSelection<T>> {
|
|
213
|
-
const task = this.context.build(input);
|
|
207
|
+
const task = await this.context.build(input);
|
|
214
208
|
if (!task) throw new SmartRouterError(400, "Smart Router input contains no routable text");
|
|
215
209
|
const client = options.client ?? getExecutionContext().client;
|
|
216
210
|
try {
|
|
@@ -450,18 +444,13 @@ export class SmartRouterContextBuilder {
|
|
|
450
444
|
constructor(private readonly tokenBudget = 4_096) {}
|
|
451
445
|
|
|
452
446
|
/** Return only the bounded task prompt required by routes:select. */
|
|
453
|
-
build(input: unknown): string {
|
|
454
|
-
return this.analyze(input).task;
|
|
447
|
+
async build(input: unknown): Promise<string> {
|
|
448
|
+
return (await this.analyze(input)).task;
|
|
455
449
|
}
|
|
456
450
|
|
|
457
451
|
/** Return bounded task text plus logical-turn metadata used for pinning. */
|
|
458
|
-
analyze(input: unknown): SmartRouterContext {
|
|
459
|
-
|
|
460
|
-
const events = conversationEvents(normalized);
|
|
461
|
-
if (events.length > 0) return contextFromEvents(events, this.tokenBudget);
|
|
462
|
-
const values = collectTaskStrings(normalized);
|
|
463
|
-
const task = truncateText(values.join("\n"), this.tokenBudget, "start");
|
|
464
|
-
return { task, logicalTurn: task, continuation: false };
|
|
452
|
+
analyze(input: unknown): Promise<SmartRouterContext> {
|
|
453
|
+
return normalizeModelContext(input, { tokenLimit: this.tokenBudget });
|
|
465
454
|
}
|
|
466
455
|
}
|
|
467
456
|
|
|
@@ -520,7 +509,7 @@ export class SmartModelRouter {
|
|
|
520
509
|
);
|
|
521
510
|
}
|
|
522
511
|
|
|
523
|
-
const context = this.service.context.analyze(input.body);
|
|
512
|
+
const context = await this.service.context.analyze(input.body);
|
|
524
513
|
if (!context.task) {
|
|
525
514
|
throw new SmartRouterError(400, "Smart Router request contains no task text");
|
|
526
515
|
}
|
|
@@ -670,174 +659,6 @@ function requestedReasoningEffort(
|
|
|
670
659
|
return parsed.data;
|
|
671
660
|
}
|
|
672
661
|
|
|
673
|
-
interface ConversationEvent {
|
|
674
|
-
readonly role: "assistant" | "system" | "tool" | "user";
|
|
675
|
-
readonly text: string;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function contextFromEvents(
|
|
679
|
-
events: readonly ConversationEvent[],
|
|
680
|
-
tokenBudget: number,
|
|
681
|
-
): SmartRouterContext {
|
|
682
|
-
const latestUserIndex = lastEventIndex(events, "user");
|
|
683
|
-
const taskEvent = latestUserIndex >= 0 ? events[latestUserIndex] : events.at(-1);
|
|
684
|
-
const task = truncateText(taskEvent?.text ?? "", tokenBudget, "start");
|
|
685
|
-
let remaining = Math.max(0, tokenBudget - countTokens(task));
|
|
686
|
-
const context: string[] = [];
|
|
687
|
-
for (
|
|
688
|
-
let index = (taskEvent ? events.indexOf(taskEvent) : events.length) - 1;
|
|
689
|
-
index >= 0;
|
|
690
|
-
index -= 1
|
|
691
|
-
) {
|
|
692
|
-
const event = events[index];
|
|
693
|
-
if (!event || !event.text || event.role === "tool") continue;
|
|
694
|
-
const value = `${event.role}: ${event.text}`;
|
|
695
|
-
const retained = truncateText(value, remaining, "end");
|
|
696
|
-
if (!retained) break;
|
|
697
|
-
context.unshift(retained);
|
|
698
|
-
remaining -= countTokens(retained);
|
|
699
|
-
}
|
|
700
|
-
const combined = [...context, task].filter(Boolean).join("\n");
|
|
701
|
-
const logicalEvents = latestUserIndex >= 0 ? events.slice(0, latestUserIndex + 1) : events;
|
|
702
|
-
return {
|
|
703
|
-
task: combined,
|
|
704
|
-
logicalTurn: logicalEvents.map((event) => `${event.role}:${event.text}`).join("\n"),
|
|
705
|
-
continuation:
|
|
706
|
-
latestUserIndex >= 0 &&
|
|
707
|
-
events.slice(latestUserIndex + 1).some((event) => event.role !== "system"),
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function conversationEvents(input: unknown): ConversationEvent[] {
|
|
712
|
-
if (typeof input === "string") return input.trim() ? [{ role: "user", text: input.trim() }] : [];
|
|
713
|
-
if (Array.isArray(input)) return input.flatMap(messageEvent);
|
|
714
|
-
if (!isRecord(input)) return [];
|
|
715
|
-
for (const key of ["messages", "input"] as const) {
|
|
716
|
-
const value = input[key];
|
|
717
|
-
if (typeof value === "string" && value.trim()) return [{ role: "user", text: value.trim() }];
|
|
718
|
-
if (Array.isArray(value)) {
|
|
719
|
-
const events = value.flatMap(messageEvent);
|
|
720
|
-
if (events.length > 0) return events;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
return [];
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function messageEvent(value: unknown): ConversationEvent[] {
|
|
727
|
-
if (typeof value === "string") return value.trim() ? [{ role: "user", text: value.trim() }] : [];
|
|
728
|
-
if (!isRecord(value)) return [];
|
|
729
|
-
const role = messageRole(value);
|
|
730
|
-
if (!role) return [];
|
|
731
|
-
const text = contentText(value.content ?? value.text ?? value.input_text ?? value.output_text);
|
|
732
|
-
return text ? [{ role, text }] : [];
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function messageRole(
|
|
736
|
-
value: Readonly<Record<string, unknown>>,
|
|
737
|
-
): ConversationEvent["role"] | undefined {
|
|
738
|
-
if (value.role === "user" || value.role === "assistant" || value.role === "system") {
|
|
739
|
-
return value.role;
|
|
740
|
-
}
|
|
741
|
-
if (
|
|
742
|
-
value.role === "tool" ||
|
|
743
|
-
value.type === "function_call_output" ||
|
|
744
|
-
value.type === "tool_result"
|
|
745
|
-
) {
|
|
746
|
-
return "tool";
|
|
747
|
-
}
|
|
748
|
-
if (value.type === "function_call" || value.type === "reasoning") return "assistant";
|
|
749
|
-
if (value.type === "message" && value.role === undefined) return "user";
|
|
750
|
-
return undefined;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function contentText(value: unknown): string {
|
|
754
|
-
if (typeof value === "string") return value.trim();
|
|
755
|
-
if (Array.isArray(value)) return value.map(contentText).filter(Boolean).join("\n");
|
|
756
|
-
if (!isRecord(value)) return "";
|
|
757
|
-
for (const key of ["text", "input_text", "output_text", "content"] as const) {
|
|
758
|
-
const text = contentText(value[key]);
|
|
759
|
-
if (text) return text;
|
|
760
|
-
}
|
|
761
|
-
return "";
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const TASK_KEYS = [
|
|
765
|
-
"prompt",
|
|
766
|
-
"task",
|
|
767
|
-
"query",
|
|
768
|
-
"question",
|
|
769
|
-
"request",
|
|
770
|
-
"description",
|
|
771
|
-
"message",
|
|
772
|
-
"text",
|
|
773
|
-
"input",
|
|
774
|
-
] as const;
|
|
775
|
-
const IGNORED_KEYS = new Set([
|
|
776
|
-
"api_key",
|
|
777
|
-
"authorization",
|
|
778
|
-
"arguments",
|
|
779
|
-
"credentials",
|
|
780
|
-
"headers",
|
|
781
|
-
"metadata",
|
|
782
|
-
"password",
|
|
783
|
-
"secret",
|
|
784
|
-
"token",
|
|
785
|
-
"tools",
|
|
786
|
-
]);
|
|
787
|
-
|
|
788
|
-
function collectTaskStrings(input: unknown): string[] {
|
|
789
|
-
if (typeof input === "string") return input.trim() ? [input.trim()] : [];
|
|
790
|
-
const preferred: string[] = [];
|
|
791
|
-
if (isRecord(input)) {
|
|
792
|
-
for (const key of TASK_KEYS) preferred.push(...stringLeaves(input[key], key, 0));
|
|
793
|
-
}
|
|
794
|
-
return preferred.length > 0 ? preferred.slice(0, 32) : stringLeaves(input, "", 0).slice(0, 32);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function stringLeaves(value: unknown, key: string, depth: number): string[] {
|
|
798
|
-
if (depth > 6 || IGNORED_KEYS.has(key.toLowerCase())) return [];
|
|
799
|
-
if (typeof value === "string") return value.trim() ? [value.trim()] : [];
|
|
800
|
-
if (Array.isArray(value)) return value.flatMap((item) => stringLeaves(item, key, depth + 1));
|
|
801
|
-
if (!isRecord(value)) return [];
|
|
802
|
-
return Object.entries(value).flatMap(([childKey, child]) =>
|
|
803
|
-
stringLeaves(child, childKey, depth + 1),
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function parseJsonString(input: unknown): unknown {
|
|
808
|
-
if (typeof input !== "string") return input;
|
|
809
|
-
const trimmed = input.trim();
|
|
810
|
-
if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) return input;
|
|
811
|
-
try {
|
|
812
|
-
return JSON.parse(trimmed) as unknown;
|
|
813
|
-
} catch {
|
|
814
|
-
return input;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function countTokens(text: string): number {
|
|
819
|
-
return CONTEXT_ENCODING.encode(text, [], []).length;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function truncateText(text: string, tokenBudget: number, edge: "start" | "end"): string {
|
|
823
|
-
if (tokenBudget <= 0 || !text) return "";
|
|
824
|
-
const tokens = CONTEXT_ENCODING.encode(text, [], []);
|
|
825
|
-
if (tokens.length <= tokenBudget) return text;
|
|
826
|
-
return CONTEXT_ENCODING.decode(
|
|
827
|
-
edge === "start" ? tokens.slice(0, tokenBudget) : tokens.slice(-tokenBudget),
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function lastEventIndex(
|
|
832
|
-
events: readonly ConversationEvent[],
|
|
833
|
-
role: ConversationEvent["role"],
|
|
834
|
-
): number {
|
|
835
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
836
|
-
if (events[index]?.role === role) return index;
|
|
837
|
-
}
|
|
838
|
-
return -1;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
662
|
function uniqueCandidates<T>(
|
|
842
663
|
candidates: readonly SmartRouterCandidate<T>[],
|
|
843
664
|
): SmartRouterCandidate<T>[] {
|