Surface resolved Databricks model metadata (#9206)

Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
jh-block
2026-05-20 12:50:44 +02:00
committed by GitHub
parent 98a54e9ec6
commit 8f9270e295
19 changed files with 369 additions and 47 deletions
+33 -1
View File
@@ -5799,6 +5799,25 @@
}
}
},
"InferenceMetadata": {
"type": "object",
"required": [
"provider",
"requestedModel"
],
"properties": {
"provider": {
"type": "string"
},
"requestedModel": {
"type": "string"
},
"resolvedModel": {
"type": "string",
"nullable": true
}
}
},
"InspectJobResponse": {
"type": "object",
"properties": {
@@ -6394,7 +6413,7 @@
},
"MessageMetadata": {
"type": "object",
"description": "Metadata for message visibility",
"description": "Metadata for message visibility and model inference details",
"required": [
"userVisible",
"agentVisible"
@@ -6404,6 +6423,14 @@
"type": "boolean",
"description": "Whether the message should be included in the agent's context window"
},
"inference": {
"allOf": [
{
"$ref": "#/components/schemas/InferenceMetadata"
}
],
"nullable": true
},
"userVisible": {
"type": "boolean",
"description": "Whether the message should be visible to the user in the UI"
@@ -6587,6 +6614,11 @@
"type": "boolean",
"description": "Whether this model supports reasoning/thinking controls"
},
"resolved_model": {
"type": "string",
"description": "The underlying model resolved from provider metadata, when the configured model is an alias or endpoint.",
"nullable": true
},
"supports_cache_control": {
"type": "boolean",
"description": "Whether this model supports cache control",
File diff suppressed because one or more lines are too long
+12 -1
View File
@@ -602,6 +602,12 @@ export type ImportSessionRequest = {
json: string;
};
export type InferenceMetadata = {
provider: string;
requestedModel: string;
resolvedModel?: string | null;
};
export type InspectJobResponse = {
processStartTime?: string | null;
runningDurationSeconds?: number | null;
@@ -746,13 +752,14 @@ export type MessageEvent = {
};
/**
* Metadata for message visibility
* Metadata for message visibility and model inference details
*/
export type MessageMetadata = {
/**
* Whether the message should be included in the agent's context window
*/
agentVisible: boolean;
inference?: InferenceMetadata | null;
/**
* Whether the message should be visible to the user in the UI
*/
@@ -822,6 +829,10 @@ export type ModelInfo = {
* Whether this model supports reasoning/thinking controls
*/
reasoning?: boolean;
/**
* The underlying model resolved from provider metadata, when the configured model is an alias or endpoint.
*/
resolved_model?: string | null;
/**
* Whether this model supports cache control
*/
+10
View File
@@ -196,6 +196,15 @@ export default function BaseChat({
const sessionModel = session?.model_config?.model_name ?? null;
const sessionProvider = session?.provider_name ?? null;
const sessionLoaded = session !== undefined;
const latestInference = useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'assistant' && message.metadata.userVisible && message.metadata.inference) {
return message.metadata.inference;
}
}
return null;
}, [messages]);
useEffect(() => {
if (!recipe || !isActiveSession) return;
@@ -508,6 +517,7 @@ export default function BaseChat({
sessionModel={sessionModel}
sessionProvider={sessionProvider}
sessionLoaded={sessionLoaded}
latestInference={latestInference}
{...customChatInputProps}
/>
</div>
+3
View File
@@ -204,6 +204,7 @@ interface ChatInputProps {
sessionModel?: string | null;
sessionProvider?: string | null;
sessionLoaded?: boolean;
latestInference?: Message['metadata']['inference'] | null;
}
export default function ChatInput({
@@ -234,6 +235,7 @@ export default function ChatInput({
sessionModel,
sessionProvider,
sessionLoaded,
latestInference,
}: ChatInputProps) {
const [_value, setValue] = useState(initialValue);
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
@@ -1757,6 +1759,7 @@ export default function ChatInput({
setView={setView}
sessionModel={effectiveModel}
sessionProvider={effectiveProvider}
latestInference={latestInference}
onModelChanged={setModelOverride}
sessionLoaded={sessionLoaded}
/>
@@ -14,7 +14,7 @@
* - Configurable batch size and delay
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from '../i18n';
import { Message, SystemNotificationContent } from '../api';
import GooseMessage from './GooseMessage';
@@ -31,6 +31,7 @@ import { NotificationEvent } from '../types/message';
import LoadingGoose from './LoadingGoose';
import { ChatType } from '../types/chat';
import { identifyConsecutiveToolCalls, isInChain } from '../utils/toolCallChaining';
import { getModelDisplayName } from './settings/models/predefinedModelsUtils';
const i18n = defineMessages({
loadingMessages: {
@@ -41,6 +42,10 @@ const i18n = defineMessages({
id: 'progressiveMessageList.searchHint',
defaultMessage: 'Press Cmd/Ctrl+F to load all messages immediately for search',
},
modelChanged: {
id: 'progressiveMessageList.modelChanged',
defaultMessage: 'Model changed: {previousModel} → {currentModel}',
},
});
interface ProgressiveMessageListProps {
@@ -91,6 +96,37 @@ export default function ProgressiveMessageList({
const hasOnlyToolResponses = (message: Message) =>
message.content.every((c) => c.type === 'toolResponse');
const getResolvedModel = useCallback((message: Message): string | null => {
if (message.role !== 'assistant' || !message.metadata.userVisible) return null;
return message.metadata.inference?.resolvedModel ?? null;
}, []);
const getPreviousResolvedModel = useCallback(
(index: number): string | null => {
for (let i = index - 1; i >= 0; i--) {
const model = getResolvedModel(messages[i]);
if (model) return model;
}
return null;
},
[getResolvedModel, messages]
);
const renderModelChangeDisclosure = useCallback(
(previousModel: string, currentModel: string) => (
<SystemNotificationInline
notification={{
msg: intl.formatMessage(i18n.modelChanged, {
previousModel: getModelDisplayName(previousModel),
currentModel: getModelDisplayName(currentModel),
}),
notificationType: 'inlineMessage',
}}
/>
),
[intl]
);
const getSystemNotification = (message: Message): SystemNotificationContent | undefined => {
return getCreditsExhaustedNotification(message) ?? getInlineSystemNotification(message);
};
@@ -231,34 +267,46 @@ export default function ProgressiveMessageList({
const isUser = isUserMessage(message);
const messageIsInChain = isInChain(index, toolCallChains);
const currentResolvedModel = getResolvedModel(message);
const previousResolvedModel = currentResolvedModel ? getPreviousResolvedModel(index) : null;
const showModelChangeDisclosure = Boolean(
currentResolvedModel &&
previousResolvedModel &&
currentResolvedModel !== previousResolvedModel
);
const messageKey = message.id ?? `msg-${index}-${message.created}`;
return (
<div
key={message.id ?? `msg-${index}-${message.created}`}
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'} ${messageIsInChain ? 'in-chain' : ''}`}
data-testid="message-container"
>
{isUser ? (
!hasOnlyToolResponses(message) && (
<UserMessage message={message} onMessageUpdate={onMessageUpdate} />
)
) : (
<GooseMessage
sessionId={chat.sessionId}
message={message}
messages={messages}
append={append}
toolCallNotifications={toolCallNotifications}
isStreaming={
isStreamingMessage &&
!isUser &&
index === messagesToRender.length - 1 &&
message.role === 'assistant'
}
submitElicitationResponse={submitElicitationResponse}
/>
)}
</div>
<Fragment key={messageKey}>
{showModelChangeDisclosure && currentResolvedModel && previousResolvedModel &&
renderModelChangeDisclosure(previousResolvedModel, currentResolvedModel)}
<div
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'} ${messageIsInChain ? 'in-chain' : ''}`}
data-testid="message-container"
>
{isUser ? (
!hasOnlyToolResponses(message) && (
<UserMessage message={message} onMessageUpdate={onMessageUpdate} />
)
) : (
<GooseMessage
sessionId={chat.sessionId}
message={message}
messages={messages}
append={append}
toolCallNotifications={toolCallNotifications}
isStreaming={
isStreamingMessage &&
!isUser &&
index === messagesToRender.length - 1 &&
message.role === 'assistant'
}
submitElicitationResponse={submitElicitationResponse}
/>
)}
</div>
</Fragment>
);
})
.filter(Boolean);
@@ -274,6 +322,9 @@ export default function ProgressiveMessageList({
onMessageUpdate,
toolCallChains,
submitElicitationResponse,
getPreviousResolvedModel,
getResolvedModel,
renderModelChangeDisclosure,
]);
return (
@@ -1,5 +1,5 @@
import { Sliders, Bot, LoaderCircle, Settings } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
import { View } from '../../../../utils/navigationUtils';
@@ -16,6 +16,7 @@ import { getModelDisplayName } from '../predefinedModelsUtils';
import { ModelSettingsPanel } from '../../localInference/ModelSettingsPanel';
import { ScrollArea } from '../../../ui/scroll-area';
import { defineMessages, useIntl } from '../../../../i18n';
import type { Message } from '../../../../api';
const i18n = defineMessages({
selectModel: {
@@ -42,6 +43,10 @@ const i18n = defineMessages({
id: 'modelsBottomBar.localModelSettingsTitle',
defaultMessage: 'Local Model Settings — {modelName}',
},
resolvedModel: {
id: 'modelsBottomBar.resolvedModel',
defaultMessage: 'Resolved model',
},
});
interface ModelsBottomBarProps {
@@ -50,6 +55,7 @@ interface ModelsBottomBarProps {
setView: (view: View) => void;
sessionModel?: string | null;
sessionProvider?: string | null;
latestInference?: Message['metadata']['inference'] | null;
onModelChanged: (override: { model: string; provider: string }) => void;
sessionLoaded?: boolean;
}
@@ -60,6 +66,7 @@ export default function ModelsBottomBar({
setView,
sessionModel,
sessionProvider,
latestInference,
onModelChanged,
sessionLoaded,
}: ModelsBottomBarProps) {
@@ -81,6 +88,14 @@ export default function ModelsBottomBar({
// rather than flashing the config default or leaving the footer blank.
const isModelLoading = Boolean(sessionId && !sessionLoaded);
const displayModel = currentModel || providerDefaultModel || displayModelName;
const resolvedModel = latestInference?.resolvedModel ?? null;
const shouldShowResolvedModel = Boolean(
!isModelLoading &&
resolvedModel &&
latestInference?.provider === currentProvider &&
latestInference?.requestedModel === currentModel &&
resolvedModel !== currentModel
);
const loadingModelLabel = intl.formatMessage(i18n.loadingModel);
const triggerLabel = isModelLoading ? loadingModelLabel : displayModel;
const menuModelLabel = isModelLoading ? loadingModelLabel : displayModelName;
@@ -118,6 +133,11 @@ export default function ModelsBottomBar({
setDisplayModelName(getModelDisplayName(currentModel));
}, [currentModel]);
const resolvedDisplayModelName = useMemo(
() => (resolvedModel ? getModelDisplayName(resolvedModel) : null),
[resolvedModel]
);
const handleModelSelected = (model: string, provider: string) => {
onModelChanged({ model, provider });
};
@@ -147,6 +167,14 @@ export default function ModelsBottomBar({
{menuModelLabel}
{!isModelLoading && displayProvider && `${displayProvider}`}
</p>
{shouldShowResolvedModel && resolvedDisplayModelName && (
<div className="mx-2 pb-2 border-b mb-2">
<h6 className="text-xs text-text-primary">{intl.formatMessage(i18n.resolvedModel)}</h6>
<p className="text-xs text-text-primary truncate" title={resolvedModel ?? undefined}>
{resolvedDisplayModelName}
</p>
</div>
)}
<DropdownMenuItem onClick={() => setIsAddModelModalOpen(true)}>
<span>{intl.formatMessage(i18n.changeModel)}</span>
<Sliders className="ml-auto h-4 w-4 rotate-90" />
+55 -1
View File
@@ -183,6 +183,13 @@ function pushMessage(currentMessages: Message[], incomingMsg: Message): Message[
const lastContent = lastMsg.content[lastMsg.content.length - 1];
const newContent = incomingMsg.content[incomingMsg.content.length - 1];
if (incomingMsg.metadata?.inference) {
lastMsg.metadata = {
...lastMsg.metadata,
inference: incomingMsg.metadata.inference,
};
}
if (
lastContent?.type === 'text' &&
newContent?.type === 'text' &&
@@ -236,6 +243,7 @@ function createEventProcessor(
let latestChatState: ChatState = ChatState.Streaming;
let lastBatchUpdate = Date.now();
let hasPendingUpdate = false;
let pendingInference: Message['metadata']['inference'] | undefined;
const flushBatchedUpdates = () => {
if (reduceMotion && hasPendingUpdate) {
@@ -271,12 +279,54 @@ function createEventProcessor(
}
};
const flushPendingInference = () => {
if (!pendingInference) {
return;
}
for (let i = currentMessages.length - 1; i >= 0; i--) {
const message = currentMessages[i];
if (message.role === 'assistant' && message.metadata.userVisible) {
currentMessages = [
...currentMessages.slice(0, i),
{
...message,
metadata: {
...message.metadata,
inference: message.metadata.inference ?? pendingInference,
},
},
...currentMessages.slice(i + 1),
];
break;
}
}
pendingInference = undefined;
};
// Returns true if the event is terminal (Finish or Error)
const processEvent = (event: SessionEvent): boolean => {
switch (event.type) {
case 'Message': {
const msg = (event as Record<string, unknown>).message as Message;
let msg = (event as Record<string, unknown>).message as Message;
const tokenState = (event as Record<string, unknown>).token_state as TokenState;
if (msg.content.length === 0 && msg.metadata?.inference) {
pendingInference = msg.metadata.inference;
return false;
}
if (pendingInference && msg.role === 'assistant' && msg.metadata.userVisible) {
msg = {
...msg,
metadata: {
...msg.metadata,
inference: msg.metadata.inference ?? pendingInference,
},
};
pendingInference = undefined;
}
currentMessages = pushMessage(currentMessages, msg);
const hasToolConfirmation = msg.content.some(
@@ -301,7 +351,9 @@ function createEventProcessor(
return false;
}
case 'Error': {
flushPendingInference();
flushBatchedUpdates();
dispatch({ type: 'SET_MESSAGES', payload: currentMessages });
const errorMsg = String((event as Record<string, unknown>).error ?? '');
if (errorMsg.includes('too far behind') && onReloadNeeded) {
// Server indicated we missed events — end streaming without setting
@@ -315,7 +367,9 @@ function createEventProcessor(
return true;
}
case 'Finish': {
flushPendingInference();
flushBatchedUpdates();
dispatch({ type: 'SET_MESSAGES', payload: currentMessages });
onFinish();
return true;
}
+6
View File
@@ -2390,6 +2390,9 @@
"modelsBottomBar.localModelSettingsTitle": {
"defaultMessage": "Local Model Settings — {modelName}"
},
"modelsBottomBar.resolvedModel": {
"defaultMessage": "Resolved model"
},
"modelsBottomBar.selectModel": {
"defaultMessage": "Select Model"
},
@@ -2687,6 +2690,9 @@
"progressiveMessageList.loadingMessages": {
"defaultMessage": "Loading messages... ({renderedCount}/{totalCount})"
},
"progressiveMessageList.modelChanged": {
"defaultMessage": "Model changed: {previousModel} → {currentModel}"
},
"progressiveMessageList.searchHint": {
"defaultMessage": "Press Cmd/Ctrl+F to load all messages immediately for search"
},
+6
View File
@@ -2333,6 +2333,9 @@
"modelsBottomBar.localModelSettingsTitle": {
"defaultMessage": "本地模型设置 — {modelName}"
},
"modelsBottomBar.resolvedModel": {
"defaultMessage": "解析后的模型"
},
"modelsBottomBar.selectModel": {
"defaultMessage": "选择模型"
},
@@ -2630,6 +2633,9 @@
"progressiveMessageList.loadingMessages": {
"defaultMessage": "正在加载消息…({renderedCount}/{totalCount}"
},
"progressiveMessageList.modelChanged": {
"defaultMessage": "模型已更改:{previousModel} → {currentModel}"
},
"progressiveMessageList.searchHint": {
"defaultMessage": "按 Cmd/Ctrl+F 立即加载全部消息以便搜索"
},