dsh-model-extension 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
- var __extCss = ".J84-9a_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.J84-9a_titleRow{align-items:center;gap:10px;display:flex}.J84-9a_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.J84-9a_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.J84-9a_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.J84-9a_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.J84-9a_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.J84-9a_rowCard{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.J84-9a_rowHead{align-items:center;gap:10px;display:flex}.J84-9a_rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}.J84-9a_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.J84-9a_rowTag{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.J84-9a_credentialDot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.J84-9a_credentialDotConfigured{background:var(--dsw-alias-state-success-primary)}.J84-9a_credentialDotMissing{background:var(--dsw-alias-state-error-primary)}.J84-9a_rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.J84-9a_primaryButton,.J84-9a_secondaryButton,.J84-9a_addButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.J84-9a_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.J84-9a_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.J84-9a_secondaryButton,.J84-9a_addButton{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);background:0 0}.J84-9a_secondaryButton:hover:not(:disabled),.J84-9a_addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.J84-9a_dangerButton{box-sizing:border-box;height:36px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.J84-9a_dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.J84-9a_rowActions .J84-9a_secondaryButton,.J84-9a_rowActions .J84-9a_dangerButton{border-radius:14px;height:28px;padding:0 10px;font-size:12px;line-height:18px}.J84-9a_primaryButton:disabled,.J84-9a_secondaryButton:disabled,.J84-9a_dangerButton:disabled,.J84-9a_addButton:disabled,.J84-9a_linkButton:disabled,.J84-9a_addModelButton:disabled{opacity:.4;cursor:default}.J84-9a_primaryButton:focus-visible,.J84-9a_secondaryButton:focus-visible,.J84-9a_dangerButton:focus-visible,.J84-9a_addButton:focus-visible,.J84-9a_linkButton:focus-visible,.J84-9a_addModelButton:focus-visible,.J84-9a_iconButton:focus-visible,.J84-9a_customizedSummary:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.J84-9a_editor{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}.J84-9a_field{flex-direction:column;gap:6px;display:flex}.J84-9a_fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}.J84-9a_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.J84-9a_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.J84-9a_advancedHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.J84-9a_editorActions{justify-content:flex-end;gap:8px;display:flex}.J84-9a_addBlock{flex-direction:column;gap:12px;display:flex}.J84-9a_addActions{flex-wrap:wrap;gap:10px;display:flex}.J84-9a_addButton{border:1px dashed var(--dsw-alias-border-l3);border-radius:16px;flex:1 1 0;gap:6px;min-width:180px;height:44px}.J84-9a_addCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;list-style:none;display:flex}.J84-9a_addCard .J84-9a_editor,.J84-9a_setupCard .J84-9a_editor{background:0 0;padding:0}.J84-9a_tabs{flex-direction:column;gap:12px;display:flex}.J84-9a_tabBar{border-bottom:.5px solid var(--dsw-alias-border-l2);gap:4px;display:flex}.J84-9a_tabButton{appearance:none;font:inherit;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:6px 12px 8px;font-size:13px;line-height:20px;position:relative}.J84-9a_tabButton:hover{color:var(--dsw-alias-label-primary)}.J84-9a_tabButton:disabled{opacity:.4;cursor:default}.J84-9a_tabButtonActive{color:var(--dsw-alias-label-primary);font-weight:500}.J84-9a_tabButtonActive:after{content:\"\";background:var(--dsw-alias-brand-primary);border-radius:2px;height:2px;position:absolute;bottom:-1px;left:10px;right:10px}.J84-9a_tabPanel{flex-direction:column;gap:12px;display:flex}.J84-9a_modelCatalog{flex-direction:column;gap:10px;display:flex}.J84-9a_modelCatalogHeading{flex-direction:column;gap:2px;display:flex}.J84-9a_modelCatalogTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.J84-9a_modelCatalogMeta,.J84-9a_modelEmpty{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.J84-9a_modelList{flex-direction:column;gap:8px;display:flex}.J84-9a_modelListHead{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.J84-9a_modelEntry{border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;padding:6px}.J84-9a_modelRow{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) auto auto;align-items:center;gap:6px;display:grid}.J84-9a_iconButton{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.J84-9a_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.J84-9a_iconButton:disabled{cursor:default;opacity:.4}.J84-9a_iconButtonDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.J84-9a_modelAdvanced{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;padding:8px 4px 2px;display:grid}.J84-9a_modelField{flex-direction:column;gap:4px;display:flex}.J84-9a_modelFieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_modelEmpty{border:1px dashed var(--dsw-alias-border-l3);text-align:center;border-radius:8px;padding:12px}.J84-9a_addModelButton{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;align-self:flex-start;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.J84-9a_addModelButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_input{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}select.J84-9a_input{cursor:pointer;max-width:240px}.J84-9a_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.J84-9a_input::placeholder{color:var(--dsw-alias-label-dimmed)}.J84-9a_input:disabled{opacity:.6;cursor:default}.J84-9a_selectInput{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 12px center;background-repeat:no-repeat;background-size:12px 12px;padding-right:32px}.J84-9a_deleteDialog{width:min(480px,100%)}.J84-9a_deleteConfirm:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.J84-9a_deleteConfirm:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.J84-9a_fetchDialog{max-width:520px}.J84-9a_candidateToolbar{align-items:center;gap:8px;margin-bottom:6px;display:flex}.J84-9a_candidateSearch{flex:240px;min-width:0}.J84-9a_candidateList{flex-direction:column;gap:2px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}.J84-9a_candidate{border-radius:6px}.J84-9a_candidateLabel{cursor:pointer;align-items:center;gap:8px;padding:6px 8px;display:flex}.J84-9a_candidateId{overflow-wrap:anywhere;flex:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}.J84-9a_candidateEmpty{color:var(--dsw-alias-label-secondary);text-align:center;margin:24px 0;font-size:13px;line-height:20px}[hidden]{display:none!important}.J84-9a_extBlock{border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:10px;margin:8px 4px 2px;padding:10px;display:flex}.J84-9a_extGroup{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}.J84-9a_extGroupFirst{border-top:none;padding-top:0}.J84-9a_extCheck{cursor:pointer;align-items:flex-start;gap:8px;display:flex}.J84-9a_extCheckLabel{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}.J84-9a_extHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_extGated{flex-direction:column;gap:12px;padding-left:2px;display:flex}.J84-9a_extField{flex-direction:column;gap:6px;display:flex}.J84-9a_extLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_extTable{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.J84-9a_extTHead{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);grid-template-columns:64px minmax(0,1fr) minmax(0,1.2fr);font-size:12px;line-height:18px;display:grid}.J84-9a_extTHead>div{padding:6px 10px}.J84-9a_extTHead>div+div{border-left:.5px solid var(--dsw-alias-border-l2)}.J84-9a_extTRow{border-top:.5px solid var(--dsw-alias-border-l2);grid-template-columns:64px minmax(0,1fr) minmax(0,1.2fr);align-items:center;display:grid}.J84-9a_extTRow>div{padding:6px 10px;font-size:14px}.J84-9a_extTRow>div+div{border-left:.5px solid var(--dsw-alias-border-l2)}.J84-9a_extTRow input[type=text]{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px}.J84-9a_extTRow input[type=text]:disabled{opacity:.6;cursor:default}.J84-9a_extCheckbox{accent-color:var(--dsw-alias-brand-primary);flex:none;width:15px;height:15px}.J84-9a_switchRow{cursor:pointer;align-items:center;gap:8px;font-size:14px;line-height:22px;display:flex}.J84-9a_extTag{background:var(--dsw-static-deepseek-100);color:var(--dsw-static-deepseek-600);border:.5px solid #4176e659;border-radius:4px;flex:none;padding:1px 6px;font-size:11px;font-weight:500;line-height:16px}.J84-9a_modalMask{background:var(--dsw-alias-bg-mask-1);z-index:100;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.J84-9a_modal{background:var(--dsw-specific-menu);border-radius:16px;width:100%;padding:20px;box-shadow:0 12px 40px #0000002e}.J84-9a_modalTitle{color:var(--dsw-alias-label-primary);margin-bottom:8px;font-size:16px;font-weight:500;line-height:24px}.J84-9a_modalDesc{color:var(--dsw-alias-label-secondary);margin-bottom:16px;font-size:14px;line-height:22px}.J84-9a_modalFoot{justify-content:flex-end;gap:8px;display:flex}.J84-9a_quickWrap{flex-direction:column;gap:6px;display:flex;position:relative}.J84-9a_quickList{z-index:20;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-specific-menu);border-radius:8px;max-height:264px;margin-top:2px;padding:4px;position:absolute;top:100%;left:0;right:0;overflow-y:auto;box-shadow:0 8px 24px #0000001f}.J84-9a_quickItem{width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;overflow-wrap:anywhere;background:0 0;border:none;border-radius:6px;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;display:block}.J84-9a_quickItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_quickEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 8px;font-size:12px;line-height:18px}@media (prefers-reduced-motion:reduce){.J84-9a_customizedSummary:before{transition:none}}\n";
6
+ var __extCss = ".J84-9a_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.J84-9a_titleRow{align-items:center;gap:10px;display:flex}.J84-9a_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.J84-9a_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.J84-9a_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.J84-9a_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.J84-9a_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.J84-9a_rowCard{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.J84-9a_rowHead{align-items:center;gap:10px;display:flex}.J84-9a_rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}.J84-9a_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.J84-9a_rowTag{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.J84-9a_credentialDot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.J84-9a_credentialDotConfigured{background:var(--dsw-alias-state-success-primary)}.J84-9a_credentialDotMissing{background:var(--dsw-alias-state-error-primary)}.J84-9a_rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.J84-9a_primaryButton,.J84-9a_secondaryButton,.J84-9a_addButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.J84-9a_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.J84-9a_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.J84-9a_secondaryButton,.J84-9a_addButton{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);background:0 0}.J84-9a_secondaryButton:hover:not(:disabled),.J84-9a_addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.J84-9a_dangerButton{box-sizing:border-box;height:36px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.J84-9a_dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.J84-9a_rowActions .J84-9a_secondaryButton,.J84-9a_rowActions .J84-9a_dangerButton{border-radius:14px;height:28px;padding:0 10px;font-size:12px;line-height:18px}.J84-9a_primaryButton:disabled,.J84-9a_secondaryButton:disabled,.J84-9a_dangerButton:disabled,.J84-9a_addButton:disabled,.J84-9a_linkButton:disabled,.J84-9a_addModelButton:disabled{opacity:.4;cursor:default}.J84-9a_primaryButton:focus-visible,.J84-9a_secondaryButton:focus-visible,.J84-9a_dangerButton:focus-visible,.J84-9a_addButton:focus-visible,.J84-9a_linkButton:focus-visible,.J84-9a_addModelButton:focus-visible,.J84-9a_iconButton:focus-visible,.J84-9a_customizedSummary:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.J84-9a_editor{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}.J84-9a_field{flex-direction:column;gap:6px;display:flex}.J84-9a_fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}.J84-9a_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.J84-9a_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.J84-9a_advancedHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.J84-9a_editorActions{justify-content:flex-end;gap:8px;display:flex}.J84-9a_addBlock{flex-direction:column;gap:12px;display:flex}.J84-9a_addActions{flex-wrap:wrap;gap:10px;display:flex}.J84-9a_addButton{border:1px dashed var(--dsw-alias-border-l3);border-radius:16px;flex:1 1 0;gap:6px;min-width:180px;height:44px}.J84-9a_addCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;list-style:none;display:flex}.J84-9a_addCard .J84-9a_editor,.J84-9a_setupCard .J84-9a_editor{background:0 0;padding:0}.J84-9a_tabs{flex-direction:column;gap:12px;display:flex}.J84-9a_tabBar{border-bottom:.5px solid var(--dsw-alias-border-l2);gap:4px;display:flex}.J84-9a_tabButton{appearance:none;font:inherit;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:6px 12px 8px;font-size:13px;line-height:20px;position:relative}.J84-9a_tabButton:hover{color:var(--dsw-alias-label-primary)}.J84-9a_tabButton:disabled{opacity:.4;cursor:default}.J84-9a_tabButtonActive{color:var(--dsw-alias-label-primary);font-weight:500}.J84-9a_tabButtonActive:after{content:\"\";background:var(--dsw-alias-brand-primary);border-radius:2px;height:2px;position:absolute;bottom:-1px;left:10px;right:10px}.J84-9a_tabPanel{flex-direction:column;gap:12px;display:flex}.J84-9a_modelCatalog{flex-direction:column;gap:10px;display:flex}.J84-9a_modelCatalogHeading{flex-direction:column;gap:2px;display:flex}.J84-9a_modelCatalogTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.J84-9a_modelCatalogMeta,.J84-9a_modelEmpty{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.J84-9a_modelList{flex-direction:column;gap:8px;display:flex}.J84-9a_modelListHead{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.J84-9a_modelEntry{border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;padding:6px}.J84-9a_modelRow{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) auto auto;align-items:center;gap:6px;display:grid}.J84-9a_iconButton{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.J84-9a_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.J84-9a_iconButton:disabled{cursor:default;opacity:.4}.J84-9a_iconButtonDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.J84-9a_modelAdvanced{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;padding:8px 4px 2px;display:grid}.J84-9a_modelField{flex-direction:column;gap:4px;display:flex}.J84-9a_modelFieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_modelEmpty{border:1px dashed var(--dsw-alias-border-l3);text-align:center;border-radius:8px;padding:12px}.J84-9a_addModelButton{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;align-self:flex-start;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.J84-9a_addModelButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_input{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}select.J84-9a_input{cursor:pointer;max-width:240px}.J84-9a_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.J84-9a_input::placeholder{color:var(--dsw-alias-label-dimmed)}.J84-9a_input:disabled{opacity:.6;cursor:default}.J84-9a_selectInput{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 12px center;background-repeat:no-repeat;background-size:12px 12px;padding-right:32px}.J84-9a_deleteDialog{width:min(480px,100%)}.J84-9a_deleteConfirm:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.J84-9a_deleteConfirm:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.J84-9a_fetchDialog{max-width:520px}.J84-9a_candidateToolbar{align-items:center;gap:8px;margin-bottom:6px;display:flex}.J84-9a_candidateSearch{flex:240px;min-width:0}.J84-9a_candidateList{flex-direction:column;gap:2px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}.J84-9a_candidate{border-radius:6px}.J84-9a_candidateLabel{cursor:pointer;align-items:center;gap:8px;padding:6px 8px;display:flex}.J84-9a_candidateId{overflow-wrap:anywhere;flex:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}.J84-9a_candidateEmpty{color:var(--dsw-alias-label-secondary);text-align:center;margin:24px 0;font-size:13px;line-height:20px}[hidden]{display:none!important}.J84-9a_extBlock{border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:10px;margin:8px 4px 2px;padding:10px;display:flex}.J84-9a_extGroup{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}.J84-9a_extGroupFirst{border-top:none;padding-top:0}.J84-9a_extCheck{cursor:pointer;align-items:flex-start;gap:8px;display:flex}.J84-9a_extCheckLabel{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}.J84-9a_extHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_extGated{flex-direction:column;gap:12px;padding-left:2px;display:flex}.J84-9a_extField{flex-direction:column;gap:6px;display:flex}.J84-9a_extLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.J84-9a_extTable{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.J84-9a_extTHead{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);grid-template-columns:64px minmax(0,1fr) minmax(0,1.2fr);font-size:12px;line-height:18px;display:grid}.J84-9a_extTHead>div{padding:6px 10px}.J84-9a_extTHead>div+div{border-left:.5px solid var(--dsw-alias-border-l2)}.J84-9a_extTRow{border-top:.5px solid var(--dsw-alias-border-l2);grid-template-columns:64px minmax(0,1fr) minmax(0,1.2fr);align-items:center;display:grid}.J84-9a_extTRow>div{padding:6px 10px;font-size:14px}.J84-9a_extTRow>div+div{border-left:.5px solid var(--dsw-alias-border-l2)}.J84-9a_extTRow input[type=text]{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px}.J84-9a_extTRow input[type=text]:disabled{opacity:.6;cursor:default}.J84-9a_extCheckbox{accent-color:var(--dsw-alias-brand-primary);flex:none;width:15px;height:15px}.J84-9a_switchRow{cursor:pointer;align-items:center;gap:8px;font-size:14px;line-height:22px;display:flex}.J84-9a_extTag{background:var(--dsw-static-deepseek-100);color:var(--dsw-static-deepseek-600);border:.5px solid #4176e659;border-radius:4px;flex:none;padding:1px 6px;font-size:11px;font-weight:500;line-height:16px}.J84-9a_modalMask{background:var(--dsw-alias-bg-mask-1);z-index:100;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.J84-9a_modal{background:var(--dsw-specific-menu);border-radius:16px;width:100%;padding:20px;box-shadow:0 12px 40px #0000002e}.J84-9a_modalTitle{color:var(--dsw-alias-label-primary);margin-bottom:8px;font-size:16px;font-weight:500;line-height:24px}.J84-9a_modalDesc{color:var(--dsw-alias-label-secondary);margin-bottom:16px;font-size:14px;line-height:22px}.J84-9a_modalFoot{justify-content:flex-end;gap:8px;display:flex}.J84-9a_quickWrap{flex-direction:column;gap:6px;display:flex;position:relative}.J84-9a_quickList{z-index:20;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-specific-menu);border-radius:8px;max-height:264px;margin-top:2px;padding:4px;position:absolute;top:100%;left:0;right:0;overflow-y:auto;box-shadow:0 8px 24px #0000001f}.J84-9a_quickItem{width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;flex-direction:column;gap:1px;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;display:flex}.J84-9a_quickItemId{overflow-wrap:anywhere}.J84-9a_quickItemApi{color:var(--dsw-alias-label-tertiary);letter-spacing:.2px;font-size:11px;line-height:14px}.J84-9a_quickItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.J84-9a_quickEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 8px;font-size:12px;line-height:18px}@media (prefers-reduced-motion:reduce){.J84-9a_customizedSummary:before{transition:none}}\n";
7
7
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="dsh-model-extension"]') === null) {
8
8
  var __tag = document.createElement('style');
