ei-tui 0.5.0 → 0.5.2
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/package.json +1 -1
- package/src/core/AGENTS.md +2 -2
- package/src/core/context-utils.ts +3 -4
- package/src/core/handlers/human-matching.ts +33 -21
- package/src/core/llm-client.ts +119 -39
- package/src/core/orchestrators/human-extraction.ts +0 -5
- package/src/core/processor.ts +5 -0
- package/src/core/queue-manager.ts +4 -0
- package/src/core/queue-processor.ts +1 -0
- package/src/core/state/queue.ts +7 -0
- package/src/core/state-manager.ts +233 -4
- package/src/core/tools/index.ts +1 -1
- package/src/core/types/data-items.ts +3 -1
- package/src/core/types/entities.ts +21 -4
- package/src/integrations/claude-code/importer.ts +0 -1
- package/src/integrations/claude-code/types.ts +0 -1
- package/src/integrations/opencode/importer.ts +0 -1
- package/src/storage/merge.ts +47 -2
- package/tui/src/commands/dlq.ts +12 -4
- package/tui/src/commands/provider.tsx +110 -90
- package/tui/src/commands/queue.ts +11 -3
- package/tui/src/commands/settings.tsx +9 -17
- package/tui/src/components/ModelListOverlay.tsx +203 -0
- package/tui/src/components/PromptInput.tsx +0 -2
- package/tui/src/context/ei.tsx +7 -0
- package/tui/src/util/persona-editor.tsx +15 -12
- package/tui/src/util/provider-editor.tsx +23 -6
- package/tui/src/util/yaml-serializers.ts +255 -73
- package/src/core/model-context-windows.ts +0 -49
- package/tui/src/commands/model.ts +0 -47
|
@@ -15,10 +15,11 @@ export interface EditProviderEditorResult {
|
|
|
15
15
|
success: boolean;
|
|
16
16
|
account: ProviderAccount | null;
|
|
17
17
|
cancelled: boolean;
|
|
18
|
+
deleted?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export async function createProviderViaEditor(ctx: CommandContext): Promise<NewProviderEditorResult> {
|
|
21
|
-
let yamlContent = newProviderToYAML();
|
|
21
|
+
export async function createProviderViaEditor(ctx: CommandContext, name?: string): Promise<NewProviderEditorResult> {
|
|
22
|
+
let yamlContent = newProviderToYAML(name);
|
|
22
23
|
|
|
23
24
|
while (true) {
|
|
24
25
|
const result = await spawnEditor({
|
|
@@ -45,12 +46,14 @@ export async function createProviderViaEditor(ctx: CommandContext): Promise<NewP
|
|
|
45
46
|
try {
|
|
46
47
|
const account = newProviderFromYAML(result.content);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
if ((account.models ?? []).length === 0) {
|
|
50
|
+
throw new Error("Provider must have at least one model. Add a model under the models: section.");
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
const human = await ctx.ei.getHuman();
|
|
50
54
|
const accounts = [...(human.settings?.accounts ?? []), account];
|
|
51
55
|
const updates: Partial<HumanSettings> = { accounts };
|
|
52
56
|
|
|
53
|
-
// If no system default_model, auto-set to this new provider
|
|
54
57
|
if (!human.settings?.default_model) {
|
|
55
58
|
updates.default_model = account.default_model
|
|
56
59
|
? `${account.name}:${account.default_model}`
|
|
@@ -118,9 +121,22 @@ export async function openProviderEditor(account: ProviderAccount, ctx: CommandC
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
try {
|
|
121
|
-
const
|
|
124
|
+
const parseResult = providerFromYAML(result.content, account);
|
|
125
|
+
|
|
126
|
+
if (parseResult._delete) {
|
|
127
|
+
const human = await ctx.ei.getHuman();
|
|
128
|
+
const accounts = (human.settings?.accounts ?? []).filter(a => a.id !== account.id);
|
|
129
|
+
await ctx.ei.updateSettings({ accounts });
|
|
130
|
+
ctx.showNotification(`Deleted provider "${account.name}"`, "info");
|
|
131
|
+
return { success: true, account: null, cancelled: false, deleted: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const updated = parseResult.account;
|
|
135
|
+
|
|
136
|
+
if ((updated.models ?? []).length === 0) {
|
|
137
|
+
throw new Error("Provider must have at least one model. Remove _delete: true from at least one model, or add a new model.");
|
|
138
|
+
}
|
|
122
139
|
|
|
123
|
-
// Update in settings
|
|
124
140
|
const human = await ctx.ei.getHuman();
|
|
125
141
|
const accounts = [...(human.settings?.accounts ?? [])];
|
|
126
142
|
const idx = accounts.findIndex(a => a.id === account.id);
|
|
@@ -164,3 +180,4 @@ export async function openProviderEditor(account: ProviderAccount, ctx: CommandC
|
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
182
|
}
|
|
183
|
+
|
|
@@ -254,7 +254,7 @@ export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinitio
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[]): string {
|
|
257
|
+
export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[], accounts?: ProviderAccount[]): string {
|
|
258
258
|
const useTraitPlaceholder = persona.traits.length === 0;
|
|
259
259
|
const useTopicPlaceholder = persona.topics.length === 0;
|
|
260
260
|
|
|
@@ -265,15 +265,18 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
|
|
|
265
265
|
groupsForYAML.push({ [groupName]: visibleSet.has(groupName) });
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
// Build tools map: all known tools, true if persona has it enabled
|
|
269
268
|
const toolsMap = buildPersonaToolsMap(persona.tools ?? [], allTools ?? [], allProviders ?? []);
|
|
269
|
+
|
|
270
|
+
const modelDisplay = (persona.model && accounts && accounts.length > 0)
|
|
271
|
+
? modelGuidToDisplay(persona.model, accounts)
|
|
272
|
+
: persona.model;
|
|
270
273
|
|
|
271
274
|
const data: EditablePersonaData = {
|
|
272
275
|
display_name: persona.display_name,
|
|
273
276
|
aliases: persona.aliases,
|
|
274
277
|
short_description: persona.short_description,
|
|
275
278
|
long_description: persona.long_description || PLACEHOLDER_LONG_DESC,
|
|
276
|
-
model:
|
|
279
|
+
model: modelDisplay,
|
|
277
280
|
group_primary: persona.group_primary,
|
|
278
281
|
groups_visible: groupsForYAML,
|
|
279
282
|
traits: useTraitPlaceholder
|
|
@@ -304,7 +307,7 @@ export interface PersonaYAMLResult {
|
|
|
304
307
|
deletedTopicIds: string[];
|
|
305
308
|
}
|
|
306
309
|
|
|
307
|
-
export function personaFromYAML(yamlContent: string, original: PersonaEntity, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[]): PersonaYAMLResult {
|
|
310
|
+
export function personaFromYAML(yamlContent: string, original: PersonaEntity, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[], accounts?: ProviderAccount[]): PersonaYAMLResult {
|
|
308
311
|
const data = YAML.parse(yamlContent) as EditablePersonaData;
|
|
309
312
|
|
|
310
313
|
const deletedTraitIds: string[] = [];
|
|
@@ -377,13 +380,23 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
|
|
|
377
380
|
}
|
|
378
381
|
}
|
|
379
382
|
}
|
|
383
|
+
|
|
384
|
+
let resolvedModel: string | undefined = data.model;
|
|
385
|
+
if (data.model && accounts && accounts.length > 0) {
|
|
386
|
+
const guid = displayToModelGuid(data.model, accounts);
|
|
387
|
+
if (guid !== undefined) {
|
|
388
|
+
resolvedModel = guid;
|
|
389
|
+
} else if (data.model.includes(':')) {
|
|
390
|
+
throw new Error(`Model "${data.model}" not found. Use "ProviderName:modelName" format with a valid provider and model.`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
380
393
|
|
|
381
394
|
const updates: Partial<PersonaEntity> = {
|
|
382
395
|
display_name: data.display_name,
|
|
383
396
|
aliases: data.aliases,
|
|
384
397
|
short_description: data.short_description,
|
|
385
398
|
long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
|
|
386
|
-
model:
|
|
399
|
+
model: resolvedModel,
|
|
387
400
|
group_primary: data.group_primary,
|
|
388
401
|
groups_visible: groupsVisible,
|
|
389
402
|
traits,
|
|
@@ -524,7 +537,6 @@ interface EditableSettingsData {
|
|
|
524
537
|
last_sync?: string | null;
|
|
525
538
|
extraction_point?: string | null;
|
|
526
539
|
extraction_model?: string | null;
|
|
527
|
-
extraction_token_limit?: string | number | null;
|
|
528
540
|
};
|
|
529
541
|
claudeCode?: {
|
|
530
542
|
integration?: boolean | null;
|
|
@@ -532,7 +544,6 @@ interface EditableSettingsData {
|
|
|
532
544
|
last_sync?: string | null;
|
|
533
545
|
extraction_point?: string | null;
|
|
534
546
|
extraction_model?: string | null;
|
|
535
|
-
extraction_token_limit?: string | number | null;
|
|
536
547
|
};
|
|
537
548
|
cursor?: {
|
|
538
549
|
integration?: boolean | null;
|
|
@@ -547,12 +558,17 @@ interface EditableSettingsData {
|
|
|
547
558
|
};
|
|
548
559
|
}
|
|
549
560
|
|
|
550
|
-
export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
561
|
+
export function settingsToYAML(settings: HumanSettings | undefined, accounts: ProviderAccount[]): string {
|
|
562
|
+
const guidToDisplay = (guid: string | undefined | null): string | null => {
|
|
563
|
+
if (!guid) return null;
|
|
564
|
+
return modelGuidToDisplay(guid, accounts);
|
|
565
|
+
};
|
|
566
|
+
|
|
551
567
|
// Always show all editable fields, using null for unset values so YAML displays them
|
|
552
568
|
const data: EditableSettingsData = {
|
|
553
|
-
default_model: settings?.default_model
|
|
554
|
-
oneshot_model: settings?.oneshot_model
|
|
555
|
-
rewrite_model: settings?.rewrite_model
|
|
569
|
+
default_model: guidToDisplay(settings?.default_model),
|
|
570
|
+
oneshot_model: guidToDisplay(settings?.oneshot_model),
|
|
571
|
+
rewrite_model: guidToDisplay(settings?.rewrite_model),
|
|
556
572
|
time_mode: settings?.time_mode ?? null,
|
|
557
573
|
name_display: settings?.name_display ?? null,
|
|
558
574
|
default_heartbeat_ms: settings?.default_heartbeat_ms ?? 1800000,
|
|
@@ -569,16 +585,14 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
|
569
585
|
opencode: {
|
|
570
586
|
integration: settings?.opencode?.integration ?? false,
|
|
571
587
|
polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 60000,
|
|
572
|
-
extraction_model: settings?.opencode?.extraction_model ?? 'default',
|
|
573
|
-
extraction_token_limit: settings?.opencode?.extraction_token_limit ?? 'default',
|
|
588
|
+
extraction_model: guidToDisplay(settings?.opencode?.extraction_model) ?? 'default',
|
|
574
589
|
last_sync: settings?.opencode?.last_sync ?? null,
|
|
575
590
|
extraction_point: settings?.opencode?.extraction_point ?? null,
|
|
576
591
|
},
|
|
577
592
|
claudeCode: {
|
|
578
593
|
integration: settings?.claudeCode?.integration ?? false,
|
|
579
594
|
polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 60000,
|
|
580
|
-
extraction_model: settings?.claudeCode?.extraction_model ?? 'default',
|
|
581
|
-
extraction_token_limit: settings?.claudeCode?.extraction_token_limit ?? 'default',
|
|
595
|
+
extraction_model: guidToDisplay(settings?.claudeCode?.extraction_model) ?? 'default',
|
|
582
596
|
last_sync: settings?.claudeCode?.last_sync ?? null,
|
|
583
597
|
extraction_point: settings?.claudeCode?.extraction_point ?? null,
|
|
584
598
|
},
|
|
@@ -600,14 +614,18 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
|
600
614
|
})
|
|
601
615
|
.replace(/^(\s+)(last_sync: .+)$/mg, '$1# [read-only] $2')
|
|
602
616
|
.replace(/^(\s+)(extraction_point: .+)$/mg, '$1# [read-only] $2')
|
|
603
|
-
.replace(/^(\s+)(extraction_model: .+)$/mg, '$1$2 # e.g. Anthropic:claude-haiku-4-5')
|
|
604
|
-
.replace(/^(\s+)(extraction_token_limit: .+)$/mg, '$1$2 # e.g. 100000 for Haiku');
|
|
617
|
+
.replace(/^(\s+)(extraction_model: .+)$/mg, '$1$2 # e.g. Anthropic:claude-haiku-4-5');
|
|
605
618
|
}
|
|
606
|
-
export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined): HumanSettings {
|
|
619
|
+
export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined, accounts: ProviderAccount[]): HumanSettings {
|
|
607
620
|
const data = YAML.parse(yamlContent) as EditableSettingsData;
|
|
608
621
|
|
|
609
622
|
const nullToUndefined = <T>(value: T | null | undefined): T | undefined =>
|
|
610
623
|
value === null ? undefined : value;
|
|
624
|
+
|
|
625
|
+
const displayToGuid = (display: string | null | undefined): string | undefined => {
|
|
626
|
+
if (!display || display === 'default') return undefined;
|
|
627
|
+
return displayToModelGuid(display, accounts) ?? display;
|
|
628
|
+
};
|
|
611
629
|
|
|
612
630
|
let ceremony: CeremonyConfig | undefined;
|
|
613
631
|
if (data.ceremony) {
|
|
@@ -629,12 +647,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
629
647
|
last_sync: original?.opencode?.last_sync,
|
|
630
648
|
extraction_point: original?.opencode?.extraction_point,
|
|
631
649
|
processed_sessions: original?.opencode?.processed_sessions,
|
|
632
|
-
extraction_model: (data.opencode.extraction_model
|
|
633
|
-
? data.opencode.extraction_model
|
|
634
|
-
: undefined,
|
|
635
|
-
extraction_token_limit: (data.opencode.extraction_token_limit != null && data.opencode.extraction_token_limit !== 'default')
|
|
636
|
-
? Number(data.opencode.extraction_token_limit)
|
|
637
|
-
: undefined,
|
|
650
|
+
extraction_model: displayToGuid(data.opencode.extraction_model),
|
|
638
651
|
};
|
|
639
652
|
}
|
|
640
653
|
|
|
@@ -646,12 +659,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
646
659
|
last_sync: original?.claudeCode?.last_sync,
|
|
647
660
|
extraction_point: original?.claudeCode?.extraction_point,
|
|
648
661
|
processed_sessions: original?.claudeCode?.processed_sessions,
|
|
649
|
-
extraction_model: (data.claudeCode.extraction_model
|
|
650
|
-
? data.claudeCode.extraction_model
|
|
651
|
-
: undefined,
|
|
652
|
-
extraction_token_limit: (data.claudeCode.extraction_token_limit != null && data.claudeCode.extraction_token_limit !== 'default')
|
|
653
|
-
? Number(data.claudeCode.extraction_token_limit)
|
|
654
|
-
: undefined,
|
|
662
|
+
extraction_model: displayToGuid(data.claudeCode.extraction_model),
|
|
655
663
|
};
|
|
656
664
|
}
|
|
657
665
|
|
|
@@ -678,9 +686,9 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
678
686
|
|
|
679
687
|
return {
|
|
680
688
|
...original,
|
|
681
|
-
default_model:
|
|
682
|
-
oneshot_model:
|
|
683
|
-
rewrite_model:
|
|
689
|
+
default_model: displayToGuid(data.default_model),
|
|
690
|
+
oneshot_model: displayToGuid(data.oneshot_model),
|
|
691
|
+
rewrite_model: displayToGuid(data.rewrite_model),
|
|
684
692
|
time_mode: nullToUndefined(data.time_mode),
|
|
685
693
|
name_display: nullToUndefined(data.name_display),
|
|
686
694
|
default_heartbeat_ms: nullToUndefined(data.default_heartbeat_ms),
|
|
@@ -781,6 +789,13 @@ export function quotesFromYAML(yamlContent: string): QuotesYAMLResult {
|
|
|
781
789
|
// PROVIDER ACCOUNT SERIALIZATION
|
|
782
790
|
// =============================================================================
|
|
783
791
|
|
|
792
|
+
interface EditableModelData {
|
|
793
|
+
name: string;
|
|
794
|
+
token_limit?: number;
|
|
795
|
+
max_output_tokens?: number;
|
|
796
|
+
_delete?: boolean;
|
|
797
|
+
}
|
|
798
|
+
|
|
784
799
|
interface EditableProviderData {
|
|
785
800
|
name: string;
|
|
786
801
|
type: "llm" | "storage";
|
|
@@ -790,50 +805,71 @@ interface EditableProviderData {
|
|
|
790
805
|
token_limit?: number | null;
|
|
791
806
|
extra_headers?: Record<string, string>;
|
|
792
807
|
enabled?: boolean;
|
|
808
|
+
models?: EditableModelData[];
|
|
809
|
+
_delete?: boolean;
|
|
793
810
|
}
|
|
794
811
|
|
|
812
|
+
export interface ProviderYAMLResult {
|
|
813
|
+
account: ProviderAccount;
|
|
814
|
+
_delete: boolean;
|
|
815
|
+
}
|
|
795
816
|
|
|
796
817
|
function resolveEnvVar(value: string | undefined): string | undefined {
|
|
797
818
|
if (!value || !value.startsWith("$")) return value;
|
|
798
819
|
const varName = value.slice(1);
|
|
799
820
|
return process.env[varName] || value;
|
|
800
821
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
default_model: "model-name",
|
|
807
|
-
token_limit: null,
|
|
808
|
-
extra_headers: {},
|
|
809
|
-
enabled: true,
|
|
810
|
-
};
|
|
822
|
+
|
|
823
|
+
const PLACEHOLDER_PROVIDER_NAME = "My Provider";
|
|
824
|
+
const PLACEHOLDER_PROVIDER_URL = "https://api.example.com/v1";
|
|
825
|
+
const PLACEHOLDER_PROVIDER_API_KEY = "your-api-key-or-$ENVAR";
|
|
826
|
+
const PLACEHOLDER_PROVIDER_DEFAULT_MODEL = "model-name";
|
|
811
827
|
|
|
812
828
|
/**
|
|
813
829
|
* Generate YAML template for a NEW provider account
|
|
814
830
|
*/
|
|
815
|
-
export function newProviderToYAML(): string {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
831
|
+
export function newProviderToYAML(name?: string): string {
|
|
832
|
+
const placeholderData = {
|
|
833
|
+
name: name ?? PLACEHOLDER_PROVIDER_NAME,
|
|
834
|
+
type: "llm",
|
|
835
|
+
url: PLACEHOLDER_PROVIDER_URL,
|
|
836
|
+
api_key: PLACEHOLDER_PROVIDER_API_KEY,
|
|
837
|
+
default_model: PLACEHOLDER_PROVIDER_DEFAULT_MODEL,
|
|
838
|
+
token_limit: null,
|
|
839
|
+
extra_headers: {},
|
|
840
|
+
enabled: true,
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const modelsYAML = [
|
|
844
|
+
"models:",
|
|
845
|
+
" - name: (default)",
|
|
846
|
+
" # _delete: true",
|
|
847
|
+
"# _delete: true # Delete this entire provider",
|
|
848
|
+
].join("\n");
|
|
849
|
+
|
|
850
|
+
return YAML.stringify(placeholderData, { lineWidth: 0 }).trimEnd() + "\n" + modelsYAML + "\n";
|
|
819
851
|
}
|
|
820
852
|
|
|
821
853
|
/**
|
|
822
854
|
* Parse YAML for a NEW provider account
|
|
823
855
|
*/
|
|
824
856
|
export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
825
|
-
const
|
|
857
|
+
const cleaned = yamlContent
|
|
858
|
+
.split('\n')
|
|
859
|
+
.filter(line => !/^\s*#/.test(line))
|
|
860
|
+
.join('\n');
|
|
861
|
+
const data = YAML.parse(cleaned) as EditableProviderData;
|
|
826
862
|
|
|
827
|
-
if (!data.name || data.name ===
|
|
863
|
+
if (!data.name || data.name === PLACEHOLDER_PROVIDER_NAME) {
|
|
828
864
|
throw new Error("Provider name is required");
|
|
829
865
|
}
|
|
830
|
-
if (!data.url || data.url ===
|
|
866
|
+
if (!data.url || data.url === PLACEHOLDER_PROVIDER_URL) {
|
|
831
867
|
throw new Error("Provider URL is required");
|
|
832
868
|
}
|
|
833
|
-
if (data.api_key ===
|
|
869
|
+
if (data.api_key === PLACEHOLDER_PROVIDER_API_KEY) {
|
|
834
870
|
data.api_key = undefined;
|
|
835
871
|
}
|
|
836
|
-
if (data.default_model ===
|
|
872
|
+
if (data.default_model === PLACEHOLDER_PROVIDER_DEFAULT_MODEL) {
|
|
837
873
|
data.default_model = undefined;
|
|
838
874
|
}
|
|
839
875
|
|
|
@@ -841,6 +877,9 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
|
841
877
|
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
842
878
|
}
|
|
843
879
|
|
|
880
|
+
// Parse models: filter out _delete:true items
|
|
881
|
+
const models = parseModels(data.models ?? []);
|
|
882
|
+
|
|
844
883
|
return {
|
|
845
884
|
id: crypto.randomUUID(),
|
|
846
885
|
name: data.name,
|
|
@@ -851,35 +890,86 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
|
851
890
|
token_limit: data.token_limit ?? undefined,
|
|
852
891
|
extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
|
|
853
892
|
enabled: data.enabled ?? true,
|
|
893
|
+
models: models.length > 0 ? models : undefined,
|
|
854
894
|
created_at: new Date().toISOString(),
|
|
855
895
|
};
|
|
856
896
|
}
|
|
857
897
|
|
|
858
898
|
/**
|
|
859
|
-
*
|
|
899
|
+
* Parse EditableModelData[] into ModelConfig[], generating new GUIDs for models without one.
|
|
900
|
+
* Filters out models with _delete: true.
|
|
901
|
+
*/
|
|
902
|
+
function parseModels(editableModels: EditableModelData[]): import('../../../src/core/types.js').ModelConfig[] {
|
|
903
|
+
const result: import('../../../src/core/types.js').ModelConfig[] = [];
|
|
904
|
+
for (const m of editableModels) {
|
|
905
|
+
if (m._delete) continue;
|
|
906
|
+
result.push({
|
|
907
|
+
id: crypto.randomUUID(),
|
|
908
|
+
name: m.name,
|
|
909
|
+
token_limit: m.token_limit,
|
|
910
|
+
max_output_tokens: m.max_output_tokens,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Serialize existing provider account to YAML for editing.
|
|
917
|
+
* Shows models[] as a nested section with _delete comments.
|
|
918
|
+
* Hides internal fields: id, total_calls, total_tokens_in, total_tokens_out, last_used.
|
|
860
919
|
*/
|
|
861
920
|
export function providerToYAML(account: ProviderAccount): string {
|
|
862
|
-
const
|
|
921
|
+
const defaultModelDisplay = account.default_model
|
|
922
|
+
? modelGuidToDisplay(account.default_model, [account])
|
|
923
|
+
: undefined;
|
|
924
|
+
|
|
925
|
+
const topData = {
|
|
863
926
|
name: account.name,
|
|
864
927
|
type: account.type as "llm" | "storage",
|
|
865
928
|
url: account.url,
|
|
866
929
|
api_key: account.api_key,
|
|
867
|
-
default_model:
|
|
930
|
+
default_model: defaultModelDisplay,
|
|
868
931
|
token_limit: account.token_limit ?? null,
|
|
869
932
|
extra_headers: account.extra_headers,
|
|
870
933
|
enabled: account.enabled ?? true,
|
|
871
934
|
};
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
935
|
+
|
|
936
|
+
const topYAML = YAML.stringify(topData, { lineWidth: 0 }).trimEnd();
|
|
937
|
+
|
|
938
|
+
const modelLines: string[] = ["models:"];
|
|
939
|
+
const modelList = account.models ?? [];
|
|
940
|
+
if (modelList.length > 0) {
|
|
941
|
+
for (const m of modelList) {
|
|
942
|
+
modelLines.push(` - name: ${m.name}`);
|
|
943
|
+
if (m.token_limit !== undefined) {
|
|
944
|
+
modelLines.push(` token_limit: ${m.token_limit}`);
|
|
945
|
+
}
|
|
946
|
+
if (m.max_output_tokens !== undefined) {
|
|
947
|
+
modelLines.push(` max_output_tokens: ${m.max_output_tokens}`);
|
|
948
|
+
}
|
|
949
|
+
modelLines.push(` _delete: false`);
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
modelLines.push(" - name: (default)");
|
|
953
|
+
modelLines.push(" _delete: false");
|
|
954
|
+
}
|
|
955
|
+
modelLines.push("_delete: false # Set to true to delete this entire provider");
|
|
956
|
+
|
|
957
|
+
return topYAML + "\n" + modelLines.join("\n") + "\n";
|
|
876
958
|
}
|
|
877
959
|
|
|
878
960
|
/**
|
|
879
|
-
* Parse YAML for an existing provider account (preserves id and created_at)
|
|
961
|
+
* Parse YAML for an existing provider account (preserves id and created_at).
|
|
962
|
+
* Returns { account, _delete } where _delete signals the entire provider should be removed.
|
|
963
|
+
* Model _delete flags cause individual models to be removed.
|
|
964
|
+
* Preserves model GUIDs on round-trip; generates new GUIDs for new models.
|
|
880
965
|
*/
|
|
881
|
-
export function providerFromYAML(yamlContent: string, original: ProviderAccount):
|
|
882
|
-
|
|
966
|
+
export function providerFromYAML(yamlContent: string, original: ProviderAccount): ProviderYAMLResult {
|
|
967
|
+
// Strip comment lines before parsing (they encode _delete hints)
|
|
968
|
+
const cleaned = yamlContent
|
|
969
|
+
.split('\n')
|
|
970
|
+
.filter(line => !/^\s*#/.test(line))
|
|
971
|
+
.join('\n');
|
|
972
|
+
const data = YAML.parse(cleaned) as EditableProviderData;
|
|
883
973
|
|
|
884
974
|
if (!data.name) {
|
|
885
975
|
throw new Error("Provider name is required");
|
|
@@ -892,7 +982,34 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
892
982
|
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
893
983
|
}
|
|
894
984
|
|
|
895
|
-
|
|
985
|
+
// Root-level _delete → signal deletion of entire provider
|
|
986
|
+
if (data._delete) {
|
|
987
|
+
return {
|
|
988
|
+
account: original,
|
|
989
|
+
_delete: true,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Parse models: preserve existing GUIDs by name match, generate new GUIDs for new models
|
|
994
|
+
const existingModels = original.models ?? [];
|
|
995
|
+
const parsedModels: import('../../../src/core/types.js').ModelConfig[] = [];
|
|
996
|
+
for (const m of data.models ?? []) {
|
|
997
|
+
if (m._delete) continue;
|
|
998
|
+
const existing = existingModels.find(em => em.name === m.name);
|
|
999
|
+
parsedModels.push({
|
|
1000
|
+
id: existing?.id ?? crypto.randomUUID(),
|
|
1001
|
+
name: m.name,
|
|
1002
|
+
token_limit: m.token_limit,
|
|
1003
|
+
max_output_tokens: m.max_output_tokens,
|
|
1004
|
+
// Preserve usage counters from original if model matched
|
|
1005
|
+
total_calls: existing?.total_calls,
|
|
1006
|
+
total_tokens_in: existing?.total_tokens_in,
|
|
1007
|
+
total_tokens_out: existing?.total_tokens_out,
|
|
1008
|
+
last_used: existing?.last_used,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const account: ProviderAccount = {
|
|
896
1013
|
id: original.id,
|
|
897
1014
|
name: data.name,
|
|
898
1015
|
type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
|
|
@@ -902,8 +1019,42 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
902
1019
|
token_limit: data.token_limit ?? undefined,
|
|
903
1020
|
extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
|
|
904
1021
|
enabled: data.enabled ?? true,
|
|
1022
|
+
models: parsedModels.length > 0 ? parsedModels : undefined,
|
|
905
1023
|
created_at: original.created_at,
|
|
906
1024
|
};
|
|
1025
|
+
|
|
1026
|
+
return { account, _delete: false };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// =============================================================================
|
|
1030
|
+
// GUID <-> DISPLAY NAME HELPERS
|
|
1031
|
+
// =============================================================================
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Convert a model GUID to "ProviderName:modelName" display string.
|
|
1035
|
+
* Falls back to the raw GUID if the model is not found.
|
|
1036
|
+
*/
|
|
1037
|
+
export function modelGuidToDisplay(guid: string, accounts: ProviderAccount[]): string {
|
|
1038
|
+
for (const account of accounts) {
|
|
1039
|
+
const model = (account.models ?? []).find(m => m.id === guid);
|
|
1040
|
+
if (model) return `${account.name}:${model.name}`;
|
|
1041
|
+
}
|
|
1042
|
+
return guid; // fallback: return raw GUID if not found
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Resolve "ProviderName:modelName" display string back to a model GUID.
|
|
1047
|
+
* Returns undefined if no matching provider+model is found.
|
|
1048
|
+
* Handles colons in model names by treating everything after the first colon as the model name.
|
|
1049
|
+
*/
|
|
1050
|
+
export function displayToModelGuid(display: string, accounts: ProviderAccount[]): string | undefined {
|
|
1051
|
+
const colonIdx = display.indexOf(':');
|
|
1052
|
+
if (colonIdx < 0) return undefined;
|
|
1053
|
+
const providerName = display.substring(0, colonIdx);
|
|
1054
|
+
const modelName = display.substring(colonIdx + 1);
|
|
1055
|
+
const account = accounts.find(a => a.name === providerName);
|
|
1056
|
+
const model = (account?.models ?? []).find(m => m.name === modelName);
|
|
1057
|
+
return model?.id;
|
|
907
1058
|
}
|
|
908
1059
|
|
|
909
1060
|
// =============================================================================
|
|
@@ -971,9 +1122,25 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
|
971
1122
|
// QUEUE ITEM YAML
|
|
972
1123
|
// =============================================================================
|
|
973
1124
|
|
|
974
|
-
|
|
975
|
-
|
|
1125
|
+
interface EditableQueueItem {
|
|
1126
|
+
id: string;
|
|
1127
|
+
state: LLMRequestState;
|
|
1128
|
+
created_at: string;
|
|
1129
|
+
attempts: number;
|
|
1130
|
+
last_attempt?: string;
|
|
1131
|
+
retry_after?: string;
|
|
1132
|
+
type?: string;
|
|
1133
|
+
priority?: LLMPriority;
|
|
1134
|
+
next_step?: string;
|
|
1135
|
+
model?: string;
|
|
1136
|
+
data?: Record<string, unknown>;
|
|
1137
|
+
_delete?: boolean;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export function queueItemsToYAML(items: LLMRequest[], accounts: ProviderAccount[]): string {
|
|
1141
|
+
const data: EditableQueueItem[] = items.map(item => ({
|
|
976
1142
|
id: item.id,
|
|
1143
|
+
_delete: false,
|
|
977
1144
|
state: item.state,
|
|
978
1145
|
created_at: item.created_at,
|
|
979
1146
|
attempts: item.attempts,
|
|
@@ -982,7 +1149,7 @@ export function queueItemsToYAML(items: LLMRequest[]): string {
|
|
|
982
1149
|
type: item.type,
|
|
983
1150
|
priority: item.priority,
|
|
984
1151
|
next_step: item.next_step,
|
|
985
|
-
model: item.model,
|
|
1152
|
+
model: item.model ? modelGuidToDisplay(item.model, accounts) : undefined,
|
|
986
1153
|
data: item.data,
|
|
987
1154
|
// NOTE: system/user prompts omitted (large); to requeue: set state='pending', attempts=0
|
|
988
1155
|
}));
|
|
@@ -998,25 +1165,40 @@ export interface QueueItemUpdate {
|
|
|
998
1165
|
data?: Record<string, unknown>;
|
|
999
1166
|
}
|
|
1000
1167
|
|
|
1001
|
-
export
|
|
1002
|
-
|
|
1168
|
+
export interface QueueItemsYAMLResult {
|
|
1169
|
+
updates: QueueItemUpdate[];
|
|
1170
|
+
deletedIds: string[];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function queueItemsFromYAML(yamlContent: string, accounts: ProviderAccount[]): QueueItemsYAMLResult {
|
|
1174
|
+
const data = YAML.parse(yamlContent) as EditableQueueItem[];
|
|
1003
1175
|
if (!Array.isArray(data)) throw new Error("Expected a YAML array of queue items");
|
|
1004
|
-
|
|
1176
|
+
|
|
1177
|
+
const deletedIds: string[] = [];
|
|
1178
|
+
const updates: QueueItemUpdate[] = [];
|
|
1179
|
+
|
|
1180
|
+
for (const item of data) {
|
|
1005
1181
|
if (!item.id) throw new Error(`Queue item missing 'id' field`);
|
|
1182
|
+
if (item._delete) {
|
|
1183
|
+
deletedIds.push(item.id);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1006
1186
|
if (!item.state) throw new Error(`Queue item ${item.id} missing 'state' field`);
|
|
1007
1187
|
const validStates: LLMRequestState[] = ["pending", "processing", "dlq"];
|
|
1008
1188
|
if (!validStates.includes(item.state)) {
|
|
1009
1189
|
throw new Error(`Queue item ${item.id} has invalid state '${item.state}'. Valid: ${validStates.join(", ")}`);
|
|
1010
1190
|
}
|
|
1011
|
-
|
|
1191
|
+
updates.push({
|
|
1012
1192
|
id: item.id,
|
|
1013
1193
|
state: item.state,
|
|
1014
1194
|
attempts: typeof item.attempts === "number" ? item.attempts : 0,
|
|
1015
|
-
model: item.model,
|
|
1195
|
+
model: item.model ? displayToModelGuid(item.model, accounts) ?? item.model : undefined,
|
|
1016
1196
|
priority: item.priority,
|
|
1017
1197
|
data: item.data,
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return { updates, deletedIds };
|
|
1020
1202
|
}
|
|
1021
1203
|
|
|
1022
1204
|
// =============================================================================
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Last updated: 2026-02-22
|
|
2
|
-
// Prefix-based lookup: "gpt-4o" matches "gpt-4o", "gpt-4o-2024-08-06", "gpt-4o-mini", etc.
|
|
3
|
-
const KNOWN_CONTEXT_WINDOWS: [string, number][] = [
|
|
4
|
-
// OpenAI
|
|
5
|
-
["gpt-4.1", 1_048_576],
|
|
6
|
-
["gpt-4o", 128_000],
|
|
7
|
-
["gpt-3.5-turbo", 16_384],
|
|
8
|
-
|
|
9
|
-
// Anthropic
|
|
10
|
-
["claude-opus-4", 200_000],
|
|
11
|
-
["claude-sonnet-4", 200_000],
|
|
12
|
-
["claude-3.5", 200_000],
|
|
13
|
-
["claude-3", 200_000],
|
|
14
|
-
|
|
15
|
-
// Google
|
|
16
|
-
["gemini-2.5", 1_000_000],
|
|
17
|
-
["gemini-2.0", 1_000_000],
|
|
18
|
-
["gemini-1.5", 1_000_000],
|
|
19
|
-
|
|
20
|
-
// Meta Llama
|
|
21
|
-
["llama-3.3", 131_072],
|
|
22
|
-
["llama-3.2", 131_072],
|
|
23
|
-
["llama-3.1", 131_072],
|
|
24
|
-
|
|
25
|
-
// Mistral
|
|
26
|
-
["mixtral", 32_768],
|
|
27
|
-
["mistral", 32_768],
|
|
28
|
-
|
|
29
|
-
// DeepSeek
|
|
30
|
-
["deepseek-coder-v2", 163_840],
|
|
31
|
-
["deepseek-v3", 131_072],
|
|
32
|
-
["deepseek", 131_072],
|
|
33
|
-
|
|
34
|
-
// Qwen
|
|
35
|
-
["qwen-2.5", 131_072],
|
|
36
|
-
["qwen", 131_072],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const DEFAULT_TOKEN_LIMIT = 8192;
|
|
40
|
-
|
|
41
|
-
export function getKnownContextWindow(modelName: string): number | undefined {
|
|
42
|
-
const lower = modelName.toLowerCase();
|
|
43
|
-
for (const [prefix, tokens] of KNOWN_CONTEXT_WINDOWS) {
|
|
44
|
-
if (lower.startsWith(prefix.toLowerCase())) return tokens;
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export { DEFAULT_TOKEN_LIMIT };
|