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.
@@ -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
- // Save to settings
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 updated = providerFromYAML(result.content, account);
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: persona.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: data.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 ?? null,
554
- oneshot_model: settings?.oneshot_model ?? null,
555
- rewrite_model: settings?.rewrite_model ?? null,
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 != null && data.opencode.extraction_model !== 'default')
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 != null && data.claudeCode.extraction_model !== 'default')
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: nullToUndefined(data.default_model),
682
- oneshot_model: nullToUndefined(data.oneshot_model),
683
- rewrite_model: nullToUndefined(data.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
- const PLACEHOLDER_PROVIDER: EditableProviderData = {
802
- name: "My Provider",
803
- type: "llm",
804
- url: "https://api.example.com/v1",
805
- api_key: "your-api-key-or-$ENVAR",
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
- return YAML.stringify(PLACEHOLDER_PROVIDER, {
817
- lineWidth: 0,
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 data = YAML.parse(yamlContent) as EditableProviderData;
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 === PLACEHOLDER_PROVIDER.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 === PLACEHOLDER_PROVIDER.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 === PLACEHOLDER_PROVIDER.api_key) {
869
+ if (data.api_key === PLACEHOLDER_PROVIDER_API_KEY) {
834
870
  data.api_key = undefined;
835
871
  }
836
- if (data.default_model === PLACEHOLDER_PROVIDER.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
- * Serialize existing provider account to YAML for editing
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 data: EditableProviderData = {
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: account.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
- return YAML.stringify(data, {
874
- lineWidth: 0,
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): ProviderAccount {
882
- const data = YAML.parse(yamlContent) as EditableProviderData;
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
- return {
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
- export function queueItemsToYAML(items: LLMRequest[]): string {
975
- const data = items.map(item => ({
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 function queueItemsFromYAML(yamlContent: string): QueueItemUpdate[] {
1002
- const data = YAML.parse(yamlContent) as QueueItemUpdate[];
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
- return data.map(item => {
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
- return {
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 };