9
9
  __tag.dataset.plugin = "dsh-model-extension";
@@ -217,8 +217,10 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
217
217
  /** Same-origin routes served by the server half. */
218
218
  const INDEX_URL = "/plugins/dsh-model-extension/models-index";
219
219
  const DOWNLOAD_URL = "/plugins/dsh-model-extension/models-download";
220
- /** Module-level cache: one page load fetches the index at most once. */
221
- let cached;
220
+ const PI_AI_URL = "/plugins/dsh-model-extension/pi-ai-catalog";
221
+ /** Module-level cache: one page load fetches each source at most once. */
222
+ let cachedIndex;
223
+ /** Normalize the models.dev route payload. */
222
224
  function normalize(raw) {
223
225
  if (!Array.isArray(raw)) return [];
224
226
  const entries = [];
@@ -237,23 +239,160 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
237
239
  }
238
240
  return entries;
239
241
  }
242
+ /** Narrow a modality list to what this UI offers. */
243
+ function supportedInput(value) {
244
+ if (!Array.isArray(value)) return [];
245
+ return value.filter((m) => typeof m === "string" && (m === "text" || m === "image"));
246
+ }
247
+ /** Read one object-valued field off a raw record. */
248
+ function objectField(source, key) {
249
+ const value = source[key];
250
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
251
+ }
240
252
  /**
241
- * The metadata index, fetched once per page load; `undefined` while the
242
- * server has no metadata file (the caller shows the not-ready hint).
253
+ * The provider a models.dev id belongs to.
254
+ *
255
+ * models.dev ids are directory paths (`zhipuai/glm-5.3-flash`), so the leading
256
+ * segment names the provider. Reading it here gives the picker the same
257
+ * provider-first navigation that pi-ai's per-provider catalog has for free —
258
+ * and it is what lets a user narrow by vendor instead of guessing between
259
+ * same-named ids.
260
+ * @param id - the models.dev directory id.
261
+ * @returns the provider segment, or undefined when the id carries none.
243
262
  */
244
- async function getMetadataIndex() {
245
- if (cached !== void 0) return cached;
263
+ function modelsDevProvider(id) {
264
+ const slash = id.indexOf("/");
265
+ return slash > 0 ? id.slice(0, slash) : void 0;
266
+ }
267
+ /** Turn models.dev rows into prefill candidates. */
268
+ function toModelsDevEntries(rows) {
269
+ return rows.map((row) => ({
270
+ source: "models.dev",
271
+ key: `models.dev\u0000${row.id}`,
272
+ id: row.id,
273
+ provider: modelsDevProvider(row.id),
274
+ context: row.context,
275
+ output: row.output,
276
+ input: row.input,
277
+ reasoning: row.reasoning
278
+ }));
279
+ }
280
+ /** Read the diagnostics warnings the server attaches to a catalog payload. */
281
+ function diagnosticsWarnings(payload) {
282
+ if (typeof payload !== "object" || payload === null) return [];
283
+ const diagnostics = payload["diagnostics"];
284
+ if (typeof diagnostics !== "object" || diagnostics === null) return [];
285
+ const warnings = diagnostics["warnings"];
286
+ if (!Array.isArray(warnings)) return [];
287
+ return warnings.filter((warning) => typeof warning === "string");
288
+ }
289
+ /** Turn a pi-ai route payload into prefill candidates, keeping both ids' provenance. */
290
+ function toPiAiEntries(payload) {
291
+ const warnings = diagnosticsWarnings(payload);
292
+ if (typeof payload !== "object" || payload === null) return {
293
+ entries: [],
294
+ warnings
295
+ };
296
+ const body = payload;
297
+ if (!Array.isArray(body["models"])) return {
298
+ entries: [],
299
+ warnings
300
+ };
301
+ const entries = [];
302
+ for (const item of body["models"]) {
303
+ if (typeof item !== "object" || item === null) continue;
304
+ const row = item;
305
+ const id = row["id"];
306
+ const provider = row["provider"];
307
+ if (typeof id !== "string" || id.length === 0) continue;
308
+ if (typeof provider !== "string" || provider.length === 0) continue;
309
+ entries.push({
310
+ source: "pi-ai",
311
+ key: `pi-ai\u0000${provider}\u0000${id}`,
312
+ id,
313
+ provider,
314
+ name: typeof row["name"] === "string" && row["name"].length > 0 ? row["name"] : void 0,
315
+ context: typeof row["contextWindow"] === "number" ? row["contextWindow"] : void 0,
316
+ output: typeof row["maxTokens"] === "number" ? row["maxTokens"] : void 0,
317
+ input: supportedInput(row["input"]),
318
+ reasoning: row["reasoning"] === true,
319
+ api: typeof row["api"] === "string" ? row["api"] : void 0,
320
+ thinkingLevelMap: objectField(row, "thinkingLevelMap"),
321
+ compat: objectField(row, "compat")
322
+ });
323
+ }
324
+ return {
325
+ entries,
326
+ warnings,
327
+ version: typeof body["piAiVersion"] === "string" ? body["piAiVersion"] : void 0,
328
+ generatedAt: typeof body["generatedAt"] === "string" ? body["generatedAt"] : void 0
329
+ };
330
+ }
331
+ /**
332
+ * Fetch one JSON route, keeping a refusal distinguishable from an empty source.
333
+ *
334
+ * The distinction matters for the pi-ai half: "the host ships no catalog" and
335
+ * "the host's catalog could not be called" both leave the list empty, but only
336
+ * the second one has a fix, and the server names it in the refusal body.
337
+ * @param url - the same-origin route.
338
+ * @returns the parsed payload, or the reason it is missing.
339
+ */
340
+ async function fetchJson(url) {
246
341
  try {
247
- const response = await fetch(INDEX_URL);
248
- if (!response.ok) return void 0;
249
- cached = normalize(await response.json());
250
- return cached;
251
- } catch {
252
- return;
342
+ const response = await fetch(url);
343
+ const payload = await response.json().catch(() => void 0);
344
+ if (response.ok) return { payload };
345
+ const message = typeof payload === "object" && payload !== null ? payload["message"] : void 0;
346
+ return { failure: typeof message === "string" ? message : `HTTP ${String(response.status)}` };
347
+ } catch (error) {
348
+ return { failure: error instanceof Error ? error.message : String(error) };
253
349
  }
254
350
  }
255
351
  /**
256
- * Download/refresh the metadata file through the server half (user-triggered
352
+ * The prefill index, fetched once per page load. Both sources are requested
353
+ * together and either may come back empty; an entirely empty index is a
354
+ * legitimate answer the caller renders as "nothing to load yet".
355
+ * @returns the merged index.
356
+ */
357
+ async function getPrefillIndex() {
358
+ if (cachedIndex !== void 0) return cachedIndex;
359
+ const [piAiResult, modelsDevResult] = await Promise.all([fetchJson(PI_AI_URL), fetchJson(INDEX_URL)]);
360
+ const piAi = toPiAiEntries(piAiResult.payload);
361
+ cachedIndex = {
362
+ piAi: piAi.entries,
363
+ modelsDev: toModelsDevEntries(normalize(modelsDevResult.payload)),
364
+ piAiVersion: piAi.version,
365
+ piAiGeneratedAt: piAi.generatedAt,
366
+ piAiWarnings: piAi.warnings,
367
+ ...piAiResult.failure !== void 0 ? { piAiFailure: piAiResult.failure } : {}
368
+ };
369
+ return cachedIndex;
370
+ }
371
+ /**
372
+ * The one wire protocol one catalog provider's entries agree on.
373
+ *
374
+ * This mirrors the host's own `sharedCatalogApi`: a route whose installed
375
+ * entries all speak the same protocol needs no `api` in configuration, because
376
+ * the host resolves every model — including ones the catalog does not describe
377
+ * — to that shared protocol. Reading it here is what lets the panel judge a
378
+ * row's compat switches the way the adapter will. A provider whose entries
379
+ * disagree (an OpenAI-style catalog spanning Responses and Chat Completions)
380
+ * has no such answer, and neither does an index that has not loaded yet.
381
+ * @param index - the fetched index, or undefined while the page still loads.
382
+ * @param provider - the route key to inspect.
383
+ * @returns the shared protocol, or undefined when none settles it.
384
+ */
385
+ function sharedCatalogApi(index, provider) {
386
+ if (index === void 0 || provider === void 0 || provider.length === 0) return void 0;
387
+ const apis = /* @__PURE__ */ new Set();
388
+ for (const entry of index.piAi) {
389
+ if (entry.provider !== provider || entry.api === void 0) continue;
390
+ apis.add(entry.api);
391
+ }
392
+ return apis.size === 1 ? [...apis][0] : void 0;
393
+ }
394
+ /**
395
+ * Download/refresh the models.dev file through the server half (user-triggered
257
396
  * from the title-row button). A success also refreshes the local cache.
258
397
  */
259
398
  async function downloadMetadata() {
@@ -264,7 +403,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
264
403
  ok: false,
265
404
  message: body?.message ?? `HTTP ${String(response.status)}`
266
405
  };
267
- cached = normalize(body["index"]);
406
+ cachedIndex = void 0;
268
407
  return { ok: true };
269
408
  } catch (error) {
270
409
  return {
@@ -274,198 +413,60 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
274
413
  }
275
414
  }
