mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Surface resolved Databricks model metadata (#9206)
Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
+33
-1
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 立即加载全部消息以便搜索"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user