mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): effort slider matches selected level after model switches (#122125)
* fix(ui): align effort picker after model switches Publish ordered per-model effort profiles from the Gateway and reconcile new-session effort overrides so labels, slider positions, and create payloads stay consistent. * fix(ci): align model profile contracts
This commit is contained in:
committed by
GitHub
parent
32a43960c6
commit
627e83d4f7
@@ -12907,6 +12907,8 @@ public struct ModelChoice: Codable, Sendable {
|
||||
public let available: Bool?
|
||||
public let contextwindow: Int?
|
||||
public let reasoning: Bool?
|
||||
public let thinkinglevels: [[String: AnyCodable]]?
|
||||
public let thinkingdefault: String?
|
||||
public let supportstools: Bool?
|
||||
public let agentruntime: [String: AnyCodable]?
|
||||
public let apikeysupported: Bool?
|
||||
@@ -12920,6 +12922,8 @@ public struct ModelChoice: Codable, Sendable {
|
||||
available: Bool? = nil,
|
||||
contextwindow: Int? = nil,
|
||||
reasoning: Bool? = nil,
|
||||
thinkinglevels: [[String: AnyCodable]]? = nil,
|
||||
thinkingdefault: String? = nil,
|
||||
supportstools: Bool? = nil,
|
||||
agentruntime: [String: AnyCodable]? = nil,
|
||||
apikeysupported: Bool? = nil,
|
||||
@@ -12932,6 +12936,8 @@ public struct ModelChoice: Codable, Sendable {
|
||||
self.available = available
|
||||
self.contextwindow = contextwindow
|
||||
self.reasoning = reasoning
|
||||
self.thinkinglevels = thinkinglevels
|
||||
self.thinkingdefault = thinkingdefault
|
||||
self.supportstools = supportstools
|
||||
self.agentruntime = agentruntime
|
||||
self.apikeysupported = apikeysupported
|
||||
@@ -12946,6 +12952,8 @@ public struct ModelChoice: Codable, Sendable {
|
||||
case available
|
||||
case contextwindow = "contextWindow"
|
||||
case reasoning
|
||||
case thinkinglevels = "thinkingLevels"
|
||||
case thinkingdefault = "thinkingDefault"
|
||||
case supportstools = "supportsTools"
|
||||
case agentruntime = "agentRuntime"
|
||||
case apikeysupported = "apiKeySupported"
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"8c91a4aac45efb318043e133624a8334599c7f0f73c7ba7212dbb661f88c3b17","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
{"contentHash":"8f8189612c59f0a57b65ebd310af811f64953e4b45271c4c62902f3fcfbd8537","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"cea38cb2354e456c9dcb62fcb59d4c5a1850eb7b35cf2c6dc1e6b4052cb239f2","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
{"contentHash":"b8a161ee64194146ccfadc417f27d39911eea97ee818538df86653d1a9f414cc","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"be5e5a878566af7ed7371bcc8bf6501771b7366c019e2e8132b1bd3d598b7b48","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
{"contentHash":"77f828ec162318163cf147fc99143fcafaf35cfd2e2654559fd14bd778ae43d9","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"f4a908c9a10596f9aec9e9ebfda956d87e7d38d7a66b98dd36dca655ab0df554","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
{"contentHash":"1d756f82df06f6ce77d5b4b1e3abfad6046ff49fed7641b3da206b60feb91404","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"3642828e0c335f3412a575e04ac5179c82226386942fcaecaf6ecf229cefb246","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
{"contentHash":"fe0b763f911c6f67d25d63b6fd0defe104bef798008f830ae0f6dbd013a79ff6","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"b976e3217681b57aab8ecab4a94ecf0f8238fe4c68de6c7bb9e2588e9c726622","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
{"contentHash":"d8f4fbf15031d5731596db8f48d15d28c2522e6d563ab0bd7fa41f4908c53314","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"b65bc3c8f77d9ae5fcfe03fe596f2cd948ce04a2544fd5df609c172ec3d4d267","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
{"contentHash":"386c6006608513ffa9c458b1ab085c5b3ca3df953b8cd09cdc17846b8f616aa6","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"e17ce7a27cdf887381927cc2e39300105c26f72fccd34da5fead687237f022be","entrypoint":"command-auth-native","importSpecifier":"openclaw/plugin-sdk/command-auth-native"}
|
||||
{"contentHash":"2c86a1a976f82d74069801758624315dc46020a418f06d565f6f6d0a74bb7cd1","entrypoint":"command-auth-native","importSpecifier":"openclaw/plugin-sdk/command-auth-native"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"d22bf2e5ae14ee5ec01ea825c2bc16c908a15f78dd462c3e160393aa871d7cf5","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
{"contentHash":"a501e9c9f95cda7027f83d52c7b4b95153531cc24540f0e6803435e75def6153","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"66c63de1253ea9f27ada35301d7f9a53504483c10ca5dd281fe50b645b5bccd2","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
{"contentHash":"ceca2545902bcea34a836763afef76c9b55bd0fa639cceb89b7122c0f7c6027a","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"e6d85aba0cc65a4acbfb40ebcb26efda2cc7510f646141180d50cec22c07f1c0","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
|
||||
{"contentHash":"d83a8d16c27fdc15ee35827799d7a245b2b46e6a5b8b98b1a1b623d466a1dbd4","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"c1226a9266ddddfd3cb3ec3842a3eb43d7b05738ff495e18b9a13654d8bbc3e7","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
{"contentHash":"3e853390cc6efef245c9c220512fca49d2f8812abf1968e302c48e48b223d633","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"538b128a5de8b38c9c3e13cdbb74d0eaa0e6ce7a8066c5852204b77b37536734","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
{"contentHash":"35904f50b6b6826386c774052cf3ab38a07a5ccabbd264237c1ce0bd5f1275c8","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"9e50d560be0fae99edf2ab5400625033c3515d00b16113b085543d6f218c17cd","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
{"contentHash":"3e7a63e4f1d24dd6a2af9b7c5fd47837781471f5d92cdc1ba15f079259617402","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"9748f103ca50929f6a67e77c6708c0fb1a24e5d83ec1ae9e15485a80fdc20398","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
{"contentHash":"7f5d4e9c2f0593f433cb06c9acbca4c7a683e7d097d8f11304450076a7deb6e7","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"4c5ae6de0b18a8ad16c4c6f8437bb18c0d2e8f511452ac47ef1442136c7f90a9","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
{"contentHash":"43d1e66a594b093ebd63d48de2a17900eef743404ef10068735a2aa8f2900f9f","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"a204b23b49393b23755ed3f6d5f44c14248abba916063c6a4f363d39a019c800","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
{"contentHash":"cc7dd850ee1af91876e83827b5087c1374328f8eb7b9d559c5df7e34695663e2","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"ed3e64c4f91e222c62209dbb7ae7632ff3ea85ae5658264b559d36997f037b7b","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
{"contentHash":"6c157ca687f5bf384fcf526ca2465f85b27e3bedba0bde3c8ecf1e4060ddcd5d","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
|
||||
@@ -219,6 +219,11 @@ describe("ModelsListResultSchema", () => {
|
||||
name: "GPT Image",
|
||||
provider: "openai",
|
||||
agentRuntime: { id: "codex", fallback: "openclaw", source: "model" },
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "Off" },
|
||||
{ id: "xhigh", label: "Extra high" },
|
||||
],
|
||||
thinkingDefault: "xhigh",
|
||||
input: ["text", "image", "audio", "video", "document"],
|
||||
};
|
||||
|
||||
@@ -241,6 +246,7 @@ describe("ModelsListResultSchema", () => {
|
||||
{
|
||||
models: [{ ...model, agentRuntime: { id: "codex", source: "unknown" } }],
|
||||
},
|
||||
{ models: [{ ...model, thinkingLevels: [{ id: "", label: "Off" }] }] },
|
||||
{ models: [{ ...model, input: ["text", "binary"] }] },
|
||||
{ models: [], providerOutcomes: [{ provider: "openai", status: "unknown" }] },
|
||||
{
|
||||
|
||||
@@ -29,6 +29,11 @@ const GatewayAgentRuntimeSchema = closedObject({
|
||||
]),
|
||||
});
|
||||
|
||||
const GatewayThinkingLevelOptionSchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
label: NonEmptyString,
|
||||
});
|
||||
|
||||
export const ModelChoiceSchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
name: NonEmptyString,
|
||||
@@ -37,6 +42,8 @@ export const ModelChoiceSchema = closedObject({
|
||||
available: Type.Optional(Type.Boolean()),
|
||||
contextWindow: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinkingLevels: Type.Optional(Type.Array(GatewayThinkingLevelOptionSchema)),
|
||||
thinkingDefault: Type.Optional(NonEmptyString),
|
||||
supportsTools: Type.Optional(Type.Boolean()),
|
||||
agentRuntime: Type.Optional(GatewayAgentRuntimeSchema),
|
||||
apiKeySupported: Type.Optional(Type.Boolean()),
|
||||
@@ -79,14 +86,7 @@ export const AgentSummarySchema = closedObject({
|
||||
}),
|
||||
),
|
||||
agentRuntime: Type.Optional(GatewayAgentRuntimeSchema),
|
||||
thinkingLevels: Type.Optional(
|
||||
Type.Array(
|
||||
closedObject({
|
||||
id: NonEmptyString,
|
||||
label: NonEmptyString,
|
||||
}),
|
||||
),
|
||||
),
|
||||
thinkingLevels: Type.Optional(Type.Array(GatewayThinkingLevelOptionSchema)),
|
||||
thinkingOptions: Type.Optional(Type.Array(NonEmptyString)),
|
||||
thinkingDefault: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
@@ -49,6 +49,8 @@ export function resolveEffectiveAgentRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
modelApi?: string | null;
|
||||
modelBaseUrl?: unknown;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
sessionEntry?: Pick<SessionEntry, "agentHarnessId" | "agentRuntimeOverride">;
|
||||
@@ -63,6 +65,8 @@ export function resolveEffectiveAgentRuntime(params: {
|
||||
resolveAgentHarnessPolicy({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
modelApi: params.modelApi,
|
||||
modelBaseUrl: params.modelBaseUrl,
|
||||
config: params.cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
|
||||
@@ -571,7 +571,7 @@ describe("models.list OpenAI routes", () => {
|
||||
await withEnvAsync({ ...WITHOUT_OPENAI_ENV_AUTH, OPENAI_API_KEY: "test-key" }, async () => {
|
||||
await expect(listModels({ catalog: [row], cfg })).resolves.toEqual({
|
||||
models: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.4-nano",
|
||||
name: "GPT-5.4 Nano",
|
||||
provider: "openai",
|
||||
@@ -579,7 +579,7 @@ describe("models.list OpenAI routes", () => {
|
||||
contextWindow: 1_000_000,
|
||||
reasoning: true,
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -683,13 +683,13 @@ describe("models.list OpenAI routes", () => {
|
||||
|
||||
await expect(listModels({ catalog: [row], cfg })).resolves.toEqual({
|
||||
models: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.5",
|
||||
name: "gpt-5.5",
|
||||
provider: "openai",
|
||||
agentRuntime: IMPLICIT_CODEX_RUNTIME,
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -704,7 +704,7 @@ describe("models.list OpenAI routes", () => {
|
||||
} as ModelCatalogEntry;
|
||||
const subscriptionProjection = {
|
||||
models: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.5",
|
||||
name: "gpt-5.5",
|
||||
provider: "openai",
|
||||
@@ -712,7 +712,7 @@ describe("models.list OpenAI routes", () => {
|
||||
contextWindow: 400_000,
|
||||
reasoning: true,
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
await expect(listModels({ catalog: [row, chatGPTRow], cfg })).resolves.toEqual(
|
||||
@@ -743,7 +743,7 @@ describe("models.list OpenAI routes", () => {
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
models: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
provider: "openai",
|
||||
@@ -752,7 +752,7 @@ describe("models.list OpenAI routes", () => {
|
||||
reasoning: true,
|
||||
input: ["text", "video"],
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -765,7 +765,7 @@ describe("models.list OpenAI routes", () => {
|
||||
} as unknown as OpenClawConfig;
|
||||
await expect(listModels({ catalog: [row], cfg: apiKeyFirst })).resolves.toEqual({
|
||||
models: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.5",
|
||||
name: "gpt-5.5",
|
||||
provider: "openai",
|
||||
@@ -773,7 +773,7 @@ describe("models.list OpenAI routes", () => {
|
||||
contextWindow: 1_000_000,
|
||||
reasoning: true,
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// strips runtime-only provider params before sending the browse API payload.
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { asPositiveSafeInteger as resolvePositiveSafeInteger } from "@openclaw/normalization-core/number-coercion";
|
||||
import type { ModelChoice } from "../../../packages/gateway-protocol/src/schema/agents-models-skills.js";
|
||||
import type { PreparedAgentCredentialModes } from "../../agents/agent-auth-credentials.js";
|
||||
import {
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
@@ -55,18 +56,15 @@ import { resolveManifestProviderAuthChoices } from "../../plugins/provider-auth-
|
||||
import type { ProviderCatalogOutcome } from "../../plugins/provider-catalog.types.js";
|
||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||
import type { GatewayAgentRuntime } from "../../shared/session-types.js";
|
||||
import { resolveGatewayModelThinkingProfile } from "../session-utils-model.js";
|
||||
import { createModelsListAuthResolver } from "./models-list-auth-resolver.js";
|
||||
import type { GatewayRequestContext } from "./types.js";
|
||||
|
||||
type ModelsListView = ModelCatalogBrowseView;
|
||||
type ModelsListEntry = Pick<
|
||||
ModelCatalogEntry,
|
||||
ModelChoice,
|
||||
"alias" | "contextWindow" | "id" | "input" | "name" | "provider" | "reasoning"
|
||||
> & { available?: boolean; supportsTools?: boolean };
|
||||
type ModelsListEntryWithCapabilities = ModelsListEntry & {
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
apiKeySupported?: boolean;
|
||||
};
|
||||
type ModelsListEntryWithCapabilities = ModelChoice;
|
||||
type ApiKeyProviderCapabilities = {
|
||||
providers: ReadonlyMap<string, boolean>;
|
||||
resolveProvider(provider: string): string;
|
||||
@@ -82,7 +80,7 @@ let loggedSlowModelsListCatalog = false;
|
||||
|
||||
// Unknown views are rejected by protocol validation first; this helper keeps the
|
||||
// handler default explicit for older clients that omit the field.
|
||||
function resolveModelsListView(params: Record<string, unknown>): ModelsListView {
|
||||
function resolveModelsListView(params: Record<string, unknown>): ModelCatalogBrowseView {
|
||||
const view = params.view;
|
||||
return view === "configured" || view === "provider-config" || view === "all" ? view : "default";
|
||||
}
|
||||
@@ -425,10 +423,9 @@ async function buildPublicModelsListEntries(params: {
|
||||
preserveUnknownAvailability?: boolean;
|
||||
apiKeyCapabilities?: ApiKeyProviderCapabilities;
|
||||
}): Promise<ModelsListEntryWithCapabilities[]> {
|
||||
return await Promise.all(
|
||||
return Promise.all(
|
||||
params.catalog.map(async (entry): Promise<ModelsListEntryWithCapabilities> => {
|
||||
const evaluation = await params.evaluateEntry(entry);
|
||||
const publicEntry = buildPublicModelProjection(entry);
|
||||
const syntheticLocalAvailable =
|
||||
evaluation.availability === undefined &&
|
||||
evaluation.routeResolution === null &&
|
||||
@@ -443,9 +440,23 @@ async function buildPublicModelsListEntries(params: {
|
||||
agentId: params.agentId,
|
||||
entry,
|
||||
});
|
||||
const thinkingProfile =
|
||||
typeof entry.reasoning === "boolean"
|
||||
? resolveGatewayModelThinkingProfile({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
provider: entry.provider,
|
||||
model: entry.id,
|
||||
modelCatalog: params.catalog,
|
||||
})
|
||||
: undefined;
|
||||
return {
|
||||
...publicEntry,
|
||||
...buildPublicModelProjection(entry),
|
||||
...(agentRuntime ? { agentRuntime } : {}),
|
||||
...(thinkingProfile && {
|
||||
thinkingLevels: thinkingProfile.levels,
|
||||
thinkingDefault: thinkingProfile.defaultLevel,
|
||||
}),
|
||||
...(capabilityProvider && params.apiKeyCapabilities?.providers.has(capabilityProvider)
|
||||
? {
|
||||
apiKeySupported: params.apiKeyCapabilities.providers.get(capabilityProvider) === true,
|
||||
|
||||
@@ -320,6 +320,14 @@ describe("models.list", () => {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
available: true,
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "minimal", label: "minimal" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "medium", label: "medium" },
|
||||
{ id: "high", label: "high" },
|
||||
],
|
||||
thinkingDefault: "medium",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1585,6 +1593,47 @@ describe("models.list", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("projects ordered thinking profiles without exposing raw compatibility metadata", async () => {
|
||||
const { request, respond } = requestModelsList({
|
||||
view: "all",
|
||||
loadGatewayModelCatalog: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
id: "reasoning-model",
|
||||
name: "Reasoning Model",
|
||||
provider: "demo-provider",
|
||||
reasoning: true,
|
||||
compat: {
|
||||
supportedReasoningEfforts: ["max", "xhigh"],
|
||||
privateRouteHint: "do-not-publish",
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
reqId: "req-models-list-thinking-profile",
|
||||
});
|
||||
await request;
|
||||
|
||||
const payload = respond.mock.calls[0]?.[1] as { models: Array<Record<string, unknown>> };
|
||||
expect(payload.models).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "reasoning-model",
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "minimal", label: "minimal" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "medium", label: "medium" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "xhigh", label: "xhigh" },
|
||||
{ id: "max", label: "max" },
|
||||
{ id: "ultra", label: "ultra" },
|
||||
],
|
||||
thinkingDefault: "medium",
|
||||
}),
|
||||
]);
|
||||
expect(payload.models[0]).not.toHaveProperty("compat");
|
||||
});
|
||||
|
||||
it("does not reinterpret context tokens or expose model input metadata", async () => {
|
||||
const { request, respond } = requestModelsList({
|
||||
view: "all",
|
||||
|
||||
@@ -1452,7 +1452,7 @@ describe("gateway server chat", () => {
|
||||
}
|
||||
| undefined;
|
||||
expect(payload?.metadata?.models).toEqual([
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
provider: "openai",
|
||||
@@ -1460,7 +1460,7 @@ describe("gateway server chat", () => {
|
||||
contextWindow: 400_000,
|
||||
reasoning: false,
|
||||
available: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(payload?.sessionInfo?.thinkingLevels?.map((level) => level.id)).toEqual(["off"]);
|
||||
expect(payload?.defaults?.thinkingLevels?.map((level) => level.id)).toEqual(["off"]);
|
||||
|
||||
@@ -76,7 +76,7 @@ function resolveGatewaySessionThinkingLevel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveGatewaySessionThinkingDefault(params: {
|
||||
function resolveGatewaySessionThinkingDefault(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
model: string;
|
||||
@@ -105,25 +105,43 @@ export function resolveGatewaySessionThinkingDefault(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSessionRowThinkingMetadata(params: {
|
||||
export function resolveGatewayModelThinkingProfile(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
agentRuntime: string;
|
||||
agentRuntime?: string;
|
||||
modelCatalog?: ModelCatalogEntry[];
|
||||
rowContext?: SessionListRowContext;
|
||||
sessionKey?: string;
|
||||
}): {
|
||||
levels: ReturnType<typeof listThinkingLevelOptions>;
|
||||
defaultLevel: ReturnType<typeof resolveGatewaySessionThinkingDefault>;
|
||||
} {
|
||||
const catalogEntry = params.modelCatalog
|
||||
? findModelCatalogEntry(params.modelCatalog, {
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
})
|
||||
: undefined;
|
||||
const agentRuntime =
|
||||
params.agentRuntime ??
|
||||
resolveEffectiveAgentRuntime({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
modelApi: catalogEntry?.api,
|
||||
modelBaseUrl: catalogEntry?.baseUrl,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
if (!params.rowContext) {
|
||||
return {
|
||||
levels: listThinkingLevelOptions(
|
||||
params.provider,
|
||||
params.model,
|
||||
params.modelCatalog,
|
||||
params.agentRuntime,
|
||||
agentRuntime,
|
||||
),
|
||||
defaultLevel: resolveGatewaySessionThinkingDefault({
|
||||
cfg: params.cfg,
|
||||
@@ -131,11 +149,11 @@ function resolveSessionRowThinkingMetadata(params: {
|
||||
model: params.model,
|
||||
agentId: params.agentId,
|
||||
modelCatalog: params.modelCatalog,
|
||||
agentRuntime: params.agentRuntime,
|
||||
agentRuntime,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const key = `${normalizeAgentId(params.agentId)}\0${params.agentRuntime}\0${createSessionRowModelCacheKey(
|
||||
const key = `${normalizeAgentId(params.agentId)}\0${agentRuntime}\0${createSessionRowModelCacheKey(
|
||||
params.provider,
|
||||
params.model,
|
||||
)}`;
|
||||
@@ -148,7 +166,7 @@ function resolveSessionRowThinkingMetadata(params: {
|
||||
params.provider,
|
||||
params.model,
|
||||
params.modelCatalog,
|
||||
params.agentRuntime,
|
||||
agentRuntime,
|
||||
),
|
||||
defaultLevel: resolveGatewaySessionThinkingDefault({
|
||||
cfg: params.cfg,
|
||||
@@ -156,7 +174,7 @@ function resolveSessionRowThinkingMetadata(params: {
|
||||
model: params.model,
|
||||
agentId: params.agentId,
|
||||
modelCatalog: params.modelCatalog,
|
||||
agentRuntime: params.agentRuntime,
|
||||
agentRuntime,
|
||||
}),
|
||||
};
|
||||
params.rowContext.thinkingMetadataByModelRef.set(key, metadata);
|
||||
@@ -206,17 +224,25 @@ export function resolveGatewaySessionThinkingProjectionInternal(
|
||||
id: persistedAgentRuntime,
|
||||
source: persistedAgentRuntimeSource,
|
||||
};
|
||||
const catalogEntry = params.modelCatalog
|
||||
? findModelCatalogEntry(params.modelCatalog, {
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
})
|
||||
: undefined;
|
||||
const thinkingRuntime = acpMeta
|
||||
? concretizeAgentRuntime(acpMeta.backend ?? agentRuntime.id)
|
||||
: resolveEffectiveAgentRuntime({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
modelApi: catalogEntry?.api,
|
||||
modelBaseUrl: catalogEntry?.baseUrl,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionEntry: params.entry,
|
||||
});
|
||||
const metadata = resolveSessionRowThinkingMetadata({
|
||||
const metadata = resolveGatewayModelThinkingProfile({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
provider: params.provider,
|
||||
@@ -270,33 +296,22 @@ export function getSessionDefaults(
|
||||
sessionKey,
|
||||
acpRuntime: false,
|
||||
});
|
||||
const thinkingRuntime = resolveEffectiveAgentRuntime({
|
||||
const thinkingProfile = resolveGatewayModelThinkingProfile({
|
||||
cfg,
|
||||
provider: resolved.provider,
|
||||
modelId: resolved.model,
|
||||
model: resolved.model,
|
||||
agentId,
|
||||
modelCatalog,
|
||||
sessionKey,
|
||||
});
|
||||
const thinkingLevels = listThinkingLevelOptions(
|
||||
resolved.provider,
|
||||
resolved.model,
|
||||
modelCatalog,
|
||||
thinkingRuntime,
|
||||
);
|
||||
return {
|
||||
modelProvider: resolved.provider ?? null,
|
||||
model: resolved.model ?? null,
|
||||
contextTokens: contextTokens ?? null,
|
||||
agentRuntime,
|
||||
thinkingLevels,
|
||||
thinkingOptions: thinkingLevels.map((level) => level.label),
|
||||
thinkingDefault: resolveGatewaySessionThinkingDefault({
|
||||
cfg,
|
||||
provider: resolved.provider,
|
||||
model: resolved.model,
|
||||
modelCatalog,
|
||||
agentRuntime: thinkingRuntime,
|
||||
}),
|
||||
thinkingLevels: thinkingProfile.levels,
|
||||
thinkingOptions: thinkingProfile.levels.map((level) => level.label),
|
||||
thinkingDefault: thinkingProfile.defaultLevel,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ import { resolveAgentAvatarUrlFromSource } from "../agents/identity-avatar-file.
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import { splitTrailingAuthProfile } from "../agents/model-ref-profile.js";
|
||||
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||
import { resolveEffectiveAgentRuntime } from "../agents/thinking-runtime.js";
|
||||
import { insideGitCheckout } from "../agents/worktrees/git.js";
|
||||
import { listThinkingLevelOptions } from "../auto-reply/thinking.js";
|
||||
import { getRuntimeConfig } from "../config/io.js";
|
||||
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
|
||||
import {
|
||||
@@ -34,7 +32,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { isAcpSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { listGatewayAgentsBasic } from "./agent-list.js";
|
||||
import { resolveGatewaySessionThinkingDefault } from "./session-utils-model.js";
|
||||
import { resolveGatewayModelThinkingProfile } from "./session-utils-model.js";
|
||||
import {
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveGatewaySessionStoreTargetWithStore,
|
||||
@@ -344,20 +342,15 @@ export function listAgentsForGateway(
|
||||
sessionKey,
|
||||
acpRuntime: false,
|
||||
});
|
||||
const thinkingRuntime = resolveEffectiveAgentRuntime({
|
||||
const agentModelCatalog = options?.modelCatalogByAgentId?.get(id) ?? modelCatalog;
|
||||
const thinkingProfile = resolveGatewayModelThinkingProfile({
|
||||
cfg,
|
||||
provider: resolvedModel.provider,
|
||||
modelId: resolvedModel.model,
|
||||
agentId: id,
|
||||
provider: resolvedModel.provider,
|
||||
model: resolvedModel.model,
|
||||
modelCatalog: agentModelCatalog,
|
||||
sessionKey,
|
||||
});
|
||||
const agentModelCatalog = options?.modelCatalogByAgentId?.get(id) ?? modelCatalog;
|
||||
const thinkingLevels = listThinkingLevelOptions(
|
||||
resolvedModel.provider,
|
||||
resolvedModel.model,
|
||||
agentModelCatalog,
|
||||
thinkingRuntime,
|
||||
);
|
||||
const workspace = resolveAgentWorkspaceDir(cfg, id);
|
||||
// Must mirror the sessions.create worktree preflight: subdirectory workspaces inside a
|
||||
// repo are worktree-capable, so the UI toggle and the create path cannot diverge.
|
||||
@@ -371,16 +364,9 @@ export function listAgentsForGateway(
|
||||
workspace,
|
||||
workspaceGit,
|
||||
agentRuntime,
|
||||
thinkingLevels,
|
||||
thinkingOptions: thinkingLevels.map((level) => level.label),
|
||||
thinkingDefault: resolveGatewaySessionThinkingDefault({
|
||||
cfg,
|
||||
provider: resolvedModel.provider,
|
||||
model: resolvedModel.model,
|
||||
agentId: id,
|
||||
modelCatalog: agentModelCatalog,
|
||||
agentRuntime: thinkingRuntime,
|
||||
}),
|
||||
thinkingLevels: thinkingProfile.levels,
|
||||
thinkingOptions: thinkingProfile.levels.map((level) => level.label),
|
||||
thinkingDefault: thinkingProfile.defaultLevel,
|
||||
},
|
||||
model ? { model } : {},
|
||||
);
|
||||
|
||||
@@ -815,6 +815,8 @@ export type ModelCatalogEntry = {
|
||||
available?: boolean;
|
||||
contextWindow?: number;
|
||||
reasoning?: boolean;
|
||||
thinkingLevels?: GatewayThinkingLevelOption[];
|
||||
thinkingDefault?: string;
|
||||
supportsTools?: boolean;
|
||||
agentRuntime?: import("../../../packages/gateway-protocol/src/schema.js").GatewayAgentRuntime;
|
||||
input?: Array<"text" | "image" | "document">;
|
||||
|
||||
@@ -230,6 +230,100 @@ suite.define(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the effort label, slider stop, and create payload aligned after a model switch", async () => {
|
||||
await withNewSessionPage(DESKTOP_CONTEXT, async (page) => {
|
||||
const levels = (ids: string[]) => ids.map((id) => ({ id, label: id }));
|
||||
const kimiLevels = levels([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
"ultra",
|
||||
]);
|
||||
const gateway = await installMockGateway(page, {
|
||||
agentModel: "kimi/k3",
|
||||
models: [
|
||||
{
|
||||
id: "k3",
|
||||
name: "Kimi K3",
|
||||
provider: "kimi",
|
||||
reasoning: true,
|
||||
thinkingLevels: kimiLevels,
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT 5.6 Sol",
|
||||
provider: "openai",
|
||||
reasoning: true,
|
||||
thinkingLevels: levels(["off", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
||||
thinkingDefault: "medium",
|
||||
},
|
||||
],
|
||||
methodResponses: {
|
||||
"agents.list": {
|
||||
...mainAgentList(),
|
||||
agents: [
|
||||
{
|
||||
id: "main",
|
||||
name: "Main",
|
||||
identity: { name: "Main" },
|
||||
model: { primary: "kimi/k3" },
|
||||
thinkingLevels: kimiLevels,
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
],
|
||||
},
|
||||
"sessions.create": { key: "agent:main:thinking-model-switch", runStarted: true },
|
||||
},
|
||||
});
|
||||
await page.goto(`${suite.server.baseUrl}new`);
|
||||
|
||||
const effortSelect = page.locator('[data-chat-thinking-select="true"]');
|
||||
await effortSelect.click();
|
||||
const thinkingSlider = page.locator('[data-chat-thinking-slider="true"]');
|
||||
await thinkingSlider.evaluate((element) => {
|
||||
const input = element as HTMLInputElement;
|
||||
input.value = "5";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await expect.poll(() => effortSelect.getAttribute("data-chat-thinking-value")).toBe("xhigh");
|
||||
|
||||
const modelSelect = page.locator('[data-chat-model-select="true"]');
|
||||
await modelSelect.click();
|
||||
await page.locator('[data-chat-model-option="openai/gpt-5.6-sol"]').click();
|
||||
await effortSelect.click();
|
||||
|
||||
await expect
|
||||
.poll(() => thinkingSlider.getAttribute("data-chat-thinking-values"))
|
||||
.toBe("off,minimal,low,medium,high,xhigh,max");
|
||||
await expect.poll(() => thinkingSlider.inputValue()).toBe("5");
|
||||
expect(await thinkingSlider.getAttribute("max")).toBe("6");
|
||||
expect(await thinkingSlider.getAttribute("aria-valuetext")).toBe("Extra high");
|
||||
expect(
|
||||
Number.parseFloat(
|
||||
await thinkingSlider.evaluate((element) =>
|
||||
(element as HTMLElement).style.getPropertyValue("--reasoning-fill"),
|
||||
),
|
||||
),
|
||||
).toBeCloseTo(83.33, 1);
|
||||
|
||||
await effortSelect.click();
|
||||
await page.locator(".new-session-page__message").fill("keep the selected effort");
|
||||
await page.getByRole("button", { name: "Start session" }).click();
|
||||
const create = await gateway.waitForRequest("sessions.create");
|
||||
expect(create.params).toMatchObject({
|
||||
message: "keep the selected effort",
|
||||
model: "openai/gpt-5.6-sol",
|
||||
thinkingLevel: "xhigh",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("restores valid preferences and repairs a worktree rejected by workspace metadata", async () => {
|
||||
await withNewSessionPage(DESKTOP_CONTEXT, async (page) => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
|
||||
@@ -26,7 +26,7 @@ type ChatModelSelectStateInput = {
|
||||
sessionsResult: SessionsListResult | null;
|
||||
};
|
||||
|
||||
export type ChatModelSelectOption = {
|
||||
type ChatModelSelectOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,12 @@ describe("chat thinking helpers", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.currentOverride).toBe("ultra");
|
||||
expect(state.selection).toEqual({
|
||||
kind: "unanchored",
|
||||
source: "override",
|
||||
value: "ultra",
|
||||
displayLabel: "Ultra",
|
||||
});
|
||||
expect(state.options.map((option) => option.value)).toEqual(["max"]);
|
||||
});
|
||||
|
||||
@@ -102,14 +107,14 @@ describe("chat thinking helpers", () => {
|
||||
thinkingDefault: "medium",
|
||||
};
|
||||
const inherited = resolveChatThinkingSelectState({
|
||||
catalog: [{ provider: "openai", id: "gpt-5.6-sol", reasoning: true }],
|
||||
catalog: [{ provider: "openai", id: "gpt-5.6-sol", name: "GPT-5.6 Sol", reasoning: true }],
|
||||
defaults,
|
||||
sessionKey: "new-session:main",
|
||||
session: { key: "new-session:main", kind: "direct", updatedAt: null },
|
||||
sessionsResult: null,
|
||||
});
|
||||
const explicit = resolveChatThinkingSelectState({
|
||||
catalog: [{ provider: "openai", id: "gpt-5.6-sol", reasoning: true }],
|
||||
catalog: [{ provider: "openai", id: "gpt-5.6-sol", name: "GPT-5.6 Sol", reasoning: true }],
|
||||
defaults,
|
||||
sessionKey: "new-session:main",
|
||||
session: {
|
||||
|
||||
+55
-31
@@ -2,7 +2,6 @@ import {
|
||||
BASE_THINKING_LEVELS,
|
||||
normalizeThinkLevel,
|
||||
resolveThinkingDefaultForModelCore,
|
||||
type ThinkingCatalogEntry,
|
||||
} from "../../../../src/auto-reply/thinking.shared.js";
|
||||
// Control UI module implements thinking behavior.
|
||||
import type {
|
||||
@@ -15,21 +14,26 @@ import { pushUniqueTrimmedSelectOption } from "../select-options.ts";
|
||||
import { sessionModelMatchesDefaults } from "../session-model-defaults.ts";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
|
||||
|
||||
function listThinkingLevelLabels(
|
||||
provider?: string | null,
|
||||
model?: string | null,
|
||||
): readonly string[] {
|
||||
void provider;
|
||||
void model;
|
||||
return BASE_THINKING_LEVELS;
|
||||
}
|
||||
|
||||
type ThinkingSessionDefaults = SessionsListResult["defaults"] | undefined;
|
||||
|
||||
type ChatThinkingSelectState = {
|
||||
currentOverride: string;
|
||||
defaultLabel: string;
|
||||
defaultValue: string;
|
||||
type ChatThinkingSelection =
|
||||
| {
|
||||
kind: "anchored";
|
||||
source: "override" | "default";
|
||||
value: string;
|
||||
displayLabel: string;
|
||||
index: number;
|
||||
}
|
||||
| {
|
||||
kind: "unanchored";
|
||||
source: "override" | "default";
|
||||
value: string;
|
||||
displayLabel: string;
|
||||
};
|
||||
|
||||
export type ChatThinkingSelectState = {
|
||||
selection: ChatThinkingSelection;
|
||||
inherited: { value: string; displayLabel: string };
|
||||
options: Array<{ value: string; label: string }>;
|
||||
};
|
||||
|
||||
@@ -147,8 +151,18 @@ function resolveThinkingTargetModel(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveThinkingCatalogEntry(
|
||||
catalog: readonly ModelCatalogEntry[],
|
||||
provider: string | null,
|
||||
model: string | null,
|
||||
): ModelCatalogEntry | undefined {
|
||||
return provider && model
|
||||
? catalog.find((entry) => entry.provider === provider && entry.id === model)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveThinkingLevelOptions(params: {
|
||||
catalog: readonly ThinkingCatalogEntry[];
|
||||
catalog: readonly ModelCatalogEntry[];
|
||||
defaults: ThinkingSessionDefaults;
|
||||
hideUnsupportedOffOnly?: boolean;
|
||||
model: string | null;
|
||||
@@ -156,14 +170,12 @@ function resolveThinkingLevelOptions(params: {
|
||||
session: GatewaySessionRow | undefined;
|
||||
}): GatewayThinkingLevelOption[] {
|
||||
const modelMatchesDefaults = sessionModelMatchesDefaults(params.session, params.defaults);
|
||||
const catalogEntry =
|
||||
params.provider && params.model
|
||||
? params.catalog.find(
|
||||
(entry) => entry.provider === params.provider && entry.id === params.model,
|
||||
)
|
||||
: undefined;
|
||||
const catalogEntry = resolveThinkingCatalogEntry(params.catalog, params.provider, params.model);
|
||||
const explicitLevels =
|
||||
(params.session?.thinkingLevels?.length ? params.session.thinkingLevels : null) ??
|
||||
(params.session?.model && catalogEntry?.thinkingLevels?.length
|
||||
? catalogEntry.thinkingLevels
|
||||
: null) ??
|
||||
(modelMatchesDefaults && params.defaults?.thinkingLevels?.length
|
||||
? params.defaults.thinkingLevels
|
||||
: null);
|
||||
@@ -187,11 +199,7 @@ function resolveThinkingLevelOptions(params: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const labels =
|
||||
explicitLabels ??
|
||||
(params.provider && params.model
|
||||
? listThinkingLevelLabels(params.provider, params.model)
|
||||
: listThinkingLevelLabels());
|
||||
const labels = explicitLabels ?? BASE_THINKING_LEVELS;
|
||||
return labels.map((label) => ({
|
||||
id: normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label),
|
||||
label,
|
||||
@@ -199,7 +207,7 @@ function resolveThinkingLevelOptions(params: {
|
||||
}
|
||||
|
||||
export function resolveChatThinkingSelectState(params: {
|
||||
catalog: readonly ThinkingCatalogEntry[];
|
||||
catalog: readonly ModelCatalogEntry[];
|
||||
defaults?: SessionsListResult["defaults"];
|
||||
session?: GatewaySessionRow;
|
||||
sessionKey: string;
|
||||
@@ -214,6 +222,7 @@ export function resolveChatThinkingSelectState(params: {
|
||||
: "";
|
||||
const defaults = params.defaults ?? params.sessionsResult?.defaults;
|
||||
const { provider, model } = resolveThinkingTargetModel({ defaults, session });
|
||||
const catalogEntry = resolveThinkingCatalogEntry(params.catalog, provider, model);
|
||||
const levels = resolveThinkingLevelOptions({
|
||||
catalog: params.catalog,
|
||||
defaults,
|
||||
@@ -228,6 +237,7 @@ export function resolveChatThinkingSelectState(params: {
|
||||
: undefined;
|
||||
const defaultLevel =
|
||||
session?.thinkingDefault ??
|
||||
(session?.model ? catalogEntry?.thinkingDefault : undefined) ??
|
||||
defaultFromSessionDefaults ??
|
||||
(provider && model
|
||||
? resolveThinkingDefaultForModelCore({
|
||||
@@ -237,11 +247,25 @@ export function resolveChatThinkingSelectState(params: {
|
||||
})
|
||||
: "off");
|
||||
const effectiveOverride = levels.length === 0 && currentOverride === "off" ? "" : currentOverride;
|
||||
const options = buildThinkingOptions(levels);
|
||||
const defaultValue = normalizeThinkingOptionValue(defaultLevel);
|
||||
const inherited = {
|
||||
value: defaultValue,
|
||||
displayLabel: formatInheritedThinkingLabel(defaultLevel),
|
||||
};
|
||||
const selectionValue = effectiveOverride || defaultValue;
|
||||
const selectionIndex = options.findIndex((option) => option.value === selectionValue);
|
||||
const source = effectiveOverride ? "override" : "default";
|
||||
const displayLabel = effectiveOverride
|
||||
? (options[selectionIndex]?.label ?? formatThinkingOverrideLabel(effectiveOverride))
|
||||
: inherited.displayLabel;
|
||||
return {
|
||||
currentOverride: effectiveOverride,
|
||||
defaultLabel: formatInheritedThinkingLabel(defaultLevel),
|
||||
defaultValue: normalizeThinkingOptionValue(defaultLevel),
|
||||
options: buildThinkingOptions(levels),
|
||||
selection:
|
||||
selectionIndex >= 0
|
||||
? { kind: "anchored", source, value: selectionValue, displayLabel, index: selectionIndex }
|
||||
: { kind: "unanchored", source, value: selectionValue, displayLabel },
|
||||
inherited,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,17 @@ import { t } from "../../../i18n/index.ts";
|
||||
import type {
|
||||
ChatFastModeSelectState,
|
||||
ChatFastModeSelectValue,
|
||||
ChatModelSelectOption,
|
||||
} from "../../../lib/chat/model-select-state.ts";
|
||||
import { formatThinkingOverrideLabel } from "../../../lib/chat/thinking.ts";
|
||||
import type { ChatThinkingSelectState } from "../../../lib/chat/thinking.ts";
|
||||
|
||||
type ChatEffortPickerParams = {
|
||||
disabled: boolean;
|
||||
disabledReason?: string;
|
||||
fastMode: ChatFastModeSelectState;
|
||||
selectedThinkingValue: string;
|
||||
sessionKey: string;
|
||||
showFastMode: boolean;
|
||||
thinkingDefaultValue: string;
|
||||
thinkingDisabled: boolean;
|
||||
thinkingOptions: ChatModelSelectOption[];
|
||||
triggerThinkingLabel: string;
|
||||
thinking: ChatThinkingSelectState;
|
||||
onFastModeSelect: (value: ChatFastModeSelectValue, sessionKey: string) => Promise<unknown>;
|
||||
onRequestUpdate?: () => void;
|
||||
onThinkingSelect: (value: string, sessionKey: string) => Promise<unknown>;
|
||||
@@ -30,37 +26,24 @@ function formatEffortLabel(label: string): string {
|
||||
}
|
||||
|
||||
export function renderChatEffortPicker(params: ChatEffortPickerParams) {
|
||||
const sliderStops = params.thinkingOptions.filter((option) => option.value !== "");
|
||||
const sliderStops = params.thinking.options;
|
||||
const showReasoning = sliderStops.length > 0;
|
||||
if (!showReasoning && (!params.showFastMode || !params.fastMode.supported)) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultStopIndex = sliderStops.findIndex(
|
||||
(option) => option.value === params.thinkingDefaultValue,
|
||||
);
|
||||
const hasThinkingOverride = params.selectedThinkingValue !== "";
|
||||
const overrideStopIndex = sliderStops.findIndex(
|
||||
(option) => option.value === params.selectedThinkingValue,
|
||||
);
|
||||
const sliderIndex = Math.max(hasThinkingOverride ? overrideStopIndex : defaultStopIndex, 0);
|
||||
const sliderUnanchored = !hasThinkingOverride && defaultStopIndex < 0;
|
||||
const selection = params.thinking.selection;
|
||||
const hasThinkingOverride = selection.source === "override";
|
||||
const selectedThinkingValue = hasThinkingOverride ? selection.value : "";
|
||||
const sliderIndex = selection.kind === "anchored" ? selection.index : 0;
|
||||
const sliderUnanchored = selection.kind === "unanchored";
|
||||
const sliderFillPercent = (index: number) =>
|
||||
sliderStops.length > 1 ? (index / (sliderStops.length - 1)) * 100 : 0;
|
||||
const defaultLevelLabel = formatThinkingOverrideLabel(params.thinkingDefaultValue);
|
||||
const selectedThinkingOption = params.thinkingOptions.find(
|
||||
(option) => option.value === params.selectedThinkingValue,
|
||||
);
|
||||
const reasoningValueText = hasThinkingOverride
|
||||
? formatEffortLabel(
|
||||
selectedThinkingOption?.label ?? formatThinkingOverrideLabel(params.selectedThinkingValue),
|
||||
)
|
||||
: defaultLevelLabel;
|
||||
const defaultLevelLabel = formatEffortLabel(params.thinking.inherited.displayLabel);
|
||||
const reasoningValueText = formatEffortLabel(selection.displayLabel);
|
||||
const reasoningValueLabel = hasThinkingOverride
|
||||
? reasoningValueText
|
||||
: t("chat.modelControls.defaultWithLevel", { level: defaultLevelLabel });
|
||||
const triggerLabel = showReasoning
|
||||
? formatEffortLabel(params.triggerThinkingLabel)
|
||||
: t("chat.modelControls.fastMode");
|
||||
const triggerLabel = showReasoning ? reasoningValueText : t("chat.modelControls.fastMode");
|
||||
const triggerTitle = params.fastMode.active
|
||||
? `${triggerLabel} · ${t("chat.modelControls.fastMode")}`
|
||||
: triggerLabel;
|
||||
@@ -116,7 +99,7 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
|
||||
const input = event.currentTarget as HTMLInputElement;
|
||||
const stop = sliderStops[Number(input.value)];
|
||||
resetSliderPreview(input);
|
||||
if (params.thinkingDisabled || !stop || stop.value === params.selectedThinkingValue) {
|
||||
if (params.thinkingDisabled || !stop || stop.value === selectedThinkingValue) {
|
||||
return;
|
||||
}
|
||||
commitThinking(stop.value);
|
||||
@@ -133,8 +116,7 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
|
||||
}
|
||||
};
|
||||
const onlyStop = sliderStops.length === 1 ? sliderStops[0] : undefined;
|
||||
const effectiveThinkingValue = params.selectedThinkingValue || params.thinkingDefaultValue;
|
||||
const onlyStopSelected = onlyStop?.value === effectiveThinkingValue;
|
||||
const onlyStopSelected = selection.kind === "anchored" && selection.index === 0;
|
||||
const speedTooltip = params.fastMode.supported
|
||||
? t("chat.modelControls.fastHelp")
|
||||
: t("chat.modelControls.speedUnsupported");
|
||||
@@ -146,7 +128,7 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
|
||||
? "chat-controls__effort-trigger--fast"
|
||||
: ""} ${params.disabled ? "chat-controls__inline-select-trigger--disabled" : ""}"
|
||||
data-chat-thinking-select="true"
|
||||
data-chat-thinking-value=${params.selectedThinkingValue}
|
||||
data-chat-thinking-value=${selectedThinkingValue}
|
||||
data-chat-thinking-disabled=${params.thinkingDisabled ? "true" : "false"}
|
||||
data-chat-fast-mode=${params.fastMode.active ? "true" : "false"}
|
||||
aria-label=${`${t("chat.selectors.thinkingLevel")}: ${triggerTitle}`}
|
||||
|
||||
@@ -142,10 +142,6 @@ function formatPickerModelLabel(label: string): string {
|
||||
return match?.[1] ?? label;
|
||||
}
|
||||
|
||||
function formatPickerThinkingLabel(label: string): string {
|
||||
return label.replace(/^Inherited:\s*/u, "");
|
||||
}
|
||||
|
||||
export function renderChatModelControls(props: ChatModelControlsProps) {
|
||||
const {
|
||||
currentOverride,
|
||||
@@ -245,11 +241,6 @@ export function renderChatModelControls(props: ChatModelControlsProps) {
|
||||
currentOverride || pickerDefaultLabel,
|
||||
props.modelCatalog,
|
||||
));
|
||||
const committedThinkingLabel =
|
||||
thinking.currentOverride === ""
|
||||
? thinking.defaultLabel
|
||||
: (thinking.options.find((entry) => entry.value === thinking.currentOverride)?.label ??
|
||||
thinking.currentOverride);
|
||||
const managedCatalog = props.modelCatalogState ?? {
|
||||
hasSnapshot: !props.modelsLoading,
|
||||
status: props.modelsLoading ? ("loading" as const) : ("ready" as const),
|
||||
@@ -281,7 +272,7 @@ export function renderChatModelControls(props: ChatModelControlsProps) {
|
||||
commonDisabled ||
|
||||
effortMutationDisabled ||
|
||||
!managedCatalog.hasSnapshot ||
|
||||
(thinking.options.length === 0 && thinking.currentOverride === "");
|
||||
(thinking.options.length === 0 && thinking.selection.source === "default");
|
||||
const showFastMode = props.showFastMode !== false;
|
||||
const effortDisabled =
|
||||
commonDisabled ||
|
||||
@@ -313,13 +304,10 @@ export function renderChatModelControls(props: ChatModelControlsProps) {
|
||||
...fastMode,
|
||||
disabled: fastMode.disabled || commonDisabled || effortMutationDisabled,
|
||||
},
|
||||
selectedThinkingValue: thinking.currentOverride,
|
||||
sessionKey: props.sessionKey,
|
||||
showFastMode,
|
||||
thinkingDefaultValue: thinking.defaultValue,
|
||||
thinkingDisabled,
|
||||
thinkingOptions: [{ value: "", label: thinking.defaultLabel }, ...thinking.options],
|
||||
triggerThinkingLabel: formatPickerThinkingLabel(committedThinkingLabel),
|
||||
thinking,
|
||||
onFastModeSelect: async (next, targetSessionKey) =>
|
||||
props.onFastModeSelect?.(next, targetSessionKey),
|
||||
onRequestUpdate: props.onRequestUpdate,
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("new-session model runtime", () => {
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restores a browser preference only after the model and thinking level validate", async () => {
|
||||
it("preserves a browser preference when an older server omits thinking profiles", async () => {
|
||||
const { context } = contextWith([
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
@@ -631,7 +631,17 @@ describe("new-session model runtime", () => {
|
||||
|
||||
it("drops a stored reasoning override when its option is no longer available", async () => {
|
||||
const { context, request } = contextWith([
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai", reasoning: true },
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
provider: "openai",
|
||||
reasoning: true,
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "high", label: "high" },
|
||||
],
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
]);
|
||||
const onSelectionChange = vi.fn();
|
||||
const control = new NewSessionModelControl(() => undefined, onSelectionChange);
|
||||
@@ -654,6 +664,114 @@ describe("new-session model runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps xhigh anchored to the selected model profile across an interactive model switch", async () => {
|
||||
const levels = (ids: string[]) => ids.map((id) => ({ id, label: id }));
|
||||
const { context, request } = contextWith([
|
||||
{
|
||||
id: "k3",
|
||||
name: "Kimi K3",
|
||||
provider: "kimi",
|
||||
reasoning: true,
|
||||
thinkingLevels: levels([
|
||||
"off",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
"ultra",
|
||||
]),
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
provider: "openai",
|
||||
reasoning: true,
|
||||
thinkingLevels: levels(["off", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
||||
thinkingDefault: "medium",
|
||||
},
|
||||
]);
|
||||
const onSelectionChange = vi.fn();
|
||||
const control = new NewSessionModelControl(() => undefined, onSelectionChange);
|
||||
control.load(context, "main", true);
|
||||
await vi.waitFor(() => {
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
renderControl(control, context).querySelector(
|
||||
'[data-chat-model-option="openai/gpt-5.6-sol"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
control.selected = "kimi/k3";
|
||||
control.thinkingLevel = "xhigh";
|
||||
|
||||
renderControl(control, context)
|
||||
.querySelector<HTMLButtonElement>('[data-chat-model-option="openai/gpt-5.6-sol"]')
|
||||
?.click();
|
||||
|
||||
expect(control.selected).toBe("openai/gpt-5.6-sol");
|
||||
expect(control.thinkingLevel).toBe("xhigh");
|
||||
expect(onSelectionChange).toHaveBeenLastCalledWith({
|
||||
model: "openai/gpt-5.6-sol",
|
||||
thinkingLevel: "xhigh",
|
||||
});
|
||||
const container = renderControl(control, context);
|
||||
const slider = container.querySelector<HTMLInputElement>('[data-chat-thinking-slider="true"]');
|
||||
expect(slider?.dataset.chatThinkingValues).toBe("off,minimal,low,medium,high,xhigh,max");
|
||||
expect(slider?.value).toBe("5");
|
||||
expect(slider?.max).toBe("6");
|
||||
expect(slider?.getAttribute("aria-valuetext")).toBe("Extra high");
|
||||
expect(
|
||||
Number.parseFloat(slider?.style.getPropertyValue("--reasoning-fill") ?? "0"),
|
||||
).toBeCloseTo(83.33, 1);
|
||||
});
|
||||
|
||||
it("clears xhigh when an interactive model switch targets a profile ending at high", async () => {
|
||||
const levels = (ids: string[]) => ids.map((id) => ({ id, label: id }));
|
||||
const { context, request } = contextWith([
|
||||
{
|
||||
id: "k3",
|
||||
name: "Kimi K3",
|
||||
provider: "kimi",
|
||||
reasoning: true,
|
||||
thinkingLevels: levels(["off", "low", "medium", "high", "xhigh"]),
|
||||
thinkingDefault: "high",
|
||||
},
|
||||
{
|
||||
id: "limited",
|
||||
name: "Limited",
|
||||
provider: "demo",
|
||||
reasoning: true,
|
||||
thinkingLevels: levels(["off", "low", "medium", "high"]),
|
||||
thinkingDefault: "medium",
|
||||
},
|
||||
]);
|
||||
const onSelectionChange = vi.fn();
|
||||
const control = new NewSessionModelControl(() => undefined, onSelectionChange);
|
||||
control.load(context, "main", true);
|
||||
await vi.waitFor(() => {
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
renderControl(control, context).querySelector('[data-chat-model-option="demo/limited"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
control.selected = "kimi/k3";
|
||||
control.thinkingLevel = "xhigh";
|
||||
|
||||
renderControl(control, context)
|
||||
.querySelector<HTMLButtonElement>('[data-chat-model-option="demo/limited"]')
|
||||
?.click();
|
||||
|
||||
expect(control.selected).toBe("demo/limited");
|
||||
expect(control.thinkingLevel).toBe("");
|
||||
expect(onSelectionChange).toHaveBeenLastCalledWith({
|
||||
model: "demo/limited",
|
||||
thinkingLevel: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses model catalog runtime metadata for an explicit cloud target", async () => {
|
||||
const { context, request } = contextWith([
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
normalizeChatModelProviderId,
|
||||
resolvePreferredServerChatModelValue,
|
||||
} from "../../lib/chat/model-ref.ts";
|
||||
import { resolveChatThinkingSelectState } from "../../lib/chat/thinking.ts";
|
||||
import { normalizeThinkingOptionValue } from "../../lib/chat/thinking.ts";
|
||||
import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts";
|
||||
import { normalizeAgentId } from "../../lib/sessions/session-key.ts";
|
||||
import {
|
||||
@@ -44,6 +44,11 @@ type NewSessionMetadataLoad = {
|
||||
};
|
||||
|
||||
type CatalogCreateTarget = Pick<SessionCatalog, "id" | "label">;
|
||||
type ReconciledNewSessionSelection = {
|
||||
model: string;
|
||||
thinkingLevel: string;
|
||||
repaired: boolean;
|
||||
};
|
||||
|
||||
function abortError(signal: AbortSignal): Error {
|
||||
return signal.reason instanceof Error
|
||||
@@ -480,65 +485,59 @@ export class NewSessionModelControl {
|
||||
if (!preference) {
|
||||
return;
|
||||
}
|
||||
// A same-agent metadata refresh revalidates the stored pair. Clear the
|
||||
// previous restoration first so retired catalog entries cannot survive.
|
||||
this.selected = "";
|
||||
this.thinkingLevel = "";
|
||||
const preferredTarget = preference.model
|
||||
? resolveDraftModelTarget(preference.model, undefined, this.catalog)
|
||||
: null;
|
||||
if (
|
||||
preference.model &&
|
||||
(!preferredTarget?.entry || preferredTarget.entry.available === false)
|
||||
) {
|
||||
this.onSelectionChange({ model: "", thinkingLevel: "" });
|
||||
return;
|
||||
}
|
||||
this.selected = preferredTarget?.entry
|
||||
? buildQualifiedChatModelValue(preferredTarget.entry.id, preferredTarget.entry.provider)
|
||||
: "";
|
||||
if (!preference.thinkingLevel) {
|
||||
return;
|
||||
}
|
||||
const sourceResult = context?.sessions.state.result ?? null;
|
||||
const agentDefaultModel = agent?.model?.primary;
|
||||
const defaultTarget = resolveDraftModelTarget(
|
||||
agentDefaultModel ?? sourceResult?.defaults.model,
|
||||
agentDefaultModel ? undefined : sourceResult?.defaults.modelProvider,
|
||||
this.catalog,
|
||||
const selection = this.reconcileSelection(
|
||||
preference.model ?? "",
|
||||
preference.thinkingLevel ?? "",
|
||||
{ agent, context },
|
||||
);
|
||||
const selectedTarget = resolveDraftModelTarget(this.selected, undefined, this.catalog);
|
||||
const draftRow: GatewaySessionRow = {
|
||||
key: "new-session:preference",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
...(selectedTarget
|
||||
? { model: selectedTarget.model, modelProvider: selectedTarget.provider ?? undefined }
|
||||
: {}),
|
||||
};
|
||||
const thinking = resolveChatThinkingSelectState({
|
||||
catalog: this.catalog,
|
||||
defaults: {
|
||||
...sourceResult?.defaults,
|
||||
modelProvider: defaultTarget?.provider ?? sourceResult?.defaults.modelProvider ?? null,
|
||||
model: defaultTarget?.model ?? sourceResult?.defaults.model ?? null,
|
||||
contextTokens: sourceResult?.defaults.contextTokens ?? null,
|
||||
agentRuntime: agent?.agentRuntime ?? sourceResult?.defaults.agentRuntime,
|
||||
thinkingLevels: agent?.thinkingLevels ?? sourceResult?.defaults.thinkingLevels,
|
||||
thinkingOptions: agent?.thinkingOptions ?? sourceResult?.defaults.thinkingOptions,
|
||||
thinkingDefault: agent?.thinkingDefault ?? sourceResult?.defaults.thinkingDefault,
|
||||
},
|
||||
session: draftRow,
|
||||
sessionKey: draftRow.key,
|
||||
sessionsResult: sourceResult,
|
||||
});
|
||||
if (thinking.options.some((entry) => entry.value === preference.thinkingLevel)) {
|
||||
this.thinkingLevel = preference.thinkingLevel;
|
||||
} else {
|
||||
this.onSelectionChange({ model: this.selected, thinkingLevel: "" });
|
||||
this.selected = selection.model;
|
||||
this.thinkingLevel = selection.thinkingLevel;
|
||||
if (selection.repaired) {
|
||||
this.onSelectionChange({ model: selection.model, thinkingLevel: selection.thinkingLevel });
|
||||
}
|
||||
}
|
||||
|
||||
private reconcileSelection(
|
||||
model: string,
|
||||
thinkingLevel: string,
|
||||
options: { agent?: GatewayAgentRow; context: ApplicationContext | undefined },
|
||||
): ReconciledNewSessionSelection {
|
||||
const requestedModel = model.trim();
|
||||
const selectedTarget = requestedModel
|
||||
? resolveDraftModelTarget(requestedModel, undefined, this.catalog)
|
||||
: null;
|
||||
if (requestedModel && (!selectedTarget?.entry || selectedTarget.entry.available === false)) {
|
||||
return { model: "", thinkingLevel: "", repaired: true };
|
||||
}
|
||||
const selected = selectedTarget?.entry
|
||||
? buildQualifiedChatModelValue(selectedTarget.entry.id, selectedTarget.entry.provider)
|
||||
: "";
|
||||
if (!thinkingLevel) {
|
||||
return { model: selected, thinkingLevel: "", repaired: false };
|
||||
}
|
||||
const defaults = options.context?.sessions.state.result?.defaults;
|
||||
const agentDefaultModel = options.agent?.model?.primary;
|
||||
const defaultTarget = selected
|
||||
? null
|
||||
: resolveDraftModelTarget(
|
||||
agentDefaultModel ?? defaults?.model,
|
||||
agentDefaultModel ? undefined : defaults?.modelProvider,
|
||||
this.catalog,
|
||||
);
|
||||
const targetEntry = selectedTarget?.entry ?? defaultTarget?.entry;
|
||||
const authoritativeLevels = selected
|
||||
? targetEntry?.thinkingLevels
|
||||
: (options.agent?.thinkingLevels ?? defaults?.thinkingLevels ?? targetEntry?.thinkingLevels);
|
||||
const normalizedThinking = normalizeThinkingOptionValue(thinkingLevel);
|
||||
const supported = authoritativeLevels?.some(
|
||||
(level) => normalizeThinkingOptionValue(level.id) === normalizedThinking,
|
||||
);
|
||||
if (targetEntry?.reasoning === false || (authoritativeLevels !== undefined && !supported)) {
|
||||
return { model: selected, thinkingLevel: "", repaired: true };
|
||||
}
|
||||
return { model: selected, thinkingLevel, repaired: false };
|
||||
}
|
||||
|
||||
resolveAgentRuntimeId(options: {
|
||||
agent?: GatewayAgentRow;
|
||||
context: ApplicationContext | undefined;
|
||||
@@ -644,15 +643,9 @@ export class NewSessionModelControl {
|
||||
onModelSelect: (value) => {
|
||||
this.selectionGeneration += 1;
|
||||
this.restoringPreference = false;
|
||||
this.selected = value;
|
||||
const target = resolveDraftModelTarget(
|
||||
value || agentDefaultModel || sourceResult?.defaults.model,
|
||||
value || agentDefaultModel ? undefined : sourceResult?.defaults.modelProvider,
|
||||
this.catalog,
|
||||
);
|
||||
if (target?.entry?.reasoning === false) {
|
||||
this.thinkingLevel = "";
|
||||
}
|
||||
const selection = this.reconcileSelection(value, this.thinkingLevel, options);
|
||||
this.selected = selection.model;
|
||||
this.thinkingLevel = selection.thinkingLevel;
|
||||
this.onSelectionChange({ model: this.selected, thinkingLevel: this.thinkingLevel });
|
||||
},
|
||||
onModelPickerTargetSelect: (groupId, catalogId) => {
|
||||
|
||||
Reference in New Issue
Block a user