276
415
  /**
277
- * Case-insensitive substring match over model ids, at most {@link limit}
278
- * candidates (plan: show ids only, never the whole list).
279
- * @param index - the metadata index, or undefined while unavailable.
280
- * @param query - the user's typed text.
281
- * @param limit - maximum candidates to return.
282
- * @returns the matching ids; empty when the index is unavailable.
283
- */
284
- function searchModelIds(index, query, limit = 10) {
285
- if (index === void 0) return [];
286
- const needle = query.trim().toLowerCase();
287
- if (needle.length === 0) return [];
288
- const hits = [];
289
- for (const entry of index) {
290
- if (!entry.id.toLowerCase().includes(needle)) continue;
291
- hits.push(entry.id);
292
- if (hits.length >= limit) break;
293
- }
294
- return hits;
295
- }
296
- //#endregion
297
- //#region src/client/models-plus/compat.ts
298
- /**
299
- * The minimal path ops carrying `after` over `before`, both as the card sees
300
- * them. Only keys the card observed are named; fields absent from both sides
301
- * produce no op, which is why edits are path-addressed rather than a rebuilt
302
- * section.
303
- * @param base - path of the edited subtree inside the user section.
304
- * @param before - the subtree as loaded, or undefined when it is new.
305
- * @param after - the subtree as edited.
306
- * @returns ordered set/unset ops; empty when nothing changed.
307
- */
308
- function pathOps(base, before, after) {
309
- const previous = typeof before === "object" && before !== null && !Array.isArray(before) ? before : {};
310
- const ops = [];
311
- for (const [key, value] of Object.entries(after)) {
312
- if (JSON.stringify(previous[key]) === JSON.stringify(value)) continue;
313
- ops.push({
314
- op: "set",
315
- path: [...base, key],
316
- value
317
- });
318
- }
319
- for (const key of Object.keys(previous)) if (!(key in after)) ops.push({
320
- op: "unset",
321
- path: [...base, key]
322
- });
323
- return ops;
324
- }
325
- /** Accepted capacity spellings: a decimal count with an optional K/M suffix. */
326
- const CAPACITY_PATTERN = /^(\d+(?:\.\d+)?)([km])?$/i;
327
- /** Decimal suffix scales — `1M` is 1000K, matching how model capacities are quoted. */
328
- const CAPACITY_SCALE = {
329
- k: 1e3,
330
- m: 1e6
331
- };
332
- /**
333
- * Read a typed capacity, so a user can write `256K` or `1M` instead of counting
334
- * zeroes. The stored value stays a plain token count.
335
- * @param text - raw field text.
336
- * @returns the count; `undefined` when blank (inherit), `NaN` when unreadable
337
- * (rejected by {@link validateModelRows} before any write).
416
+ * How many candidates the picker renders before it stops and reports the rest.
417
+ *
418
+ * Ten was too few once one id could arrive from a dozen providers; fifty shows
419
+ * a vendor's catalog without pouring a 1354-row index into the DOM.
338
420
  */
339
- function parseCapacity(text) {
340
- const trimmed = text.trim();
341
- if (trimmed.length === 0) return void 0;
342
- const match = CAPACITY_PATTERN.exec(trimmed);
343
- if (match === null) return NaN;
344
- const suffix = match[2]?.toLowerCase();
345
- const scale = suffix === "k" || suffix === "m" ? CAPACITY_SCALE[suffix] : 1;
346
- const scaled = Number(match[1]) * scale;
347
- const rounded = Math.round(scaled);
348
- return Math.abs(scaled - rounded) < 1e-6 ? rounded : scaled;
349
- }
421
+ const DEFAULT_PREFILL_LIMIT = 50;
350
422
  /**
351
- * Spell a stored count back in the shortest form that survives a round trip
352
- * through {@link parseCapacity}; a count that is not a whole number of
353
- * thousands stays written out.
354
- * @param value - stored capacity.
355
- * @returns the field text.
356
- */
357
- function formatCapacity(value) {
358
- if (!Number.isInteger(value) || value <= 0) return String(value);
359
- if (value % CAPACITY_SCALE.m === 0) return `${String(value / CAPACITY_SCALE.m)}M`;
360
- if (value % CAPACITY_SCALE.k === 0) return `${String(value / CAPACITY_SCALE.k)}K`;
361
- return String(value);
362
- }
363
- /** Convert a schema-validated catalog value into records without dropping hidden fields. */
364
- function modelDrafts(value) {
365
- if (!Array.isArray(value)) return [];
366
- return value.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : {});
367
- }
368
- /** Canonical key order for one models[] row (plugin-known fields first). */
369
- const ROW_KEY_ORDER = [
370
- "id",
371
- "name",
372
- "contextWindow",
373
- "maxTokens",
374
- "input",
375
- "reasoningEfforts",
376
- "compat"
377
- ];
378
- /** Canonical key order for the row's compat sub-object. */
379
- const COMPAT_KEY_ORDER = [
380
- "supportsDeveloperRole",
381
- "supportsReasoningEffort",
382
- "thinkingFormat"
383
- ];
384
- /**
385
- * Re-key one model row into the canonical order (plugin-known fields in
386
- * order, unknown/future fields kept at the tail so nothing is dropped).
387
- * YAML emits keys in object insertion order, and interactive edits append
388
- * keys at the tail — without this, rows saved after quick-load or gated-key
389
- * removals come out with scrambled key orders (purely cosmetic, but noisy
390
- * in settings.yaml diffs).
391
- * @param row - the drafted row.
392
- * @returns the re-keyed row (shallow copy).
423
+ * The providers a source offers, alphabetically.
424
+ *
425
+ * This is what turns "which `glm-5.3-flash` did you mean?" into a choice the
426
+ * user can actually make: the same id ships under several providers, and each
427
+ * provider's copy can carry different capacities and compat switches.
428
+ * @param index - the fetched index, or undefined while the page still loads.
429
+ * @param source - the source to enumerate.
430
+ * @returns the provider names; empty when the source has none.
393
431
  */
