Merge branch 'codex/20260912-model-capabilities-c48271f9-model-capabilities'

This commit is contained in:
2026-09-12 22:09:09 +08:00
29 changed files with 681 additions and 242 deletions

View File

@@ -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;
}

View File

@@ -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() } : {}),
});
}
}

View File

@@ -128,6 +128,7 @@ export interface ProviderAccount {
resourceUrl?: string;
customModels?: string[];
worksSquareModelCapabilities?: ImportedModelCapabilities;
worksSquareModelCapabilitiesV2?: import('../../shared/managed-model-capabilities').ManagedModelCatalog;
worksSquareCredentialMode?: string;
worksSquareCredentialExpiresAt?: string;
};

View File

@@ -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" />

View File

@@ -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}

View File

@@ -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')