Merge branch 'codex/20260912-model-capabilities-c48271f9-model-capabilities'
This commit is contained in:
@@ -81,10 +81,11 @@ export async function setCodingConversationModel(
|
||||
export async function setCodingConversationThinking(
|
||||
conversationId: string,
|
||||
thinkingLevel: ConversationThinkingLevel,
|
||||
reasoningChoice?: import('../../shared/managed-model-capabilities').ManagedReasoningChoice,
|
||||
): Promise<ConversationModelState> {
|
||||
const response = await hostApiFetch<{ model: ConversationModelState }>(
|
||||
`/api/coding/conversations/${encodeURIComponent(conversationId)}/thinking`,
|
||||
{ method: 'POST', body: JSON.stringify({ thinkingLevel }) },
|
||||
{ method: 'POST', body: JSON.stringify({ thinkingLevel, reasoningChoice }) },
|
||||
);
|
||||
return response.model;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ProviderAccount, ProviderVendorInfo } from '@/lib/providers';
|
||||
import type { ConversationThinkingLevel, ProductModelRef } from '@/types/coding-conversation';
|
||||
import { getImportedModelProfile } from '../../shared/imported-model-profile';
|
||||
import { unknownManagedModelCapability, type ManagedModelCapability } from '../../shared/managed-model-capabilities';
|
||||
|
||||
export interface CodingModelOption {
|
||||
key: string;
|
||||
@@ -8,6 +9,7 @@ export interface CodingModelOption {
|
||||
modelId: string;
|
||||
label: string;
|
||||
availableThinkingLevels: ConversationThinkingLevel[] | null;
|
||||
managedCapability?: ManagedModelCapability;
|
||||
}
|
||||
|
||||
const STANDARD_THINKING_LEVELS: readonly ConversationThinkingLevel[] = [
|
||||
@@ -23,6 +25,7 @@ function availableThinkingLevels(
|
||||
account: ProviderAccount,
|
||||
modelId: string,
|
||||
): ConversationThinkingLevel[] | null {
|
||||
if (account.id === 'niancode-user-models') return [];
|
||||
const serverCapability = account.metadata?.worksSquareModelCapabilities?.[modelId];
|
||||
if (serverCapability) {
|
||||
return [
|
||||
@@ -85,6 +88,8 @@ export function buildCodingModelOptions(
|
||||
modelId,
|
||||
label: `${account.label}${vendor && vendor !== account.label ? ` · ${vendor}` : ''} / ${modelId}`,
|
||||
availableThinkingLevels: availableThinkingLevels(account, modelId),
|
||||
...(account.id === 'niancode-user-models' ? { managedCapability:
|
||||
account.metadata?.worksSquareModelCapabilitiesV2?.models[modelId] ?? unknownManagedModelCapability() } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ export interface ProviderAccount {
|
||||
resourceUrl?: string;
|
||||
customModels?: string[];
|
||||
worksSquareModelCapabilities?: ImportedModelCapabilities;
|
||||
worksSquareModelCapabilitiesV2?: import('../../shared/managed-model-capabilities').ManagedModelCatalog;
|
||||
worksSquareCredentialMode?: string;
|
||||
worksSquareCredentialExpiresAt?: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LoaderCircle, Mic, Plus, RotateCcw, Send, Square, X } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -69,6 +70,12 @@ export function CodingComposer({
|
||||
onRemoveAttachment,
|
||||
onRefreshRuntime,
|
||||
}: CodingComposerProps) {
|
||||
const accounts = useProviderStore(state => state.accounts);
|
||||
const selectedModel = snapshot?.conversation.model.model ?? conversation?.model;
|
||||
const managed = selectedModel?.accountId === 'niancode-user-models';
|
||||
const capability = selectedModel ? accounts.find(account => account.id === selectedModel.accountId)
|
||||
?.metadata?.worksSquareModelCapabilitiesV2?.models[selectedModel.modelId] : undefined;
|
||||
const acceptsImages = !managed || capability?.inputModalities?.includes('image') === true;
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const latestValueRef = useRef(value);
|
||||
useEffect(() => {
|
||||
@@ -149,7 +156,7 @@ export function CodingComposer({
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
multiple
|
||||
disabled={!editable || submitting}
|
||||
disabled={!editable || submitting || !acceptsImages}
|
||||
className="sr-only"
|
||||
data-testid="coding-file-attachment-input"
|
||||
onChange={(event) => {
|
||||
@@ -193,7 +200,7 @@ export function CodingComposer({
|
||||
if (canSend) onSubmit();
|
||||
}}
|
||||
onPaste={(event) => {
|
||||
if (submitting) return;
|
||||
if (submitting || !acceptsImages) return;
|
||||
const files = Array.from(event.clipboardData.files).filter((file) => file.type.startsWith('image/'));
|
||||
if (files.length > 0) onAddFiles(files);
|
||||
}}
|
||||
@@ -204,8 +211,9 @@ export function CodingComposer({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0 rounded-full p-0"
|
||||
disabled={!editable || submitting}
|
||||
disabled={!editable || submitting || !acceptsImages}
|
||||
aria-label="添加图片"
|
||||
title={acceptsImages ? '添加图片' : '该模型尚未确认支持图片输入'}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Plus className="h-4 w-4" aria-hidden="true" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { managedReasoningChoiceKey, managedReasoningOptions } from '../../../shared/managed-model-capabilities';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import {
|
||||
Check,
|
||||
@@ -84,12 +85,16 @@ export function CodingComposerRuntimeControls({
|
||||
const model = snapshot?.conversation.model.model ?? conversation.model ?? null;
|
||||
const modelValue = model ? codingModelKey(model) : '';
|
||||
const hasCurrentOption = options.some((option) => option.key === modelValue);
|
||||
const managedCapability = options.find(option => option.key === modelValue)?.managedCapability
|
||||
?? snapshot?.conversation.model.managedCapability;
|
||||
const managedOptions = managedCapability ? managedReasoningOptions(managedCapability) : undefined;
|
||||
const thinkingLevel = model?.thinkingLevel ?? 'off';
|
||||
const thinkingValue = managedOptions ? managedReasoningChoiceKey(model?.reasoningChoice ?? { mode: 'default' }) : thinkingLevel;
|
||||
const availableThinkingLevels = snapshot?.conversation.model.availableThinkingLevels;
|
||||
const thinkingOptions = availableThinkingLevels
|
||||
const thinkingOptions = managedOptions ?? (availableThinkingLevels
|
||||
? THINKING_OPTIONS.filter((option) => availableThinkingLevels.includes(option.value))
|
||||
: THINKING_OPTIONS;
|
||||
const thinkingIsUnavailable = availableThinkingLevels?.every((level) => level === 'off') ?? false;
|
||||
: THINKING_OPTIONS);
|
||||
const thinkingIsUnavailable = managedOptions ? false : availableThinkingLevels?.every((level) => level === 'off') ?? false;
|
||||
const runStatus = snapshot?.run.status ?? 'preparing';
|
||||
const running = ['queued', 'running', 'retrying', 'compacting', 'aborting'].includes(runStatus);
|
||||
const runtimeErrorCode = snapshot?.run.error?.code ?? snapshot?.worker.error?.code;
|
||||
@@ -106,7 +111,9 @@ export function CodingComposerRuntimeControls({
|
||||
: actionError;
|
||||
const runtimeControlsDisabled = Boolean(busyAction) || running;
|
||||
const modelLabel = model?.modelId ?? '选择模型';
|
||||
const currentThinkingLabel = thinkingLabel(thinkingLevel);
|
||||
const currentThinkingLabel = managedOptions
|
||||
? managedOptions.find(option => option.value === thinkingValue)?.label ?? '请重新选择'
|
||||
: thinkingLabel(thinkingLevel);
|
||||
const thinkingDisplayLabel = thinkingIsUnavailable ? '不可调' : currentThinkingLabel;
|
||||
|
||||
const perform = (key: string, action: () => Promise<void>) => {
|
||||
@@ -133,6 +140,15 @@ export function CodingComposerRuntimeControls({
|
||||
};
|
||||
|
||||
const selectThinking = (value: string) => {
|
||||
if (managedOptions) {
|
||||
const option = managedOptions.find(option => option.value === value);
|
||||
if (!option || value === thinkingValue) return;
|
||||
perform('thinking', async () => {
|
||||
await setCodingConversationThinking(conversation.id, 'off', option.choice);
|
||||
await onRefresh();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const selected = value as ConversationThinkingLevel;
|
||||
if (selected === thinkingLevel) return;
|
||||
perform('thinking', async () => {
|
||||
@@ -266,7 +282,7 @@ export function CodingComposerRuntimeControls({
|
||||
className="z-[101] min-w-44 rounded-[14px] border border-border/80 bg-background p-1.5 text-sm shadow-[0_18px_50px_rgba(26,31,42,0.18)] outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1"
|
||||
aria-label="选择推理强度"
|
||||
>
|
||||
<DropdownMenu.RadioGroup value={thinkingLevel} onValueChange={selectThinking}>
|
||||
<DropdownMenu.RadioGroup value={thinkingValue} onValueChange={selectThinking}>
|
||||
{thinkingOptions.map((option) => (
|
||||
<DropdownMenu.RadioItem
|
||||
key={option.value}
|
||||
|
||||
@@ -109,7 +109,12 @@ function ModelCard({ model }: { model: CodingModelOption }) {
|
||||
<p className="min-w-0 break-all text-base font-semibold" title={model.modelId}>{model.modelId}</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1.5">
|
||||
<span className="mr-0.5 text-[11px] font-medium text-muted-foreground">可选思考强度</span>
|
||||
{model.availableThinkingLevels === null
|
||||
{model.managedCapability
|
||||
? <span className="text-[11px] font-medium text-muted-foreground">
|
||||
{model.managedCapability.inputModalities === null ? '输入能力未知' : model.managedCapability.inputModalities.includes('image') ? '支持图片输入' : '文本输入'}
|
||||
{' · '}{model.managedCapability.reasoning.effortValues?.join(' / ') || (model.managedCapability.reasoning.canDisable ? '支持思考开关' : '模型默认')}
|
||||
</span>
|
||||
: model.availableThinkingLevels === null
|
||||
? <span className="text-[11px] font-medium text-muted-foreground">进入对话后可查看</span>
|
||||
: model.availableThinkingLevels.length === 0
|
||||
|| model.availableThinkingLevels.every((level) => level === 'off')
|
||||
|
||||
Reference in New Issue
Block a user