394
- function normalizeModelRow(row) {
395
- const next = {};
396
- for (const key of ROW_KEY_ORDER) if (key in row) next[key] = row[key];
397
- for (const key of Object.keys(row)) if (!ROW_KEY_ORDER.includes(key)) next[key] = row[key];
398
- const compat = next["compat"];
399
- if (typeof compat === "object" && compat !== null && !Array.isArray(compat)) {
400
- const source = compat;
401
- const normalized = {};
402
- for (const key of COMPAT_KEY_ORDER) if (key in source) normalized[key] = source[key];
403
- for (const key of Object.keys(source)) if (!COMPAT_KEY_ORDER.includes(key)) normalized[key] = source[key];
404
- next["compat"] = normalized;
405
- }
406
- return next;
432
+ function providersOf(index, source) {
433
+ if (index === void 0) return [];
434
+ const entries = source === "pi-ai" ? index.piAi : index.modelsDev;
435
+ const providers = /* @__PURE__ */ new Set();
436
+ for (const entry of entries) if (entry.provider !== void 0 && entry.provider.length > 0) providers.add(entry.provider);
437
+ return [...providers].sort((a, b) => a.localeCompare(b));
407
438
  }
408
439
  /**
409
- * Validate adapter constraints that the serialized schema cannot express.
410
- * @param value - user-owned `models` value, or undefined while inherited.
411
- * @returns the first invalid row, or undefined when the adapter will accept it.
440
+ * Candidates matching one source, one provider and one id substring.
441
+ *
442
+ * Either filter may be blank, and a blank one *widens* rather than matching
443
+ * nothing: a provider with no id typed lists that provider's whole catalog,
444
+ * which is how a user browses a vendor whose model names they have not
445
+ * memorized. Both blank lists the source's first page.
446
+ * @param index - the fetched index, or undefined while the page still loads.
447
+ * @param query - the source, provider, id substring and cap.
448
+ * @returns the capped candidates and the total match count.
412
449
  */
413
- function validateModelRows(value) {
414
- if (value === void 0) return void 0;
415
- const models = modelDrafts(value);
416
- const seen = /* @__PURE__ */ new Set();
417
- for (const [index, model] of models.entries()) {
418
- const id = model["id"];
419
- const trimmed = typeof id === "string" ? id.trim() : void 0;
420
- if (trimmed === void 0 || trimmed.length === 0) return {
421
- index,
422
- key: "modelIdRequired"
423
- };
424
- if (seen.has(trimmed)) return {
425
- index,
426
- key: "modelIdDuplicate"
427
- };
428
- seen.add(trimmed);
429
- const name = model["name"];
430
- if (name !== void 0 && (typeof name !== "string" || name.length === 0)) return {
431
- index,
432
- key: "modelNameInvalid"
433
- };
434
- const contextWindow = model["contextWindow"];
435
- if (contextWindow !== void 0 && (typeof contextWindow !== "number" || !Number.isInteger(contextWindow) || contextWindow <= 0)) return {
436
- index,
437
- key: "modelContextInvalid"
438
- };
439
- const maxTokens = model["maxTokens"];
440
- if (maxTokens !== void 0 && (typeof maxTokens !== "number" || !Number.isInteger(maxTokens) || maxTokens <= 0)) return {
441
- index,
442
- key: "modelMaxTokensInvalid"
443
- };
450
+ function searchPrefill(index, query) {
451
+ if (index === void 0) return {
452
+ hits: [],
453
+ total: 0
454
+ };
455
+ const entries = query.source === "pi-ai" ? index.piAi : index.modelsDev;
456
+ const provider = query.provider?.trim() ?? "";
457
+ const needle = query.text?.trim().toLowerCase() ?? "";
458
+ const limit = query.limit ?? DEFAULT_PREFILL_LIMIT;
459
+ const hits = [];
460
+ let total = 0;
461
+ for (const entry of entries) {
462
+ if (provider.length > 0 && entry.provider !== provider) continue;
463
+ if (needle.length > 0 && !entry.id.toLowerCase().includes(needle)) continue;
464
+ total += 1;
465
+ if (hits.length < limit) hits.push(entry);
444
466
  }
445
- }
446
- //#endregion
447
- //#region src/client/models-plus/presets.ts
448
- /** The tri-state `compat.supportsDeveloperRole` seed: absent (reads checked). */
449
- const DEVELOPER_ROLE_SEED = {};
450
- /** Draft seed for one new model row. */
451
- function newModelDraft() {
452
467
  return {
453
- id: "",
454
- input: ["text", "image"],
455
- reasoningEfforts: {
456
- off: null,
457
- minimal: "minimal",
458
- low: "low",
459
- medium: "medium",
460
- high: "high",
461
- xhigh: "xhigh",
462
- max: "max"
463
- },
464
- compat: {
465
- ...DEVELOPER_ROLE_SEED,
466
- supportsReasoningEffort: true,
467
- thinkingFormat: "openai"
468
- }
468
+ hits,
469
+ total
469
470
  };
470
471
  }
471
472
  //#endregion
@@ -494,6 +495,99 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
494
495
  "string-thinking",
495
496
  "ant-ling"
496
497
  ];
498
+ /**
499
+ * The compatibility switches llm-pi-ai *offers*, by wire protocol (the `offer`
500
+ * set of its COMPAT_GATES @ 0.1.5-rc.2). Only these may reach a model row: the
501
+ * adapter refuses a switch no protocol offers rather than ignoring it, so
502
+ * copying pi-ai's catalog `compat` across verbatim would fail resolution on
503
+ * the withheld fields it also records (routing preferences, session affinity,
504
+ * deferred tools, grammar tools). Re-check against the host's COMPAT_GATES on
505
+ * every adapter-anchor bump — the same drift gate the host keeps for itself.
506
+ */
507
+ const COMPAT_FIELDS = {
508
+ "openai-completions": [
509
+ "supportsStore",
510
+ "supportsDeveloperRole",
511
+ "supportsReasoningEffort",
512
+ "supportsUsageInStreaming",
513
+ "supportsFinishReason",
514
+ "maxTokensField",
515
+ "requiresToolResultName",
516
+ "requiresAssistantAfterToolResult",
517
+ "requiresThinkingAsText",
518
+ "requiresReasoningContentOnAssistantMessages",
519
+ "thinkingFormat",
520
+ "chatTemplateKwargs",
521
+ "chatTemplateArgs",
522
+ "supportsThinkingTokenBudget",
523
+ "thinkingTokenBudgetField",
524
+ "vllmPriority",
525
+ "supportsStrictMode",
526
+ "cacheControlFormat",
527
+ "supportsLongCacheRetention"
528
+ ],
529
+ "openai-responses": [
530
+ "supportsDeveloperRole",
531
+ "supportsMaxOutputTokens",
532
+ "supportsStrictMode",
533
+ "supportsLongCacheRetention"
534
+ ],
535
+ "azure-openai-responses": [
536
+ "supportsDeveloperRole",
537
+ "supportsMaxOutputTokens",
538
+ "supportsStrictMode",
539
+ "supportsLongCacheRetention"
540
+ ],
541
+ "openai-codex-responses": [
542
+ "supportsDeveloperRole",
543
+ "supportsMaxOutputTokens",
544
+ "supportsStrictMode",
545
+ "supportsLongCacheRetention"
546
+ ],
547
+ "anthropic-messages": [
548
+ "supportsEagerToolInputStreaming",
549
+ "supportsLongCacheRetention",
550
+ "supportsCacheControlOnTools",
551
+ "supportsTemperature",
552
+ "forceAdaptiveThinking",
553
+ "allowEmptySignature",
554
+ "supportsStrictTools"
555
+ ],
556
+ "bedrock-converse-stream": ["supportsStrictMode"]
557
+ };
558
+ /**
559
+ * Switches the base panel already renders a dedicated control for; the
560
+ * "other compatibility switches" block lists everything else.
561
+ */
562
+ const COMPAT_BASE_FIELDS = [
563
+ "supportsDeveloperRole",
564
+ "supportsReasoningEffort",
565
+ "thinkingFormat"
566
+ ];
567
+ /**
568
+ * The offered compat field names for one wire protocol.
569
+ * @param api - the model's protocol, when known.
570
+ * @returns the field names; empty for an unknown or absent protocol.
571
+ */
572
+ function offeredCompatFields(api) {
573
+ if (api === void 0) return [];
574
+ const fields = COMPAT_FIELDS[api];
575
+ return fields === void 0 ? [] : fields;
576
+ }
577
+ /**
578
+ * Whether one protocol takes one compat switch.
579
+ *
580
+ * The adapter *refuses* a switch its protocol does not declare rather than
581
+ * ignoring it, so this is the difference between a control that can be offered
582
+ * and one that would fail the save. An unknown protocol takes nothing: its
583
+ * field set cannot be read, and the adapter resolves it to the same refusal.
584
+ * @param api - the model's wire protocol, when known.
585
+ * @param field - the compat field name.
586
+ * @returns true when the protocol offers the field.
587
+ */
588
+ function protocolOffers(api, field) {
589
+ return offeredCompatFields(api).includes(field);
590
+ }
497
591
  /** Extra copy for the extension, keyed like the official dictionaries (en is authoritative). */
498
592
  const en = {
499
593
  nav: "Models+",
@@ -719,6 +813,347 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
719
813
  thinkingFormat: "思考格式"
720
814
  };
721
815
  //#endregion
816
+ //#region src/client/models-plus/compat.ts
817
+ /**
818
+ * The minimal path ops carrying `after` over `before`, both as the card sees
819
+ * them. Only keys the card observed are named; fields absent from both sides
820
+ * produce no op, which is why edits are path-addressed rather than a rebuilt
821
+ * section.
822
+ * @param base - path of the edited subtree inside the user section.
823
+ * @param before - the subtree as loaded, or undefined when it is new.
824
+ * @param after - the subtree as edited.
825
+ * @returns ordered set/unset ops; empty when nothing changed.
826
+ */
827
+ function pathOps(base, before, after) {
828
+ const previous = typeof before === "object" && before !== null && !Array.isArray(before) ? before : {};
829
+ const ops = [];
830
+ for (const [key, value] of Object.entries(after)) {
831
+ if (JSON.stringify(previous[key]) === JSON.stringify(value)) continue;
832
+ ops.push({
833
+ op: "set",
834
+ path: [...base, key],
835
+ value
836
+ });
837
+ }
838
+ for (const key of Object.keys(previous)) if (!(key in after)) ops.push({
839
+ op: "unset",
840
+ path: [...base, key]
841
+ });
842
+ return ops;
843
+ }
844
+ /** Accepted capacity spellings: a decimal count with an optional K/M suffix. */
845
+ const CAPACITY_PATTERN = /^(\d+(?:\.\d+)?)([km])?$/i;
846
+ /** Decimal suffix scales — `1M` is 1000K, matching how model capacities are quoted. */
847
+ const CAPACITY_SCALE = {
848
+ k: 1e3,
849
+ m: 1e6
850
+ };
851
+ /**
852
+ * Read a typed capacity, so a user can write `256K` or `1M` instead of counting
853
+ * zeroes. The stored value stays a plain token count.
854
+ * @param text - raw field text.
855
+ * @returns the count; `undefined` when blank (inherit), `NaN` when unreadable
856
+ * (rejected by {@link validateModelRows} before any write).
857
+ */
858
+ function parseCapacity(text) {
859
+ const trimmed = text.trim();
860
+ if (trimmed.length === 0) return void 0;
861
+ const match = CAPACITY_PATTERN.exec(trimmed);
862
+ if (match === null) return NaN;
863
+ const suffix = match[2]?.toLowerCase();
864
+ const scale = suffix === "k" || suffix === "m" ? CAPACITY_SCALE[suffix] : 1;
865
+ const scaled = Number(match[1]) * scale;
866
+ const rounded = Math.round(scaled);
867
+ return Math.abs(scaled - rounded) < 1e-6 ? rounded : scaled;
868
+ }
869
+ /**
870
+ * Spell a stored count back in the shortest form that survives a round trip
871
+ * through {@link parseCapacity}; a count that is not a whole number of
872
+ * thousands stays written out.
873
+ * @param value - stored capacity.
874
+ * @returns the field text.
875
+ */
876
+ function formatCapacity(value) {
877
+ if (!Number.isInteger(value) || value <= 0) return String(value);
878
+ if (value % CAPACITY_SCALE.m === 0) return `${String(value / CAPACITY_SCALE.m)}M`;
879
+ if (value % CAPACITY_SCALE.k === 0) return `${String(value / CAPACITY_SCALE.k)}K`;
880
+ return String(value);
881
+ }
882
+ /** Convert a schema-validated catalog value into records without dropping hidden fields. */
883
+ function modelDrafts(value) {
884
+ if (!Array.isArray(value)) return [];
885
+ return value.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : {});
886
+ }
887
+ /** Canonical key order for one models[] row (plugin-known fields first). */
888
+ const ROW_KEY_ORDER = [
889
+ "id",
890
+ "name",
891
+ "contextWindow",
892
+ "maxTokens",
893
+ "input",
894
+ "reasoningEfforts",
895
+ "compat"
896
+ ];
897
+ /** Canonical key order for the row's compat sub-object. */
898
+ const COMPAT_KEY_ORDER = [
899
+ "supportsDeveloperRole",
900
+ "supportsReasoningEffort",
901
+ "thinkingFormat"
902
+ ];
903
+ /**
904
+ * Re-key one model row into the canonical order (plugin-known fields in
905
+ * order, unknown/future fields kept at the tail so nothing is dropped).
906
+ * YAML emits keys in object insertion order, and interactive edits append
907
+ * keys at the tail — without this, rows saved after quick-load or gated-key
908
+ * removals come out with scrambled key orders (purely cosmetic, but noisy
909
+ * in settings.yaml diffs).
910
+ * @param row - the drafted row.
911
+ * @returns the re-keyed row (shallow copy).
912
+ */
913
+ function normalizeModelRow(row) {
914
+ const next = {};
915
+ for (const key of ROW_KEY_ORDER) if (key in row) next[key] = row[key];
916
+ for (const key of Object.keys(row)) if (!ROW_KEY_ORDER.includes(key)) next[key] = row[key];
917
+ const compat = next["compat"];
918
+ if (typeof compat === "object" && compat !== null && !Array.isArray(compat)) {
919
+ const source = compat;
920
+ const normalized = {};
921
+ for (const key of COMPAT_KEY_ORDER) if (key in source) normalized[key] = source[key];
922
+ for (const key of Object.keys(source)) if (!COMPAT_KEY_ORDER.includes(key)) normalized[key] = source[key];
923
+ next["compat"] = normalized;
924
+ }
925
+ return next;
926
+ }
927
+ /**
928
+ * The message for a refused compat switch, shared by both provider cards so the
929
+ * same failure reads the same way wherever it is hit.
930
+ * @param failure - a `modelCompatNotOffered` failure from {@link validateModelRows}.
931
+ * @returns the localised sentence.
932
+ */
933
+ function compatFailureMessage(failure) {
934
+ const offered = failure.offered ?? [];
935
+ const available = offered.length === 0 ? ",该协议不接受任何可配置开关" : `,该协议可用:${offered.join("、")}`;
936
+ return `模型 ${String(failure.index + 1)}:compat 开关 ${String(failure.field)} 不适用于当前协议(${String(failure.api)})${available}。请在展开的「其他兼容开关」中清空该项以移除,或回到供应商页把协议改成端点真正使用的协议。`;
937
+ }
938
+ /**
939
+ * Validate adapter constraints that the serialized schema cannot express.
940
+ *
941
+ * The compat half of the check exists because the adapter *refuses* a switch a
942
+ * model's protocol does not declare rather than ignoring it — and the protocol
943
+ * a row resolves to is `route.api` first, the installed catalog entry second.
944
+ * A row can therefore hold switches that were legal when it was written (a
945
+ * protocol change on the route, or a prefill taken from another protocol's
946
+ * catalog entry) and only fail at save time, in English, from the host. This
947
+ * turns that into a positioned, named, localised refusal.
948
+ *
949
+ * An unknown protocol is skipped on purpose: the route's protocol could not be
950
+ * resolved, so the row's real one is unknowable here, and a guess would refuse
951
+ * a configuration the host accepts. The host's own diagnostic still covers it.
952
+ * @param value - user-owned `models` value, or undefined while inherited.
953
+ * @param api - the protocol every row resolves to, when the route settles one.
954
+ * @returns the first invalid row, or undefined when the adapter will accept it.
955
+ */
956
+ function validateModelRows(value, api) {
957
+ if (value === void 0) return void 0;
958
+ const models = modelDrafts(value);
959
+ const seen = /* @__PURE__ */ new Set();
960
+ for (const [index, model] of models.entries()) {
961
+ const id = model["id"];
962
+ const trimmed = typeof id === "string" ? id.trim() : void 0;
963
+ if (trimmed === void 0 || trimmed.length === 0) return {
964
+ index,
965
+ key: "modelIdRequired"
966
+ };
967
+ if (seen.has(trimmed)) return {
968
+ index,
969
+ key: "modelIdDuplicate"
970
+ };
971
+ seen.add(trimmed);
972
+ const name = model["name"];
973
+ if (name !== void 0 && (typeof name !== "string" || name.length === 0)) return {
974
+ index,
975
+ key: "modelNameInvalid"
976
+ };
977
+ const contextWindow = model["contextWindow"];
978
+ if (contextWindow !== void 0 && (typeof contextWindow !== "number" || !Number.isInteger(contextWindow) || contextWindow <= 0)) return {
979
+ index,
980
+ key: "modelContextInvalid"
981
+ };
982
+ const maxTokens = model["maxTokens"];
983
+ if (maxTokens !== void 0 && (typeof maxTokens !== "number" || !Number.isInteger(maxTokens) || maxTokens <= 0)) return {
984
+ index,
985
+ key: "modelMaxTokensInvalid"
986
+ };
987
+ const compat = model["compat"];
988
+ if (api !== void 0 && typeof compat === "object" && compat !== null && !Array.isArray(compat)) for (const field of Object.keys(compat)) {
989
+ if (protocolOffers(api, field)) continue;
990
+ return {
991
+ index,
992
+ key: "modelCompatNotOffered",
993
+ field,
994
+ api,
995
+ offered: offeredCompatFields(api)
996
+ };
997
+ }
998
+ }
999
+ }
1000
+ //#endregion
1001
+ //#region src/client/models-plus/presets.ts
1002
+ /**
1003
+ * Draft defaults for a NEW model row (added by hand or adopted from a fetch),
1004
+ * per the v1.0.0 plan: reasoning effort ON with every level declared, thinking
1005
+ * format `openai`, both input modalities checked. These are draft seeds that
1006
+ * land in settings.yaml when the row is saved — they are not inherited-state
1007
+ * sentinels, except `supportsDeveloperRole`, which stays UNWRITTEN (its
1008
+ * tri-state reads an absent key as checked, so only an explicit user uncheck
1009
+ * ever writes `false`).
1010
+ *
1011
+ * The compat half of the seed is PROTOCOL-DEPENDENT, and that is load-bearing:
1012
+ * `supportsReasoningEffort` and `thinkingFormat` belong to `openai-completions`
1013
+ * alone, and the adapter refuses a switch its protocol does not declare rather
1014
+ * than ignoring it. Seeding them onto an `anthropic-messages` route — which is
1015
+ * what a fixed vocabulary here did — made every hand-added model on such a
1016
+ * route fail the save over a switch the user never wrote. So the seed asks
1017
+ * {@link protocolOffers} first and writes nothing its protocol would refuse.
1018
+ * `reasoningEfforts` is unaffected: the levels travel through each protocol's
1019
+ * own thinking parameter, so every protocol takes them.
1020
+ */
1021
+ /** The tri-state `compat.supportsDeveloperRole` seed: absent (reads checked). */
1022
+ const DEVELOPER_ROLE_SEED = {};
1023
+ /**
1024
+ * Draft seed for one new model row.
1025
+ *
1026
+ * An UNKNOWN protocol (no route-level `api`, and the installed catalog does not
1027
+ * describe the route) seeds no compat at all: nothing can say which switches
1028
+ * its endpoint would take, and a guessed `supportsReasoningEffort` on an
1029
+ * Anthropic-shaped route is refused rather than dropped. The levels stay seeded
1030
+ * — they are protocol-independent — so the row still opens with its effort
1031
+ * table populated.
1032
+ * @param api - the wire protocol this row will resolve to, when known.
1033
+ * @returns the draft row.
1034
+ */
1035
+ function newModelDraft(api) {
1036
+ const compat = { ...DEVELOPER_ROLE_SEED };
1037
+ if (protocolOffers(api, "supportsReasoningEffort")) compat["supportsReasoningEffort"] = true;
1038
+ if (protocolOffers(api, "thinkingFormat")) compat["thinkingFormat"] = "openai";
1039
+ return {
1040
+ id: "",
1041
+ input: ["text", "image"],
1042
+ reasoningEfforts: {
1043
+ off: null,
1044
+ minimal: "minimal",
1045
+ low: "low",
1046
+ medium: "medium",
1047
+ high: "high",
1048
+ xhigh: "xhigh",
1049
+ max: "max"
1050
+ },
1051
+ ...Object.keys(compat).length === 0 ? {} : { compat }
1052
+ };
1053
+ }
1054
+ //#endregion
1055
+ //#region src/client/models-plus/prefill.ts
1056
+ /**
1057
+ * Mapping pi-ai catalog values onto a Models+ row.
1058
+ *
1059
+ * The two sides describe the same thing, but two conventions differ in a way
1060
+ * that makes a straight copy wrong:
1061
+ *
1062
+ * 1. **`null` means the opposite in each.** pi-ai's `thinkingLevelMap[level]`
1063
+ * is *unsupported* when null; our `reasoningEfforts[level]` is *supported,
1064
+ * send nothing* when null (the adapter then drops the key entirely, and
1065
+ * pi-ai reads an absent `off` key as "supported but send no parameter").
1066
+ * Copying one across would invert the meaning for 338 of the catalog's
1067
+ * models, so values are mapped rather than copied: a pi-ai `null` becomes
1068
+ * "omit our key", and an absent pi-ai key becomes an explicit entry.
1069
+ * 2. **An absent base level has a protocol-specific default.** pi-ai resolves
1070
+ * an absent `off`/`minimal`/`low`/`medium`/`high` by falling back to the
1071
+ * level name — except on the Anthropic protocols, where `minimal` collapses
1072
+ * onto `low`, and on Mistral, where every level resolves to `high`. Writing
1073
+ * the level name there would send a spelling those providers never see.
1074
+ *
1075
+ * `xhigh` and `max` are the one pair pi-ai treats as unsupported when absent,
1076
+ * so they are only written when the catalog declares them.
1077
+ *
1078
+ * @module dsh-model-extension/prefill
1079
+ */
1080
+ /** The seven levels, in pi-ai's escalation order. */
1081
+ const LEVELS = [
1082
+ "off",
1083
+ "minimal",
1084
+ "low",
1085
+ "medium",
1086
+ "high",
1087
+ "xhigh",
1088
+ "max"
1089
+ ];
1090
+ /**
1091
+ * The wire spelling pi-ai falls back to for one absent base level, per
1092
+ * protocol. This mirrors the fallbacks inside pi-ai's own dispatch branches.
1093
+ * @param api - the model's wire protocol.
1094
+ * @param level - the level whose key was absent.
1095
+ * @returns the spelling pi-ai would send for that level.
1096
+ */
1097
+ function defaultWireSpelling(api, level) {
1098
+ if (api === "anthropic-messages" || api === "bedrock-converse-stream") return level === "minimal" ? "low" : level;
1099
+ if (api === "mistral-conversations") return "high";
1100
+ return level;
1101
+ }
1102
+ /**
1103
+ * Convert a catalog `thinkingLevelMap` into our `reasoningEfforts` dict.
1104
+ *
1105
+ * A model that does not reason gets `false`. An outcome that offers nothing
1106
+ * beyond `off` also becomes `false`, because the adapter rejects a dict whose
1107
+ * only level is `off` — and the two describe the same request anyway.
1108
+ * @param levelMap - the catalog's map, when it carries one.
1109
+ * @param api - the model's wire protocol, which picks the fallback spellings.
1110
+ * @param reasoning - whether the model reasons at all.
1111
+ * @returns the dict to write into the row, or `false` for a non-reasoning model.
1112
+ */
1113
+ function reasoningEffortsFrom(levelMap, api, reasoning) {
1114
+ if (!reasoning) return false;
1115
+ const map = levelMap ?? {};
1116
+ const efforts = {};
1117
+ for (const level of LEVELS) {
1118
+ if (!(level in map)) {
1119
+ if (level === "off") {
1120
+ efforts[level] = null;
1121
+ continue;
1122
+ }
1123
+ if (level === "xhigh" || level === "max") continue;
1124
+ efforts[level] = defaultWireSpelling(api, level);
1125
+ continue;
1126
+ }
1127
+ const wire = map[level];
1128
+ if (wire === null) continue;
1129
+ efforts[level] = wire;
1130
+ }
1131
+ return Object.keys(efforts).some((level) => level !== "off") ? efforts : false;
1132
+ }
1133
+ /**
1134
+ * Project a catalog `compat` block onto the switches the host offers for one
1135
+ * protocol.
1136
+ *
1137
+ * The filter is load-bearing twice over. It drops pi-ai's withheld switches
1138
+ * (routing preferences, session affinity, deferred tools, grammar tools), which
1139
+ * the adapter *refuses* rather than ignores — a verbatim copy would make the
1140
+ * route fail to resolve. And it filters by the protocol the row will be JUDGED
1141
+ * by, which is not always the catalog entry's own: a hand-declared route states
1142
+ * its `api`, that value overrides the catalog for every model on the route, and
1143
+ * a block taken from another protocol's entry (`supportsStore` on a gateway
1144
+ * that speaks `anthropic-messages`) is refused at save time.
1145
+ * @param compat - the catalog's compat record, when it carries one.
1146
+ * @param api - the protocol the row resolves to, as the adapter resolves it.
1147
+ * @returns the writable subset; empty when nothing survives.
1148
+ */
1149
+ function compatFrom(compat, api) {
1150
+ const offered = offeredCompatFields(api);
1151
+ if (offered.length === 0 || compat === void 0) return {};
1152
+ const next = {};
1153
+ for (const field of offered) if (field in compat) next[field] = compat[field];
1154
+ return next;
1155
+ }
1156
+ //#endregion
722
1157
  //#region src/client/models-plus/models-plus.module.css
723
1158
  var models_plus_module_default = {
724
1159
  "addActions": "J84-9a_addActions",
@@ -786,6 +1221,8 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
786
1221
  "primaryButton": "J84-9a_primaryButton",
787
1222
  "quickEmpty": "J84-9a_quickEmpty",
788
1223
  "quickItem": "J84-9a_quickItem",
1224
+ "quickItemApi": "J84-9a_quickItemApi",
1225
+ "quickItemId": "J84-9a_quickItemId",
789
1226
  "quickList": "J84-9a_quickList",
790
1227
  "quickWrap": "J84-9a_quickWrap",
791
1228
  "rowActions": "J84-9a_rowActions",
@@ -812,33 +1249,170 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
812
1249
  //#endregion
813
1250
  //#region src/client/models-plus/QuickLoad.tsx
814
1251
  /**
815
- * Quick-load: a searchable dropdown over the models.dev metadata index.
816
- * Plan constraints: ids only, case-insensitive substring match, at most 10
817
- * candidates rendered — the index is filtered on every keystroke and the
818
- * candidate list is only mounted while open, so a 370-row index never lands
819
- * in the DOM wholesale.
1252
+ * Quick-load: source → provider → model id, three narrowing steps.
1253
+ *
1254
+ * The single search box could not answer the question it raised. One model id
1255
+ * ships under a dozen providers (`glm-5.3-flash` exists in zai, opencode,
1256
+ * opencode-go, baseten, together, huggingface and two gateways), and each
1257
+ * provider's copy can carry different capacities and compat switches — so a
1258
+ * flat list of ten same-looking ids left the user to pick blind, or to decode
1259
+ * a suffix the plugin had appended for want of a better idea.
1260
+ *
1261
+ * Splitting the axis fixes it at the source: the user states *which catalog*
1262
+ * and *whose copy* first, and the id search then runs inside that slice.
1263
+ *
1264
+ * Both drop-downs are plugin-rendered rather than native. A `<datalist>` was
1265
+ * tried for the provider field and abandoned: the browser positions its popup
1266
+ * itself (it opened across unrelated fields) and none of it is themeable, so
1267
+ * it could not look like the list beside it. One `quickList` now serves both,
1268
+ * which also keeps only one popup on screen at a time.
1269
+ */
1270
+ /** The two sources, in the order the picker offers them; pi-ai leads by default. */
1271
+ const SOURCES = [{
1272
+ id: "pi-ai",
1273
+ label: "llm-pi-ai"
1274
+ }, {
1275
+ id: "models.dev",
1276
+ label: "models.dev"
1277
+ }];
1278
+ /**
1279
+ * One line describing what the selected source currently offers, shown while
1280
+ * nothing is typed. The pi-ai version and snapshot date matter here: a DSH home
1281
+ * can hold more than one pi-ai copy, and knowing which one answered is the
1282
+ * difference between a diagnosis and a mystery.
1283
+ * @param index - the fetched index.
1284
+ * @param source - the selected source.
1285
+ * @returns the summary line.
820
1286
  */
1287
+ function sourceSummary(index, source) {
1288
+ if (source === "pi-ai") {
1289
+ if (index.piAi.length === 0) return index.piAiFailure === void 0 ? "pi-ai 目录不可用" : `pi-ai 目录不可用:${index.piAiFailure}`;
1290
+ const version = index.piAiVersion === void 0 ? "" : ` v${index.piAiVersion}`;
1291
+ const when = index.piAiGeneratedAt === void 0 ? "" : ` · ${index.piAiGeneratedAt.slice(0, 10)}`;
1292
+ return `pi-ai 目录${version},${String(index.piAi.length)} 个模型${when};选择提供方或输入 model id`;
1293
+ }
1294
+ if (index.modelsDev.length === 0) return "models.dev 元数据未下载——点击页首「下载/更新元数据」";
1295
+ return `models.dev,${String(index.modelsDev.length)} 条;选择提供方或输入 model id`;
1296
+ }
821
1297
  /**
822
- * Render the quick-load input with its filtered candidate dropdown.
1298
+ * Render the three-step quick-load picker and its candidate list.
823
1299
  * @param props - the pick handler and disabled flag.
824
- * @returns the quick-load field.
1300
+ * @returns the quick-load block.
825
1301
  */
826
1302
  function QuickLoad(props) {
827
- const [query, setQuery] = (0, react.useState)("");
828
- const [open, setOpen] = (0, react.useState)(false);
1303
+ const [source, setSource] = (0, react.useState)("pi-ai");
1304
+ const [provider, setProvider] = (0, react.useState)("");
1305
+ const [text, setText] = (0, react.useState)("");
1306
+ const [panel, setPanel] = (0, react.useState)(null);
829
1307
  const [index, setIndex] = (0, react.useState)(void 0);
1308
+ const textField = (0, react.useRef)(null);
830
1309
  const ensureIndex = () => {
831
- if (index !== void 0) return;
832
- getMetadataIndex().then((loaded) => {
1310
+ getPrefillIndex().then((loaded) => {
833
1311
  setIndex(loaded);
834
1312
  });
835
1313
  };
836
- const hits = searchModelIds(index, query, 10);
837
- const pick = (id) => {
838
- const entry = index?.find((candidate) => candidate.id === id);
839
- setOpen(false);
840
- setQuery("");
841
- if (entry !== void 0) props.onPick(entry);
1314
+ const providers = providersOf(index, source);
1315
+ const chosenProvider = providers.includes(provider) ? provider : "";
1316
+ const { hits, total } = searchPrefill(index, {
1317
+ source,
1318
+ provider: chosenProvider,
1319
+ text
1320
+ });
1321
+ const pick = (entry) => {
1322
+ setPanel(null);
1323
+ setText(entry.id);
1324
+ props.onPick(entry);
1325
+ };
1326
+ const switchSource = (next) => {
1327
+ setSource(next);
1328
+ setProvider("");
1329
+ setText("");
1330
+ setPanel(null);
1331
+ };
1332
+ /**
1333
+ * Commit one provider choice and move straight on to the id field, so the
1334
+ * flow is pick → type rather than pick → click → type.
1335
+ * @param name - the provider to select; blank means "every provider".
1336
+ */
1337
+ const chooseProvider = (name) => {
1338
+ setProvider(name);
1339
+ setText("");
1340
+ textField.current?.focus();
1341
+ };
1342
+ const providerBody = () => {
1343
+ if (index === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1344
+ className: models_plus_module_default["quickEmpty"],
1345
+ children: "正在载入预填数据…"
1346
+ });
1347
+ const needle = provider.trim().toLowerCase();
1348
+ const matches = needle.length === 0 ? providers : providers.filter((name) => name.toLowerCase().includes(needle));
1349
+ if (providers.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1350
+ className: models_plus_module_default["quickEmpty"],
1351
+ children: "该数据源没有提供方信息"
1352
+ });
1353
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1355
+ type: "button",
1356
+ className: models_plus_module_default["quickItem"],
1357
+ onMouseDown: (event) => {
1358
+ event.preventDefault();
1359
+ chooseProvider("");
1360
+ },
1361
+ children: "全部(不限提供方)"
1362
+ }),
1363
+ matches.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1364
+ type: "button",
1365
+ className: models_plus_module_default["quickItem"],
1366
+ onMouseDown: (event) => {
1367
+ event.preventDefault();
1368
+ chooseProvider(name);
1369
+ },
1370
+ children: name
1371
+ }, name)),
1372
+ matches.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1373
+ className: models_plus_module_default["quickEmpty"],
1374
+ children: "没有匹配的提供方"
1375
+ }) : null
1376
+ ] });
1377
+ };
1378
+ const modelBody = () => {
1379
+ if (index === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1380
+ className: models_plus_module_default["quickEmpty"],
1381
+ children: "正在载入预填数据…"
1382
+ });
1383
+ if (chosenProvider.length === 0 && text.trim().length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1384
+ className: models_plus_module_default["quickEmpty"],
1385
+ children: sourceSummary(index, source)
1386
+ }), source === "pi-ai" && index.piAiWarnings.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1387
+ className: models_plus_module_default["error"],
1388
+ children: index.piAiWarnings.join(";")
1389
+ }) : null] });
1390
+ if (hits.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1391
+ className: models_plus_module_default["quickEmpty"],
1392
+ children: "没有匹配的模型"
1393
+ });
1394
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [hits.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1395
+ type: "button",
1396
+ className: models_plus_module_default["quickItem"],
1397
+ onMouseDown: (event) => {
1398
+ event.preventDefault();
1399
+ pick(entry);
1400
+ },
1401
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1402
+ className: models_plus_module_default["quickItemId"],
1403
+ children: entry.id
1404
+ }), entry.api === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1405
+ className: models_plus_module_default["quickItemApi"],
1406
+ children: entry.api
1407
+ })]
1408
+ }, entry.key)), total > hits.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1409
+ className: models_plus_module_default["quickEmpty"],
1410
+ children: [
1411
+ "另有 ",
1412
+ total - hits.length,
1413
+ " 项匹配——继续输入或选定提供方以缩小范围"
1414
+ ]
1415
+ }) : null] });
842
1416
  };
