mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-08 01:48:26 -10:00
fix(provider): expose DeepSeek Flash reasoning effort / 修复 DeepSeek Flash 思考深度 (#10117)
* fix(provider): expose reasoning effort for DeepSeek Flash aliases Problem: DeepSeek Flash short aliases appeared in provider model catalogs without selectable reasoning depth.\n\nRoot cause: capability overrides and OpenCode model contracts only recognized the deepseek-v4-flash spelling.\n\nFix: register deepseek-flash in official and Token Rhythm capability maps, and recognize it across OpenCode routes.\n\nVerification: go test ./internal/config ./internal/provider * feat(settings): add per-model reasoning effort control Problem: model settings exposed context, output, and vision overrides but offered no way to configure supported reasoning levels.\n\nRoot cause: model-level effort metadata was available in the provider view but was not wired into the model dialog.\n\nFix: add an automatic/explicit effort selector, persist supported efforts and the selected default through modelOverrides, and localize the control in English and Chinese.\n\nVerification: pnpm exec tsc --noEmit; node --import ./scripts/css-stub-register.mjs --import tsx src/__tests__/provider-editor-compact.test.tsx * fix(settings): make model reasoning levels configurable Problem: the model editor only selected a default effort and could not configure which levels were exposed.\n\nFix: add model-level available-effort checkboxes plus a default-effort selector, while keeping Automatic as the reset path to provider capabilities.\n\nVerification: pnpm exec tsc --noEmit; provider-editor-compact.test.tsx * style(settings): refine model effort editor layout Improve the model dialog hierarchy by grouping reasoning controls in a compact card, balancing the two columns, and giving the capabilities panel a lighter surface. * fix(settings): preserve configured reasoning levels Problem: selecting available effort levels while keeping Automatic discarded the configured list, and unchecking the selected default could leave an invalid default.\n\nFix: persist available levels independently of the default, clear a default when its level is removed, and validate the default against the selected list before applying.\n\nVerification: provider-editor-compact.test.tsx; pnpm exec tsc --noEmit
This commit is contained in:
1 parent
32664afeb5
commit
35785505c8
16 files changed
+192
-114
No files matched your search
@@ -15,6 +15,7 @@ await act(async () => root.render(<LocaleProvider><ProviderEditor initial={provi
|
||||
const save = document.querySelector('.provider-editor-footer .btn--primary') as HTMLButtonElement;
|
||||
const edit = () => (document.querySelector('.provider-model-draft__option button') as HTMLButtonElement).click();
|
||||
async function change(input: HTMLInputElement,value:string) { await act(async()=>{Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value')!.set!.call(input,value);input.dispatchEvent(new window.Event('input',{bubbles:true}));}); }
|
||||
async function changeSelect(select: HTMLSelectElement,value:string) { await act(async()=>{Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype,'value')!.set!.call(select,value);select.dispatchEvent(new window.Event('change',{bubbles:true}));}); }
|
||||
const dialog = () => document.querySelector('dialog')!;
|
||||
const submit = async () => act(async()=>{dialog().querySelector('form')!.dispatchEvent(new window.Event('submit',{bubbles:true,cancelable:true}));});
|
||||
assert.equal(save.disabled,true);
|
||||
@@ -34,6 +35,7 @@ await change(numbers[0],'128000');await change(numbers[1],'4096');
|
||||
const image=dialog().querySelector<HTMLInputElement>('aside input:not(:disabled)')!;
|
||||
await act(async()=>image.click());
|
||||
assert.equal(dialog().querySelectorAll('aside input:disabled').length,4,'text is locked; video and PDF unavailable');
|
||||
await changeSelect(dialog().querySelector('select')!, '');
|
||||
await submit();
|
||||
assert.equal(save.disabled,false);
|
||||
assert.equal(document.querySelector('.provider-context-badge')!.textContent,'128K');
|
||||
@@ -45,6 +47,15 @@ assert.equal(saved.modelOverrides[0].contextWindow,128000);
|
||||
assert.equal(saved.modelOverrides[0].maxOutputTokens,4096);
|
||||
assert.equal(saved.modelOverrides[0].vision,true);
|
||||
assert.equal(saved.modelOverrides[0].reasoningProtocol,'deepseek','preserves unrelated reasoning configuration');
|
||||
assert.deepEqual(saved.modelOverrides[0].supportedEfforts,['high'],'automatic default preserves configured effort options');
|
||||
assert.equal(saved.modelOverrides[0].defaultEffort,'','automatic default is stored without a model default');
|
||||
await act(async()=>edit());
|
||||
const highLevel = Array.from(dialog().querySelectorAll<HTMLInputElement>('.provider-model-dialog__effort-card input[type="checkbox"]')).find(input => input.getAttribute('aria-label') === 'high' || input.parentElement?.textContent?.trim() === 'high');
|
||||
assert.ok(highLevel, 'configured effort checkbox is rendered');
|
||||
await act(async()=>highLevel!.click());
|
||||
await submit();await act(async()=>save.click());
|
||||
assert.deepEqual(saved.modelOverrides[0].supportedEfforts,[],'removing the default effort clears the invalid option');
|
||||
assert.equal(saved.modelOverrides[0].defaultEffort,'','removing the default effort falls back to automatic');
|
||||
await act(async()=>edit());
|
||||
await act(async()=>{
|
||||
dialog().querySelectorAll<HTMLButtonElement>('section label button').forEach(button=>button.click());
|
||||
|
||||
@@ -94,20 +94,20 @@
|
||||
.provider-connection-field > .mem-select { max-width: none; }
|
||||
.provider-editor-footer .btn { min-height: 36px; padding-inline: 18px; font-size: 14px; }
|
||||
|
||||
.provider-model-dialog { width: min(820px, calc(100vw - 32px)); max-height: calc(100vh - 40px); box-sizing: border-box; padding: 28px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-elev); color: var(--fg); font: 14px var(--font-sans); }
|
||||
.provider-model-dialog { width: min(900px, calc(100vw - 32px)); max-height: calc(100vh - 40px); box-sizing: border-box; padding: 28px 32px 24px; border: 1px solid var(--border); border-radius: 16px; background: var(--bg-elev); color: var(--fg); font: 14px var(--font-sans); }
|
||||
.provider-model-dialog::backdrop { background: #0008; }
|
||||
.provider-model-dialog header, .provider-model-dialog footer { display: flex; align-items: center; gap: 12px; }
|
||||
.provider-model-dialog h2 { flex: 1; margin: 0; font-size: 21px; }
|
||||
.provider-model-dialog h3 { margin: 0 0 16px; font-size: 16px; }
|
||||
.provider-model-dialog__id { display: grid; gap: 12px; margin: 24px 0; }
|
||||
.provider-model-dialog__id > span { display: flex; align-items: center; gap: 10px; overflow-wrap: anywhere; }
|
||||
.provider-model-dialog__columns { display: grid; grid-template-columns: 1.2fr 1fr; gap: 28px; padding: 24px 0; border-block: 1px solid var(--border); }
|
||||
.provider-model-dialog__columns aside { border-left: 1px solid var(--border); padding-left: 24px; }
|
||||
.provider-model-dialog__columns { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(280px, .95fr); gap: 32px; padding: 22px 0 24px; border-block: 1px solid var(--border); }
|
||||
.provider-model-dialog__columns aside { border-left: 0; border-radius: 12px; background: color-mix(in srgb, var(--bg) 62%, transparent); padding: 18px 20px; }
|
||||
.provider-model-dialog section label { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; }
|
||||
.provider-model-dialog .mem-input, .provider-model-dialog .mem-select { width: 100%; box-sizing: border-box; font-size: 14px; min-height: 40px; }
|
||||
.provider-model-dialog section label .mem-input { grid-column: 1 / -1; }
|
||||
.provider-model-dialog p, .provider-model-dialog small { color: var(--fg-faint); line-height: 1.5; }
|
||||
.provider-model-dialog p { margin: 8px 0 20px; font-size: 12px; }
|
||||
.provider-model-dialog p { margin: 7px 0 16px; font-size: 12px; }
|
||||
.provider-model-dialog details { margin-top: 24px; }
|
||||
.provider-model-dialog summary { cursor: pointer; margin-bottom: 12px; }
|
||||
.provider-model-dialog dl { margin: 0; }
|
||||
@@ -127,6 +127,11 @@
|
||||
.provider-model-dialog__chips label { display: inline-flex; align-items: center; gap: 8px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.provider-model-dialog__chips input { accent-color: var(--accent); margin: 0; }
|
||||
.provider-model-dialog__chips label:has(input:disabled:not(:checked)) { color: var(--fg-faint); }
|
||||
.provider-model-dialog__effort-card { margin-top: 18px; padding: 16px; border: 1px solid var(--border); border-radius: 12px; background: color-mix(in srgb, var(--bg) 48%, transparent); }
|
||||
.provider-model-dialog__effort-card > label { margin-bottom: 9px; }
|
||||
.provider-model-dialog__effort-card .provider-model-dialog__chips { margin-bottom: 14px; }
|
||||
.provider-model-dialog__effort-card .provider-model-dialog__chips label { padding: 7px 9px; background: var(--bg-elev); }
|
||||
.provider-model-dialog__effort-card > p { margin-bottom: 0; }
|
||||
|
||||
.provider-model-dialog .mem-input, .provider-model-dialog .mem-select { background: var(--bg-elev); }
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import { ModalCloseButton } from "./ModalCloseButton";
|
||||
import type { ProviderModelCapabilityView } from "../lib/types";
|
||||
import { modelDraftError } from "../lib/providerModelDraft";
|
||||
|
||||
export interface ModelDetailsDraft { model: string; contextWindow: string; maxOutputTokens: number; vision: boolean | null; }
|
||||
export default function ProviderModelDialog({ initial, candidates, contextDefault, capability, baseURL, busy, onClose, onApply, onDelete }: {
|
||||
export interface ModelDetailsDraft { model: string; contextWindow: string; maxOutputTokens: number; vision: boolean | null; supportedEfforts: string[]; defaultEffort: string; }
|
||||
export default function ProviderModelDialog({ initial, candidates, contextDefault, capability, baseURL, busy, effortOptions, onClose, onApply, onDelete }: {
|
||||
initial?: ModelDetailsDraft; candidates: string[]; contextDefault?: number;
|
||||
effortOptions?: string[]; effortDefault?: string;
|
||||
capability?: ProviderModelCapabilityView; baseURL?: string; busy: boolean; onClose: () => void; onApply: (draft: ModelDetailsDraft) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
@@ -19,6 +20,8 @@ export default function ProviderModelDialog({ initial, candidates, contextDefaul
|
||||
const [context, setContext] = useState(initial?.contextWindow ?? "");
|
||||
const [output, setOutput] = useState(initial?.maxOutputTokens ? String(Math.max(-1, initial.maxOutputTokens)) : "");
|
||||
const [vision, setVision] = useState(initial?.vision == null ? "auto" : String(initial.vision));
|
||||
const [effort, setEffort] = useState(initial?.defaultEffort ?? "");
|
||||
const [selectedEfforts, setSelectedEfforts] = useState<string[]>(initial?.supportedEfforts ?? effortOptions ?? []);
|
||||
const [error, setError] = useState(false);
|
||||
useLayoutEffect(() => {
|
||||
const previous = document.activeElement as HTMLElement | null;
|
||||
@@ -29,9 +32,16 @@ export default function ProviderModelDialog({ initial, candidates, contextDefaul
|
||||
const validation = modelDraftError({model, context, output, vision: "auto"}, candidates, initial?.model);
|
||||
const imageBlocked = imageInputHardBlocked(baseURL, model, capability);
|
||||
const imageState = imageBlocked ? "unsupported" : imageInputState(vision === "auto" ? "auto" : vision === "true" ? "on" : "off", capability);
|
||||
const toggleEffort = (option: string, checked: boolean) => {
|
||||
setSelectedEfforts(current => {
|
||||
const next = checked ? [...current, option] : current.filter(item => item !== option);
|
||||
if (!checked && effort === option) setEffort("");
|
||||
return next;
|
||||
});
|
||||
};
|
||||
return createPortal(<dialog ref={dialog} className="provider-model-dialog" data-app-overlay="" aria-labelledby={titleId}
|
||||
onCancel={event => { event.preventDefault(); if (!busy) onClose(); }}>
|
||||
<form onSubmit={event => { event.preventDefault(); if (validation) { setError(true); return; } onApply({ model:model.trim(), contextWindow:context.trim(), maxOutputTokens:Number(output) || 0, vision:vision === "auto" ? null : !imageBlocked && vision === "true" }); }}>
|
||||
<form onSubmit={event => { event.preventDefault(); if (validation) { setError(true); return; } const defaultEffort = effort && selectedEfforts.includes(effort) ? effort : ""; onApply({ model:model.trim(), contextWindow:context.trim(), maxOutputTokens:Number(output) || 0, vision:vision === "auto" ? null : !imageBlocked && vision === "true", supportedEfforts: selectedEfforts, defaultEffort }); }}>
|
||||
<header><h2 id={titleId}>{t(initial ? "settings.modelDialog.edit" : "settings.models.add")}</h2><ModalCloseButton label={t("common.close")} disabled={busy} onClick={onClose}/></header>
|
||||
<label className="provider-model-dialog__id">{t("settings.modelDialog.id")}
|
||||
{initial ? <span><LockKeyhole size={16}/>{model}</span> : <input autoFocus className="mem-input" value={model} disabled={busy} onChange={e=>setModel(e.target.value)} placeholder="deepseek-v4-flash"/>}
|
||||
@@ -46,6 +56,20 @@ export default function ProviderModelDialog({ initial, candidates, contextDefaul
|
||||
<input id={`${titleId}-output`} className="mem-input" type="number" min={-1} value={output} disabled={busy} placeholder={t("settings.models.inherit")} onChange={e=>setOutput(e.target.value)}/>
|
||||
</label>
|
||||
<p>{t("settings.modelDialog.outputHint")}</p>
|
||||
{effortOptions && effortOptions.length > 0 && <div className="provider-model-dialog__effort-card">
|
||||
<label>{t("settings.modelDialog.reasoningEffortOptions")}
|
||||
<button type="button" className="btn provider-icon-action" title={t("settings.modelDialog.reset")} aria-label={t("settings.modelDialog.resetReasoningEffort")} disabled={busy} onClick={()=>{setEffort("");setSelectedEfforts([]);}}><RotateCcw size={16}/></button>
|
||||
</label>
|
||||
<div className="provider-model-dialog__chips">
|
||||
{effortOptions.map(option => <label key={option}><input type="checkbox" checked={selectedEfforts.includes(option)} disabled={busy} onChange={event=>toggleEffort(option, event.target.checked)}/>{option}</label>)}
|
||||
</div>
|
||||
<label htmlFor={`${titleId}-effort`}>{t("settings.modelDialog.reasoningEffortDefault")}</label>
|
||||
<select id={`${titleId}-effort`} className="mem-select" value={effort} disabled={busy || selectedEfforts.length === 0} onChange={event=>setEffort(event.target.value)}>
|
||||
<option value="">{t("settings.modelDialog.automatic")}</option>
|
||||
{selectedEfforts.map(option => <option key={option} value={option}>{option}</option>)}
|
||||
</select>
|
||||
<p>{t("settings.modelDialog.reasoningEffortHint")}</p>
|
||||
</div>}
|
||||
</section>
|
||||
<aside><h3>{t("settings.modelDialog.capabilities")}</h3>
|
||||
<div className="provider-model-dialog__capability-title">{t("settings.modelDialog.input")}</div>
|
||||
|
||||
@@ -6331,7 +6331,7 @@ export function ProviderEditor({
|
||||
setModelContextWindows(current => ({...current, [draft.model]:draft.contextWindow}));
|
||||
setModelOverrides(current => {
|
||||
const previous = current.find(item => item.model === draft.model);
|
||||
return [...current.filter(item => item.model !== draft.model), {...previous, model:draft.model, reasoningProtocol:previous?.reasoningProtocol ?? "", supportedEfforts:previous?.supportedEfforts ?? [], defaultEffort:previous?.defaultEffort ?? "", vision:draft.vision, maxOutputTokens:draft.maxOutputTokens}];
|
||||
return [...current.filter(item => item.model !== draft.model), {...previous, model:draft.model, reasoningProtocol:previous?.reasoningProtocol ?? "", supportedEfforts:draft.supportedEfforts, defaultEffort:draft.defaultEffort, vision:draft.vision, maxOutputTokens:draft.maxOutputTokens}];
|
||||
});
|
||||
setModelDialog(null);
|
||||
};
|
||||
@@ -6521,7 +6521,8 @@ export function ProviderEditor({
|
||||
{fetchFallback && <div role="alert" className="provider-fetch-status provider-fetch-status--warn">{fetchFallback}</div>}
|
||||
{modelDialog !== null && <Suspense fallback={null}><ProviderModelDialog
|
||||
baseURL={effectiveRequestUrl} candidates={modelCandidateNames} contextDefault={Number(ctx) || undefined}
|
||||
initial={modelDialog ? {model:modelDialog, contextWindow:modelContextWindows[modelDialog] ?? "", maxOutputTokens:modelOverrides.find(item=>item.model === modelDialog)?.maxOutputTokens ?? 0, vision:modelOverrides.find(item=>item.model === modelDialog)?.vision ?? null} : undefined}
|
||||
effortOptions={(modelDialog && modelOverrides.find(item=>item.model === modelDialog)?.supportedEfforts?.length ? (modelOverrides.find(item=>item.model === modelDialog)?.supportedEfforts ?? []) : (supportedEfforts ?? [])).filter(Boolean)}
|
||||
initial={modelDialog ? (() => { const override = modelOverrides.find(item=>item.model === modelDialog); return {model:modelDialog, contextWindow:modelContextWindows[modelDialog] ?? "", maxOutputTokens:override?.maxOutputTokens ?? 0, vision:override?.vision ?? null, supportedEfforts:override?.supportedEfforts ?? supportedEfforts, defaultEffort:override?.defaultEffort ?? ""}; })() : undefined}
|
||||
capability={modelCapabilities.find(item=>item.model === modelDialog)} busy={busy || fetchingModels}
|
||||
onClose={()=>setModelDialog(null)} onApply={applyModelDetails} onDelete={deleteModel}/></Suspense>}
|
||||
<ProviderEditorModelPicker
|
||||
|
||||
@@ -2592,6 +2592,12 @@ export const en = {
|
||||
"settings.modelDialog.resetOutput": "Reset output limit",
|
||||
"settings.modelDialog.outputLimit": "Output limit (tokens)",
|
||||
"settings.modelDialog.outputHint": "Blank inherits; -1 omits the optional request limit. Model limits still apply.",
|
||||
"settings.modelDialog.reasoningEffort": "Reasoning effort",
|
||||
"settings.modelDialog.reasoningEffortOptions": "Available reasoning levels",
|
||||
"settings.modelDialog.reasoningEffortDefault": "Default reasoning level",
|
||||
"settings.modelDialog.resetReasoningEffort": "Reset reasoning effort",
|
||||
"settings.modelDialog.automatic": "Automatic",
|
||||
"settings.modelDialog.reasoningEffortHint": "Automatic inherits the provider default. Higher levels may use more time and tokens.",
|
||||
"settings.modelDialog.overrideHint": "Overrides the capability declaration, not what the model can actually do.",
|
||||
"settings.modelDialog.capabilities": "Model capabilities",
|
||||
"settings.modelDialog.input": "Input",
|
||||
|
||||
@@ -1867,6 +1867,12 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"settings.modelDialog.resetOutput": "重設輸出上限",
|
||||
"settings.modelDialog.outputLimit": "輸出上限(Token)",
|
||||
"settings.modelDialog.outputHint": "留空繼承;-1 不傳送可選輸出限制,仍受模型實際限制。",
|
||||
"settings.modelDialog.reasoningEffort": "思考強度",
|
||||
"settings.modelDialog.reasoningEffortOptions": "可用思考檔位",
|
||||
"settings.modelDialog.reasoningEffortDefault": "預設思考檔位",
|
||||
"settings.modelDialog.resetReasoningEffort": "恢復自動思考強度",
|
||||
"settings.modelDialog.automatic": "自動",
|
||||
"settings.modelDialog.reasoningEffortHint": "自動繼承服務商預設值。更高檔位可能消耗更多時間和 token。",
|
||||
"settings.modelDialog.overrideHint": "僅覆寫能力宣告,不會改變模型實際能力。",
|
||||
"settings.modelDialog.capabilities": "模型能力",
|
||||
"settings.modelDialog.input": "輸入",
|
||||
|
||||
@@ -2595,6 +2595,12 @@ export const zh: Record<DictKey, string> = {
|
||||
"settings.modelDialog.resetOutput": "重置输出上限",
|
||||
"settings.modelDialog.outputLimit": "输出上限(Token)",
|
||||
"settings.modelDialog.outputHint": "留空继承;-1 不发送可选输出限制,仍受模型实际限制。",
|
||||
"settings.modelDialog.reasoningEffort": "思考强度",
|
||||
"settings.modelDialog.reasoningEffortOptions": "可用思考档位",
|
||||
"settings.modelDialog.reasoningEffortDefault": "默认思考档位",
|
||||
"settings.modelDialog.resetReasoningEffort": "恢复自动思考强度",
|
||||
"settings.modelDialog.automatic": "自动",
|
||||
"settings.modelDialog.reasoningEffortHint": "自动继承服务商默认值。更高档位可能消耗更多时间和 token。",
|
||||
"settings.modelDialog.overrideHint": "仅覆盖能力声明,不会改变模型实际能力。",
|
||||
"settings.modelDialog.capabilities": "模型能力",
|
||||
"settings.modelDialog.input": "输入",
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
func deepSeekV4EffortOverrides() map[string]ProviderModelOverride {
|
||||
flash := ProviderModelOverride{SupportedEfforts: []string{"disabled", "low", "high", "max"}, DefaultEffort: "high"}
|
||||
return map[string]ProviderModelOverride{
|
||||
"deepseek-flash": flash,
|
||||
"deepseek-v4-flash": flash,
|
||||
openai.OfficialDeepSeekVisionModel: flash,
|
||||
"deepseek-v4-pro": {SupportedEfforts: []string{"disabled", "low", "high", "max"}, DefaultEffort: "high"},
|
||||
|
||||
@@ -103,16 +103,8 @@ var (
|
||||
longCat20Models = []string{"LongCat-2.0"}
|
||||
deepSeekOfficialModels = []string{"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", openai.OfficialDeepSeekVisionModel}
|
||||
deepSeekOfficialVisionModels = []string{openai.OfficialDeepSeekVisionModel}
|
||||
tokenRhythmModels = []string{
|
||||
"deepseek-v4-flash", "deepseek-v4-pro", "glm-5", "glm-5.1",
|
||||
"minimax-m2.7", "kimi-k2.5", "kimi-k2.6", "minimax-m2.5",
|
||||
"mimo-v2.5-pro", "qwen3.7-max", "kimi-k2.7-code", "glm-5.2",
|
||||
"qwen3.8-max", "deepseek-v4-flash-0731",
|
||||
}
|
||||
tokenRhythmVisionModels = []string{"kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code"}
|
||||
|
||||
mimoV25Models = []string{"mimo-v2.5-pro", "mimo-v2.5"}
|
||||
mimoV25VisionModels = []string{"mimo-v2.5"}
|
||||
mimoV25Models = []string{"mimo-v2.5-pro", "mimo-v2.5"}
|
||||
mimoV25VisionModels = []string{"mimo-v2.5"}
|
||||
|
||||
minimaxMSeriesModels = []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"}
|
||||
minimaxMSeriesVisionModels = []string{"MiniMax-M3"}
|
||||
@@ -165,48 +157,6 @@ func qwenModelContextOverrides() map[string]ProviderModelOverride {
|
||||
}
|
||||
}
|
||||
|
||||
func tokenRhythmModelOverrides() map[string]ProviderModelOverride {
|
||||
return map[string]ProviderModelOverride{
|
||||
"deepseek-v4-flash": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "low", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"deepseek-v4-pro": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"deepseek-v4-flash-0731": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "low", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"glm-5": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
},
|
||||
"glm-5.1": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
ContextWindow: 200_000,
|
||||
},
|
||||
"glm-5.2": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
},
|
||||
"minimax-m2.7": {ContextWindow: 200_000},
|
||||
"kimi-k2.5": {ContextWindow: 256_000},
|
||||
"kimi-k2.6": {ContextWindow: 256_000},
|
||||
"minimax-m2.5": {ContextWindow: 200_000},
|
||||
"mimo-v2.5-pro": {ContextWindow: 256_000},
|
||||
"kimi-k2.7-code": {ContextWindow: 256_000},
|
||||
}
|
||||
}
|
||||
|
||||
func kimiK3DirectOverride() ProviderModelOverride {
|
||||
return ProviderModelOverride{
|
||||
ReasoningProtocol: ReasoningProtocolOpenAI,
|
||||
|
||||
@@ -438,52 +438,6 @@ func TestCuratedProviderPresetsHideRedundantDeepSeekAnthropicPreset(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRhythmPresetMatchesPublicAPIIntegration(t *testing.T) {
|
||||
preset, ok := CuratedProviderPreset("token-rhythm")
|
||||
if !ok || len(preset.Entries) != 1 {
|
||||
t.Fatalf("Token Rhythm preset = %+v, want one entry", preset)
|
||||
}
|
||||
if preset.Label != "Token Rhythm" || preset.KeyEnv != "TOKEN_RHYTHM_API_KEY" {
|
||||
t.Fatalf("Token Rhythm identity = label %q key %q", preset.Label, preset.KeyEnv)
|
||||
}
|
||||
entry := preset.Entries[0]
|
||||
if entry.Kind != "openai" || entry.BaseURL != "https://tokenrhythm.studio/v1" || entry.ModelsURL != "https://tokenrhythm.studio/v1/models" {
|
||||
t.Fatalf("Token Rhythm endpoint mismatch: %+v", entry)
|
||||
}
|
||||
if entry.DefaultModel() != "deepseek-v4-flash" || !entry.HasModel("qwen3.8-max") || entry.HasModel("qwen-image-2.0") {
|
||||
t.Fatalf("Token Rhythm chat catalog mismatch: models=%v default=%q", entry.Models, entry.DefaultModel())
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := cfg.UpsertProvider(entry); err != nil {
|
||||
t.Fatalf("upsert Token Rhythm preset: %v", err)
|
||||
}
|
||||
deepseek, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash")
|
||||
if !ok || deepseek.ContextWindow != 1_000_000 || ReasoningProtocolForEntry(deepseek) != ReasoningProtocolDeepSeek {
|
||||
t.Fatalf("Token Rhythm DeepSeek capability mismatch: %+v", deepseek)
|
||||
}
|
||||
kimi, ok := cfg.ResolveModel("token-rhythm/kimi-k2.7-code")
|
||||
if !ok || kimi.ContextWindow != 256_000 || !EffectiveVision(kimi) {
|
||||
t.Fatalf("Token Rhythm Kimi capability mismatch: %+v", kimi)
|
||||
}
|
||||
glm, ok := cfg.ResolveModel("token-rhythm/glm-5.1")
|
||||
if !ok || glm.ContextWindow != 200_000 || EffectiveVision(glm) || ReasoningProtocolForEntry(glm) != ReasoningProtocolGLM {
|
||||
t.Fatalf("Token Rhythm GLM capability mismatch: %+v", glm)
|
||||
}
|
||||
glmCap := EffortCapabilityForEntry(glm)
|
||||
if !glmCap.Supported || glmCap.Default != "enabled" || !stringSlicesEqual(glmCap.Levels, []string{"auto", "enabled", "disabled"}) {
|
||||
t.Fatalf("Token Rhythm GLM effort mismatch: %+v", glmCap)
|
||||
}
|
||||
flash0731, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash-0731")
|
||||
if !ok || ReasoningProtocolForEntry(flash0731) != ReasoningProtocolDeepSeek {
|
||||
t.Fatalf("Token Rhythm DeepSeek 0731 protocol mismatch: %+v", flash0731)
|
||||
}
|
||||
flashCap := EffortCapabilityForEntry(flash0731)
|
||||
if !flashCap.Supported || flashCap.Default != "high" || !stringSlicesEqual(flashCap.Levels, []string{"auto", "disabled", "low", "high", "max"}) {
|
||||
t.Fatalf("Token Rhythm DeepSeek 0731 effort mismatch: %+v", flashCap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCuratedProviderPresetsStepFunUsesOfficialBaseURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
id string
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
var tokenRhythmModels = []string{
|
||||
"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", "glm-5", "glm-5.1",
|
||||
"minimax-m2.7", "kimi-k2.5", "kimi-k2.6", "minimax-m2.5",
|
||||
"mimo-v2.5-pro", "qwen3.7-max", "kimi-k2.7-code", "glm-5.2",
|
||||
"qwen3.8-max", "deepseek-v4-flash-0731",
|
||||
}
|
||||
|
||||
var tokenRhythmVisionModels = []string{"kimi-k2.5", "kimi-k2.6", "kimi-k2.7-code"}
|
||||
|
||||
func tokenRhythmModelOverrides() map[string]ProviderModelOverride {
|
||||
return map[string]ProviderModelOverride{
|
||||
"deepseek-flash": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "low", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"deepseek-v4-flash": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "low", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"deepseek-v4-pro": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"deepseek-v4-flash-0731": {
|
||||
ReasoningProtocol: ReasoningProtocolDeepSeek,
|
||||
SupportedEfforts: []string{"disabled", "low", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
},
|
||||
"glm-5": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
},
|
||||
"glm-5.1": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
ContextWindow: 200_000,
|
||||
},
|
||||
"glm-5.2": {
|
||||
ReasoningProtocol: ReasoningProtocolGLM,
|
||||
SupportedEfforts: []string{"enabled", "disabled"},
|
||||
DefaultEffort: "enabled",
|
||||
},
|
||||
"minimax-m2.7": {ContextWindow: 200_000},
|
||||
"kimi-k2.5": {ContextWindow: 256_000},
|
||||
"kimi-k2.6": {ContextWindow: 256_000},
|
||||
"minimax-m2.5": {ContextWindow: 200_000},
|
||||
"mimo-v2.5-pro": {ContextWindow: 256_000},
|
||||
"kimi-k2.7-code": {ContextWindow: 256_000},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTokenRhythmPresetMatchesPublicAPIIntegration(t *testing.T) {
|
||||
preset, ok := CuratedProviderPreset("token-rhythm")
|
||||
if !ok || len(preset.Entries) != 1 {
|
||||
t.Fatalf("Token Rhythm preset = %+v, want one entry", preset)
|
||||
}
|
||||
if preset.Label != "Token Rhythm" || preset.KeyEnv != "TOKEN_RHYTHM_API_KEY" {
|
||||
t.Fatalf("Token Rhythm identity = label %q key %q", preset.Label, preset.KeyEnv)
|
||||
}
|
||||
entry := preset.Entries[0]
|
||||
if entry.Kind != "openai" || entry.BaseURL != "https://tokenrhythm.studio/v1" || entry.ModelsURL != "https://tokenrhythm.studio/v1/models" {
|
||||
t.Fatalf("Token Rhythm endpoint mismatch: %+v", entry)
|
||||
}
|
||||
if entry.DefaultModel() != "deepseek-v4-flash" || !entry.HasModel("qwen3.8-max") || entry.HasModel("qwen-image-2.0") {
|
||||
t.Fatalf("Token Rhythm chat catalog mismatch: models=%v default=%q", entry.Models, entry.DefaultModel())
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := cfg.UpsertProvider(entry); err != nil {
|
||||
t.Fatalf("upsert Token Rhythm preset: %v", err)
|
||||
}
|
||||
deepseek, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash")
|
||||
if !ok || deepseek.ContextWindow != 1_000_000 || ReasoningProtocolForEntry(deepseek) != ReasoningProtocolDeepSeek {
|
||||
t.Fatalf("Token Rhythm DeepSeek capability mismatch: %+v", deepseek)
|
||||
}
|
||||
shortFlash, ok := cfg.ResolveModel("token-rhythm/deepseek-flash")
|
||||
if !ok || ReasoningProtocolForEntry(shortFlash) != ReasoningProtocolDeepSeek {
|
||||
t.Fatalf("Token Rhythm DeepSeek short alias capability mismatch: %+v", shortFlash)
|
||||
}
|
||||
shortFlashCap := EffortCapabilityForEntry(shortFlash)
|
||||
if !shortFlashCap.Supported || shortFlashCap.Default != "high" || !stringSlicesEqual(shortFlashCap.Levels, []string{"auto", "disabled", "low", "high", "max"}) {
|
||||
t.Fatalf("Token Rhythm DeepSeek short alias effort mismatch: %+v", shortFlashCap)
|
||||
}
|
||||
kimi, ok := cfg.ResolveModel("token-rhythm/kimi-k2.7-code")
|
||||
if !ok || kimi.ContextWindow != 256_000 || !EffectiveVision(kimi) {
|
||||
t.Fatalf("Token Rhythm Kimi capability mismatch: %+v", kimi)
|
||||
}
|
||||
glm, ok := cfg.ResolveModel("token-rhythm/glm-5.1")
|
||||
if !ok || glm.ContextWindow != 200_000 || EffectiveVision(glm) || ReasoningProtocolForEntry(glm) != ReasoningProtocolGLM {
|
||||
t.Fatalf("Token Rhythm GLM capability mismatch: %+v", glm)
|
||||
}
|
||||
glmCap := EffortCapabilityForEntry(glm)
|
||||
if !glmCap.Supported || glmCap.Default != "enabled" || !stringSlicesEqual(glmCap.Levels, []string{"auto", "enabled", "disabled"}) {
|
||||
t.Fatalf("Token Rhythm GLM effort mismatch: %+v", glmCap)
|
||||
}
|
||||
flash0731, ok := cfg.ResolveModel("token-rhythm/deepseek-v4-flash-0731")
|
||||
if !ok || ReasoningProtocolForEntry(flash0731) != ReasoningProtocolDeepSeek {
|
||||
t.Fatalf("Token Rhythm DeepSeek 0731 protocol mismatch: %+v", flash0731)
|
||||
}
|
||||
flashCap := EffortCapabilityForEntry(flash0731)
|
||||
if !flashCap.Supported || flashCap.Default != "high" || !stringSlicesEqual(flashCap.Levels, []string{"auto", "disabled", "low", "high", "max"}) {
|
||||
t.Fatalf("Token Rhythm DeepSeek 0731 effort mismatch: %+v", flashCap)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func OpenCodeGoRecommendedRoute(model string) (string, bool) {
|
||||
|
||||
func OpenCodeGoDeepSeekModel(model string) bool {
|
||||
switch strings.TrimSpace(model) {
|
||||
case "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp":
|
||||
case "deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -9,7 +9,7 @@ func TestOpenCodeGoContractUsesExactEffectiveEndpointAndModel(t *testing.T) {
|
||||
for _, kind := range []string{"openai", "anthropic", "responses"} {
|
||||
endpoint := map[string]string{"openai": "chat/completions", "anthropic": "messages", "responses": "responses"}[kind]
|
||||
url := "https://opencode.ai/zen/go/v1/" + endpoint
|
||||
for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"} {
|
||||
for _, model := range []string{"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"} {
|
||||
c, ok := LookupOpenCodeGoContract(kind, "https://custom.example", url, "", model)
|
||||
if !ok || c.RecommendedRoute != OpenCodeGoRouteChat || c.Reasoning.Default != "high" || c.Reasoning.Validate(model, "max") != nil {
|
||||
t.Fatalf("%s %s: %+v %v", kind, model, c, ok)
|
||||
|
||||
@@ -34,7 +34,7 @@ func OpenCodeGoChatModels() map[string]OpenCodeGoModelLimits {
|
||||
"glm-5.3": {Context: 1_000_000, MaxOutput: 131_072}, "glm-5.2": {Context: 1_000_000, MaxOutput: 131_072},
|
||||
"glm-5.1": {Context: 202_752, MaxOutput: 32_768}, "kimi-k3": {Context: 1_048_576, MaxOutput: 131_072},
|
||||
"kimi-k2.7-code": {Context: 262_144, MaxOutput: 262_144}, "kimi-k2.6": {Context: 262_144, MaxOutput: 65_536},
|
||||
"deepseek-v4-pro": {Context: 1_000_000, MaxOutput: 384_000}, "deepseek-v4-flash": {Context: 1_000_000, MaxOutput: 384_000},
|
||||
"deepseek-flash": {Context: 1_000_000, MaxOutput: 384_000}, "deepseek-v4-pro": {Context: 1_000_000, MaxOutput: 384_000}, "deepseek-v4-flash": {Context: 1_000_000, MaxOutput: 384_000},
|
||||
"deepseek-v4-flash-vision-exp": {Context: 1_000_000, MaxOutput: 384_000}, "mimo-v2.5-pro": {Context: 1_048_576, MaxOutput: 128_000},
|
||||
"mimo-v2.5": {Context: 1_000_000, MaxOutput: 128_000}, "hy3": {Context: 256_000, MaxOutput: 64_000},
|
||||
})
|
||||
|
||||
@@ -58,16 +58,16 @@ func TestOpenCodeGoResponsesModelsMatchPinnedLimits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFilterOfficialOpenCodeGoModelsKeepsOnlyRouteCompatibleModels(t *testing.T) {
|
||||
all := []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-v4-flash", "unknown-future-model"}
|
||||
all := []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash", "unknown-future-model"}
|
||||
tests := []struct {
|
||||
name string
|
||||
kind string
|
||||
baseURL string
|
||||
want []string
|
||||
}{
|
||||
{name: "chat", kind: "openai", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-v4-flash"}},
|
||||
{name: "anthropic", kind: "anthropic", baseURL: "https://opencode.ai/zen/go", want: []string{"qwen3.8-max", "qwen3.7-plus", "deepseek-v4-flash"}},
|
||||
{name: "responses", kind: "responses", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "deepseek-v4-flash"}},
|
||||
{name: "chat", kind: "openai", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash"}},
|
||||
{name: "anthropic", kind: "anthropic", baseURL: "https://opencode.ai/zen/go", want: []string{"qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash"}},
|
||||
{name: "responses", kind: "responses", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "deepseek-flash", "deepseek-v4-flash"}},
|
||||
{name: "custom endpoint is untouched", kind: "anthropic", baseURL: "https://relay.example/zen/go", want: all},
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user