843
1417
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
1418
  className: models_plus_module_default["quickWrap"],
@@ -847,42 +1421,88 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
847
1421
  className: models_plus_module_default["extLabel"],
848
1422
  children: "快速装入"
849
1423
  }),
850
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
851
- className: models_plus_module_default["input"],
852
- type: "text",
853
- value: query,
854
- placeholder: "搜索模型 id(如 aper)…",
855
- "aria-label": "快速装入",
856
- disabled: props.disabled,
857
- onFocus: () => {
858
- ensureIndex();
859
- setOpen(true);
860
- },
861
- onBlur: () => {
862
- setOpen(false);
1424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1425
+ style: {
1426
+ display: "flex",
1427
+ gap: 6,
1428
+ alignItems: "center"
863
1429
  },
864
- onChange: (event) => {
865
- setQuery(event.target.value);
866
- setOpen(true);
867
- }
1430
+ children: [
1431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1432
+ className: `${models_plus_module_default["input"]} ${models_plus_module_default["selectInput"]}`,
1433
+ style: {
1434
+ flex: "0 0 108px",
1435
+ minWidth: 0
1436
+ },
1437
+ value: source,
1438
+ disabled: props.disabled,
1439
+ "aria-label": "数据源",
1440
+ onChange: (event) => {
1441
+ switchSource(event.target.value);
1442
+ },
1443
+ children: SOURCES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1444
+ value: option.id,
1445
+ children: option.label
1446
+ }, option.id))
1447
+ }),
1448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1449
+ className: models_plus_module_default["input"],
1450
+ style: {
1451
+ flex: "0 0 132px",
1452
+ minWidth: 0
1453
+ },
1454
+ type: "text",
1455
+ value: provider,
1456
+ placeholder: "提供方:全部",
1457
+ "aria-label": "提供方",
1458
+ autoComplete: "off",
1459
+ disabled: props.disabled,
1460
+ onFocus: () => {
1461
+ ensureIndex();
1462
+ setPanel("provider");
1463
+ },
1464
+ onBlur: () => {
1465
+ setPanel(null);
1466
+ },
1467
+ onChange: (event) => {
1468
+ setProvider(event.target.value);
1469
+ setPanel("provider");
1470
+ }
1471
+ }),
1472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1473
+ className: models_plus_module_default["input"],
1474
+ style: {
1475
+ flex: "1 1 auto",
1476
+ minWidth: 0
1477
+ },
1478
+ type: "text",
1479
+ ref: textField,
1480
+ value: text,
1481
+ placeholder: "model id(如 glm)…",
1482
+ "aria-label": "model id",
1483
+ autoComplete: "off",
1484
+ disabled: props.disabled,
1485
+ onFocus: () => {
1486
+ ensureIndex();
1487
+ setPanel("model");
1488
+ },
1489
+ onBlur: () => {
1490
+ setPanel(null);
1491
+ },
1492
+ onChange: (event) => {
1493
+ setText(event.target.value);
1494
+ setPanel("model");
1495
+ }
1496
+ })
1497
+ ]
868
1498
  }),
869
- open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1499
+ panel === "provider" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
870
1500
  className: models_plus_module_default["quickList"],
871
- children: index === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
872
- className: models_plus_module_default["quickEmpty"],
873
- children: "元数据预填不可用——点击页首「下载/更新元数据」"
874
- }) : hits.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
875
- className: models_plus_module_default["quickEmpty"],
876
- children: query.trim().length === 0 ? "输入关键字搜索模型 id" : "没有匹配的模型"
877
- }) : hits.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
878
- type: "button",
879
- className: models_plus_module_default["quickItem"],
880
- onMouseDown: (event) => {
881
- event.preventDefault();
882
- pick(id);
883
- },
884
- children: id
885
- }, id))
1501
+ children: providerBody()
1502
+ }) : null,
1503
+ panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1504
+ className: models_plus_module_default["quickList"],
1505
+ children: modelBody()
886
1506
  }) : null
887
1507
  ]
888
1508
  });
@@ -899,7 +1519,23 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
899
1519
  * rides the card's existing path-ops save with no extra wire calls. Clearing
900
1520
  * the effort gate drops `reasoningEfforts` AND `compat.thinkingFormat` in ONE
901
1521
  * commit so partial updates never overwrite each other (no orphan settings).
1522
+ *
1523
+ * Which compat switches this panel may write at all is decided by `api` (see
1524
+ * {@link ModelEntryPanelProps.api}): the adapter refuses a switch the model's
1525
+ * protocol does not declare instead of ignoring it, so a control offered on the
1526
+ * wrong protocol is a save that cannot succeed. An UNKNOWN protocol hides
1527
+ * nothing — nothing can be judged — and the host's own diagnostic still covers
1528
+ * that case.
1529
+ */
1530
+ /**
1531
+ * Whether a draft field is absent or whitespace-only.
1532
+ * @param value - the field's current value.
1533
+ * @returns true when the field carries nothing worth keeping.
902
1534
  */
1535
+ function isBlank(value) {
1536
+ if (value === void 0 || value === null) return true;
1537
+ return typeof value === "string" && value.trim().length === 0;
1538
+ }
903
1539
  /** The row's compat sub-object as a plain record (absent → empty). */
904
1540
  function compatOf(model) {
905
1541
  const value = model["compat"];
@@ -922,7 +1558,18 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
922
1558
  * @returns the expanded panel.
923
1559
  */
924
1560
  function ModelEntryPanel(props) {
925
- const { model, onChange, disabled } = props;
1561
+ const { model, onChange, disabled, api } = props;
1562
+ /**
1563
+ * Whether this panel may OFFER a control for one compat switch.
1564
+ *
1565
+ * Not the same question as `protocolOffers`, and deliberately so: an
1566
+ * unresolvable protocol answers `true` throughout, because with no protocol to
1567
+ * read nothing can be judged — hiding a control on a guess would remove a
1568
+ * switch the route may well take, while the host still names anything it
1569
+ * refuses. A resolved protocol that offers nothing (one pi-ai gives no compat
1570
+ * type) answers `false` throughout, which is exactly what the adapter says.
1571
+ */
1572
+ const mayOffer = (field) => api === void 0 || offeredCompatFields(api).includes(field);
926
1573
  const [capacityText, setCapacityText] = (0, react.useState)(() => /* @__PURE__ */ new Map());
927
1574
  /** Patch one key onto the row; `undefined` clears it. */
928
1575
  const setKey = (key, value) => {
@@ -944,21 +1591,45 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
944
1591
  const setInput = (modalities) => {
945
1592
  setKey("input", modalities.length === 0 ? void 0 : [...modalities]);
946
1593
  };
1594
+ /**
1595
+ * Apply one prefill candidate.
1596
+ *
1597
+ * Every value is *restated* from the picked source rather than merged into
1598
+ * whatever was already there: capacities, modalities, reasoning levels and
1599
+ * compat are replaced wholesale. A prefill answers "what does this catalog
1600
+ * say about the model", so an absent field clears — leaving another source's
1601
+ * value behind would describe a model neither catalog claims, and switching
1602
+ * sources then re-picking is meant to be a clean rewrite.
1603
+ *
1604
+ * Two things do not follow that rule. `id` and `name` are filled only into a
1605
+ * blank row, because a catalog id is the request id for the vendor's *own*
1606
+ * endpoint while behind a gateway the string on the wire is often different.
1607
+ * And pi-ai's level map is *mapped* rather than copied, because `null` means
1608
+ * the opposite on each side.
1609
+ * @param entry - the picked candidate.
1610
+ */
947
1611
  const applyMetadata = (entry) => {
948
1612
  const next = { ...model };
1613
+ const judgedBy = props.api ?? entry.api;
949
1614
  if (entry.context === void 0) Reflect.deleteProperty(next, "contextWindow");
950
1615
  else next["contextWindow"] = entry.context;
951
1616
  if (entry.output === void 0) Reflect.deleteProperty(next, "maxTokens");
952
1617
  else next["maxTokens"] = entry.output;
953
- const compat = { ...compatOf(model) };
954
- compat["supportsReasoningEffort"] = entry.reasoning;
955
- if (!entry.reasoning) {
956
- Reflect.deleteProperty(compat, "thinkingFormat");
1618
+ const modality = entry.input.filter((m) => m === "text" || m === "image");
1619
+ if (modality.length > 0) next["input"] = modality;
1620
+ else Reflect.deleteProperty(next, "input");
1621
+ if (entry.source === "pi-ai") {
1622
+ if (isBlank(next["id"])) next["id"] = entry.id;
1623
+ if (isBlank(next["name"]) && entry.name !== void 0) next["name"] = entry.name;
1624
+ next["reasoningEfforts"] = reasoningEffortsFrom(entry.thinkingLevelMap, judgedBy, entry.reasoning);
1625
+ const compat = compatFrom(entry.compat, judgedBy);
1626
+ if (Object.keys(compat).length === 0) Reflect.deleteProperty(next, "compat");
1627
+ else next["compat"] = compat;
1628
+ } else {
957
1629
  Reflect.deleteProperty(next, "reasoningEfforts");
1630
+ if (entry.reasoning && mayOffer("supportsReasoningEffort")) next["compat"] = { supportsReasoningEffort: true };
1631
+ else Reflect.deleteProperty(next, "compat");
958
1632
  }
959
- setKey("compat", Object.keys(compat).length === 0 ? void 0 : compat);
960
- const supported = entry.input.filter((m) => m === "text" || m === "image");
961
- if (supported.length > 0) next["input"] = supported;
962
1633
  onChange(next);
963
1634
  setCapacityText(/* @__PURE__ */ new Map());
964
1635
  };
@@ -998,8 +1669,61 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
998
1669
  };
999
1670
  const compat = compatOf(model);
1000
1671
  const developerRole = compat["supportsDeveloperRole"] !== false;
1001
- const supportsReasoningEffort = compat["supportsReasoningEffort"] === true;
1672
+ /**
1673
+ * Switches a prefill wrote that no control above stands for. Listing them
1674
+ * keeps a prefilled value from becoming invisible state: the pi-ai catalog
1675
+ * records far more of the compat surface than this panel models explicitly,
1676
+ * and that API refuses an unknown switch rather than ignoring it — so a value
1677
+ * the user cannot see is a value the user cannot fix.
1678
+ *
1679
+ * The set is the panel's base list narrowed to what this protocol takes. A
1680
+ * switch the protocol refuses keeps NO control — writing it would fail the
1681
+ * save — so it lands here instead, where it is named as refused and can be
1682
+ * cleared. That is the only way out of a row written before a protocol change
1683
+ * on the route, which no amount of hiding would repair.
1684
+ */
1685
+ const dedicated = COMPAT_BASE_FIELDS.filter((field) => mayOffer(field));
1686
+ const extraCompat = Object.entries(compat).filter(([key]) => !dedicated.includes(key)).sort(([a], [b]) => a.localeCompare(b));
1687
+ /**
1688
+ * Read an edited switch back as the type it already had.
1689
+ * @param previous - the value the switch carried when rendered.
1690
+ * @param raw - the field text.
1691
+ * @returns the coerced value, or the previous one when the text is unreadable.
1692
+ */
1693
+ const readCompatValue = (previous, raw) => {
1694
+ if (typeof previous === "boolean") return raw === "true";
1695
+ if (typeof previous === "number") {
1696
+ const parsed = Number(raw);
1697
+ return Number.isNaN(parsed) ? previous : parsed;
1698
+ }
1699
+ if (typeof previous === "string") return raw;
1700
+ try {
1701
+ return JSON.parse(raw);
1702
+ } catch {
1703
+ return previous;
1704
+ }
1705
+ };
1706
+ /**
1707
+ * The switch as a wire field: whether the endpoint takes `reasoning_effort`.
1708
+ *
1709
+ * Gated on the protocol as well as on the stored value: on a protocol that
1710
+ * does not take the switch a leftover `true` describes nothing this route can
1711
+ * act on, and the field's real home is the refused-switch list below.
1712
+ */
1713
+ const gateOn = mayOffer("supportsReasoningEffort") && compat["supportsReasoningEffort"] === true;
1002
1714
  const efforts = effortsOf(model);
1715
+ /**
1716
+ * Whether the reasoning area is worth showing.
1717
+ *
1718
+ * The switch and the area answer different questions: the switch tells the
1719
+ * adapter the endpoint accepts `reasoning_effort`, while the area edits what
1720
+ * *this model* offers. A pi-ai prefill can leave the switch unset on purpose
1721
+ * — an absent key lets the adapter auto-detect from the endpoint — while
1722
+ * still carrying levels worth editing, and those levels reach the provider
1723
+ * through the protocol's own thinking parameter either way. So the area keys
1724
+ * off either signal, not the switch alone.
1725
+ */
1726
+ const reasoningAreaShown = gateOn || efforts !== void 0 || mayOffer("thinkingFormat") && compat["thinkingFormat"] !== void 0;
1003
1727
  const toggleGate = (checked) => {
1004
1728
  if (checked) {
1005
1729
  setCompatKey("supportsReasoningEffort", true);
@@ -1078,7 +1802,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1078
1802
  }, modality))
1079
1803
  })]
1080
1804
  }),
1081
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1805
+ mayOffer("supportsDeveloperRole") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1082
1806
  className: models_plus_module_default["extGroup"],
1083
1807
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1084
1808
  className: models_plus_module_default["extCheck"],
@@ -1106,26 +1830,37 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1106
1830
  })]
1107
1831
  })]
1108
1832
  })
1109
- }),
1110
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1833
+ }) : null,
1834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1111
1835
  className: models_plus_module_default["extGroup"],
1112
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1836
+ children: [mayOffer("supportsReasoningEffort") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1113
1837
  className: models_plus_module_default["switchRow"],
1114
1838
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1115
1839
  type: "checkbox",
1116
1840
  className: models_plus_module_default["extCheckbox"],
1117
- checked: supportsReasoningEffort,
1841
+ checked: gateOn,
1118
1842
  disabled,
1119
1843
  onChange: (event) => {
1120
1844
  toggleGate(event.target.checked);
1121
1845
  }
1122
1846
  }), "端点接受推理挡位参数(reasoning_effort)"]
1123
- })
1847
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1848
+ className: models_plus_module_default["extHint"],
1849
+ children: [
1850
+ "当前协议(",
1851
+ String(api),
1852
+ ")不接受 reasoning_effort 开关,故此处不提供;下面的挡位仍通过该协议自身的 thinking 参数生效。"
1853
+ ]
1854
+ }), mayOffer("supportsReasoningEffort") && !gateOn && reasoningAreaShown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1855
+ className: models_plus_module_default["extHint"],
1856
+ style: { marginTop: 6 },
1857
+ children: "此项未设置:由 llm-pi-ai 按其 baseURL 检测结果决定是否发送 reasoning_effort; 下面的挡位不受影响,仍通过各协议自己的 thinking 参数生效。"
1858
+ }) : null]
1124
1859
  }),
1125
1860
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1126
1861
  className: models_plus_module_default["extGated"],
1127
- hidden: !supportsReasoningEffort,
1128
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1862
+ hidden: !reasoningAreaShown,
1863
+ children: [mayOffer("thinkingFormat") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1129
1864
  className: models_plus_module_default["extField"],
1130
1865
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1131
1866
  className: models_plus_module_default["extLabel"],
@@ -1145,7 +1880,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1145
1880
  children: format
1146
1881
  }, format))]
1147
1882
  })]
1148
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1883
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1149
1884
  className: models_plus_module_default["extField"],
1150
1885
  children: [
1151
1886
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1218,7 +1953,53 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1218
1953
  }) : null
1219
1954
  ]
1220
1955
  })]
1221
- })
1956
+ }),
1957
+ extraCompat.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1958
+ className: models_plus_module_default["extGroup"],
1959
+ children: [
1960
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1961
+ className: models_plus_module_default["extLabel"],
1962
+ style: { marginBottom: 6 },
1963
+ children: "其他兼容开关"
1964
+ }),
1965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1966
+ style: {
1967
+ display: "flex",
1968
+ flexDirection: "column",
1969
+ gap: 6
1970
+ },
1971
+ children: extraCompat.map(([key, value]) => {
1972
+ const refused = !mayOffer(key);
1973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1974
+ style: {
1975
+ display: "flex",
1976
+ flexDirection: "column",
1977
+ gap: 2
1978
+ },
1979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1980
+ className: refused ? models_plus_module_default["error"] : models_plus_module_default["extHint"],
1981
+ children: refused ? `${key} —— 当前协议(${String(api)})不接受此开关,请清空输入框以移除` : key
1982
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1983
+ className: models_plus_module_default["input"],
1984
+ type: "text",
1985
+ defaultValue: typeof value === "string" ? value : JSON.stringify(value) ?? "",
1986
+ "aria-label": key,
1987
+ disabled,
1988
+ onBlur: (event) => {
1989
+ const raw = event.target.value.trim();
1990
+ setCompatKey(key, raw.length === 0 ? void 0 : readCompatValue(value, raw));
1991
+ }
1992
+ })]
1993
+ }, key);
1994
+ })
1995
+ }),
1996
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1997
+ className: models_plus_module_default["extHint"],
1998
+ style: { marginTop: 6 },
1999
+ children: "这些开关由 pi-ai 目录预填;清空输入框即移除该开关,回到目录默认。"
2000
+ })
2001
+ ]
2002
+ }) : null
1222
2003
  ]
1223
2004
  });
1224
2005
  }
@@ -1236,8 +2017,8 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1236
2017
  return typeof value === "string" ? value : "";
1237
2018
  }
1238
2019
  /** Adopt one candidate, v1.0.0 defaults + the endpoint's disclosed capacities. */
1239
- function adopt(candidate) {
1240
- const row = newModelDraft();
2020
+ function adopt(candidate, routeApi) {
2021
+ const row = newModelDraft(routeApi);
1241
2022
  row["id"] = candidate.id;
1242
2023
  row["name"] = candidate.name ?? candidate.id;
1243
2024
  if (candidate.contextWindow !== void 0) row["contextWindow"] = candidate.contextWindow;
@@ -1320,7 +2101,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1320
2101
  const byId = new Map(models.map((model) => [textOf(model, "id"), model]));
1321
2102
  for (const candidate of candidates) {
1322
2103
  if (!picked.has(candidate.id)) continue;
1323
- byId.set(candidate.id, byId.get(candidate.id) ?? adopt(candidate));
2104
+ byId.set(candidate.id, byId.get(candidate.id) ?? adopt(candidate, props.routeApi));
1324
2105
  }
1325
2106
  onChange([...byId.values()]);
1326
2107
  closePicker();
@@ -1463,19 +2244,20 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1463
2244
  ]
1464
2245
  }), expanded.has(index) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelEntryPanel, {
1465
2246
  model,
2247
+ api: props.routeApi,
1466
2248
  disabled,
1467
2249
  onChange: (next) => {
1468
2250
  onChange(models.map((row, at) => at === index ? next : row));
1469
2251
  }
1470
2252
  }) : null]
1471
- }, `${String(index)}:${textOf(model, "id")}`))
2253
+ }, String(index)))
1472
2254
  }),
1473
2255
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1474
2256
  type: "button",
1475
2257
  className: models_plus_module_default["addModelButton"],
1476
2258
  disabled,
1477
2259
  onClick: () => {
1478
- onChange([...models, newModelDraft()]);
2260
+ onChange([...models, newModelDraft(props.routeApi)]);
1479
2261
  },
1480
2262
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1481
2263
  width: "14",
@@ -1625,6 +2407,22 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1625
2407
  namespace,
1626
2408
  schema
1627
2409
  ]);
2410
+ /**
2411
+ * The pi-ai catalog, loaded only on the family that needs it: it answers the
2412
+ * protocol of a route that states none (a catalog route), which is what the
2413
+ * model panel judges compat switches by.
2414
+ */
2415
+ const [prefillIndex, setPrefillIndex] = (0, react.useState)(void 0);
2416
+ (0, react.useEffect)(() => {
2417
+ if (layout !== "pi-ai") return;
2418
+ let stale = false;
2419
+ getPrefillIndex().then((loaded) => {
2420
+ if (!stale) setPrefillIndex(loaded);
2421
+ });
2422
+ return () => {
2423
+ stale = true;
2424
+ };
2425
+ }, [layout]);
1628
2426
  (0, react.useEffect)(() => {
1629
2427
  let stale = false;
1630
2428
  setKeyState(void 0);
@@ -1644,7 +2442,18 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1644
2442
  const value = next === void 0 || next.trim().length === 0 ? void 0 : next;
1645
2443
  setDraft((current) => value === void 0 ? schema.deletePath(current, [key]) : schema.setPath(current, [key], value));
1646
2444
  };
1647
- const modelFailure = validateModelRows(schema.getPath(draft, ["models"]));
2445
+ /**
2446
+ * The protocol every row of this route resolves to, resolved the way the
2447
+ * adapter resolves it: a hand-declared route's own `api` first — it overrides
2448
+ * the catalog for every model on the route — then the protocol the installed
2449
+ * catalog's entries for this route agree on, then nothing.
2450
+ *
2451
+ * The model panel needs it because the adapter judges each row's compat
2452
+ * switches by THIS protocol, not by the catalog entry the row was prefilled
2453
+ * from; without it a row can carry switches the route will refuse.
2454
+ */
2455
+ const routeApi = layout === "pi-ai" ? (props.declared === true ? stringAt(draft, "api") ?? stringAt(fallback, "api") : void 0) ?? sharedCatalogApi(prefillIndex, props.provider) : void 0;
2456
+ const modelFailure = validateModelRows(schema.getPath(draft, ["models"]), routeApi);
1648
2457
  const keyFailure = (() => {
1649
2458
  if (keyDraft.length === 0) return void 0;
1650
2459
  const value = keyDraft.trim();
@@ -1672,8 +2481,8 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1672
2481
  const applyOnce = async () => {
1673
2482
  const ns = namespace.ns;
1674
2483
  let next = layout === "pi-ai" && stringAt(draft, "apiKeyEnv") === void 0 && stringAt(fallback, "apiKeyEnv") === void 0 && keyValue.length > 0 ? schema.setPath(draft, ["apiKeyEnv"], keyRef) : draft;
1675
- const rowFailure = validateModelRows(schema.getPath(next, ["models"]));
1676
- if (rowFailure !== void 0) return `模型 ${String(rowFailure.index + 1)}:${{
2484
+ const rowFailure = validateModelRows(schema.getPath(next, ["models"]), routeApi);
2485
+ if (rowFailure !== void 0) return rowFailure.key === "modelCompatNotOffered" ? compatFailureMessage(rowFailure) : `模型 ${String(rowFailure.index + 1)}:${{
1677
2486
  modelIdRequired: "模型 ID 不能为空。",
1678
2487
  modelIdDuplicate: "模型 ID 不能重复。",
1679
2488
  modelNameInvalid: "显示名称不能为空。",
@@ -1778,7 +2587,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1778
2587
  role: "tabpanel",
1779
2588
  hidden: tab !== "provider",
1780
2589
  children: [
1781
- layout === "pi-ai" && props.declared === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2590
+ layout === "pi-ai" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1782
2591
  className: models_plus_module_default["field"],
1783
2592
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1784
2593
  className: models_plus_module_default["fieldLabel"],
@@ -1868,6 +2677,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1868
2677
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalog, {
1869
2678
  models,
1870
2679
  overridden: modelsOverridden,
2680
+ routeApi,
1871
2681
  defaultContextWindow: typeof defaultContextWindow === "number" ? defaultContextWindow : void 0,
1872
2682
  defaultMaxTokens: typeof defaultMaxTokens === "number" ? defaultMaxTokens : void 0,
1873
2683
  probe,
@@ -1890,7 +2700,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1890
2700
  }) : null,
1891
2701
  modelFailure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1892
2702
  className: models_plus_module_default["advancedHint"],
1893
- children: `模型 ${String(modelFailure.index + 1)}:${modelFailure.key}`
2703
+ children: modelFailure.key === "modelCompatNotOffered" ? compatFailureMessage(modelFailure) : `模型 ${String(modelFailure.index + 1)}:${modelFailure.key}`
1894
2704
  }),
1895
2705
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1896
2706
  className: models_plus_module_default["editorActions"],
@@ -1954,7 +2764,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1954
2764
  const profileDisabled = disabled || committed;
1955
2765
  const routeInvalid = route.length > 0 && !ROUTE_PATTERN.test(route);
1956
2766
  const routeTaken = taken.includes(route);
1957
- const modelFailure = validateModelRows(models);
2767
+ const modelFailure = validateModelRows(models, protocol);
1958
2768
  const keyFailure = (() => {
1959
2769
  if (keyDraft.length === 0) return void 0;
1960
2770
  const value = keyDraft.trim();
@@ -1966,7 +2776,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
1966
2776
  })();
1967
2777
  const keyValue = keyDraft.trim();
1968
2778
  const ready = route.length > 0 && !routeInvalid && !routeTaken && baseURL.length > 0 && models.length > 0 && modelFailure === void 0 && keyFailure === void 0;
1969
- const hint = failure !== void 0 || ready || keyFailure !== void 0 || route.length === 0 || routeInvalid || routeTaken ? void 0 : baseURL.length === 0 ? "自定义提供方需要填写 API 地址。" : modelFailure !== void 0 ? `模型 ${String(modelFailure.index + 1)}:${modelFailure.key}` : "自定义提供方至少需要一个模型。";
2779
+ const hint = failure !== void 0 || ready || keyFailure !== void 0 || route.length === 0 || routeInvalid || routeTaken ? void 0 : baseURL.length === 0 ? "自定义提供方需要填写 API 地址。" : modelFailure !== void 0 ? modelFailure.key === "modelCompatNotOffered" ? compatFailureMessage(modelFailure) : `模型 ${String(modelFailure.index + 1)}:${modelFailure.key}` : "自定义提供方至少需要一个模型。";
1970
2780
  /** Perform the create, returning a failure message or undefined. */
1971
2781
  const createOnce = async () => {
1972
2782
  const keyRef = deriveKeyRef(route);
@@ -2158,6 +2968,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
2158
2968
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalog, {
2159
2969
  models,
2160
2970
  overridden: true,
2971
+ routeApi: protocol,
2161
2972
  defaultContextWindow: void 0,
2162
2973
  defaultMaxTokens: void 0,
2163
2974
  probe: {