@ottocode/sdk 0.1.247 → 0.1.249

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.247",
3
+ "version": "0.1.249",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -110,18 +110,24 @@ export function getLocalDataDir(projectRoot: string): string {
110
110
  return joinPath(projectRoot, '.otto');
111
111
  }
112
112
 
113
+ async function loadFsPromises(): Promise<typeof import('node:fs/promises')> {
114
+ return Function('specifier', 'return import(specifier)')('node:fs/promises');
115
+ }
116
+
113
117
  export async function ensureDir(dir: string) {
114
- try {
115
- // Attempt to create a marker file to ensure directory exists
116
- await Bun.write(joinPath(dir, '.keep'), '');
117
- } catch {
118
- const { promises: fs } = await import('node:fs');
119
- await fs.mkdir(dir, { recursive: true }).catch(() => {});
120
- }
118
+ const { mkdir, writeFile } = await loadFsPromises();
119
+ await mkdir(dir, { recursive: true }).catch(() => {});
120
+ await writeFile(joinPath(dir, '.keep'), '').catch(() => {});
121
121
  }
122
122
 
123
123
  export async function fileExists(p: string) {
124
- return await Bun.file(p).exists();
124
+ try {
125
+ const { access } = await loadFsPromises();
126
+ await access(p);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
125
131
  }
126
132
 
127
133
  export { joinPath };
package/src/index.ts CHANGED
@@ -40,6 +40,20 @@ export type {
40
40
  // Providers (from internal providers module)
41
41
  // =======================
42
42
  export { catalog } from './providers/src/index.ts';
43
+ export {
44
+ DEFAULT_REMOTE_MODEL_CATALOG_URL,
45
+ getCachedProviderCatalogEntry,
46
+ getModelCatalogCachePath,
47
+ mergeCachedModelCatalog,
48
+ normalizeModelCatalogPayload,
49
+ readCachedModelCatalog,
50
+ readCachedModelCatalogSync,
51
+ writeCachedModelCatalog,
52
+ } from './providers/src/index.ts';
53
+ export type {
54
+ CachedModelCatalog,
55
+ CachedProviderCatalogEntry,
56
+ } from './providers/src/index.ts';
43
57
  export {
44
58
  isProviderId,
45
59
  providerIds,
@@ -104,7 +104,7 @@ function buildOllamaCloudEntry(): ProviderCatalogEntry {
104
104
  export function mergeManualCatalog(
105
105
  base: CatalogMap,
106
106
  ): Record<BuiltInProviderId, ProviderCatalogEntry> {
107
- const ollamaCloudEntry = buildOllamaCloudEntry();
107
+ const ollamaCloudEntry = base[OLLAMA_CLOUD_ID] ?? buildOllamaCloudEntry();
108
108
  const manualEntry = buildOttoRouterEntry();
109
109
  const merged: Record<BuiltInProviderId, ProviderCatalogEntry> = {
110
110
  ...(base as Record<BuiltInProviderId, ProviderCatalogEntry>),
@@ -3567,6 +3567,56 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
3567
3567
  output: 65536,
3568
3568
  },
3569
3569
  },
3570
+ {
3571
+ id: 'deepseek/deepseek-v4-flash',
3572
+ label: 'DeepSeek V4 Flash',
3573
+ modalities: {
3574
+ input: ['text'],
3575
+ output: ['text'],
3576
+ },
3577
+ toolCall: true,
3578
+ reasoningText: true,
3579
+ attachment: false,
3580
+ temperature: true,
3581
+ knowledge: '2025-05',
3582
+ releaseDate: '2026-04-24',
3583
+ lastUpdated: '2026-04-24',
3584
+ openWeights: true,
3585
+ cost: {
3586
+ input: 0.14,
3587
+ output: 0.28,
3588
+ cacheRead: 0.028,
3589
+ },
3590
+ limit: {
3591
+ context: 1048576,
3592
+ output: 393216,
3593
+ },
3594
+ },
3595
+ {
3596
+ id: 'deepseek/deepseek-v4-pro',
3597
+ label: 'DeepSeek V4 Pro',
3598
+ modalities: {
3599
+ input: ['text'],
3600
+ output: ['text'],
3601
+ },
3602
+ toolCall: true,
3603
+ reasoningText: true,
3604
+ attachment: false,
3605
+ temperature: true,
3606
+ knowledge: '2025-05',
3607
+ releaseDate: '2026-04-24',
3608
+ lastUpdated: '2026-04-24',
3609
+ openWeights: true,
3610
+ cost: {
3611
+ input: 1.74,
3612
+ output: 3.48,
3613
+ cacheRead: 0.145,
3614
+ },
3615
+ limit: {
3616
+ context: 1048576,
3617
+ output: 393216,
3618
+ },
3619
+ },
3570
3620
  {
3571
3621
  id: 'google/gemini-2.0-flash-001',
3572
3622
  ownedBy: 'google',
@@ -10465,4 +10515,700 @@ export const catalog: Partial<Record<BuiltInProviderId, ProviderCatalogEntry>> =
10465
10515
  api: 'https://api.githubcopilot.com',
10466
10516
  doc: 'https://docs.github.com/en/copilot',
10467
10517
  },
10518
+ 'ollama-cloud': {
10519
+ id: 'ollama-cloud',
10520
+ models: [
10521
+ {
10522
+ id: 'cogito-2.1:671b',
10523
+ label: 'cogito-2.1:671b',
10524
+ modalities: {
10525
+ input: ['text'],
10526
+ output: ['text'],
10527
+ },
10528
+ toolCall: true,
10529
+ reasoningText: true,
10530
+ attachment: false,
10531
+ releaseDate: '2025-11-19',
10532
+ lastUpdated: '2026-01-19',
10533
+ openWeights: true,
10534
+ limit: {
10535
+ context: 163840,
10536
+ output: 32000,
10537
+ },
10538
+ },
10539
+ {
10540
+ id: 'deepseek-v3.1:671b',
10541
+ label: 'deepseek-v3.1:671b',
10542
+ modalities: {
10543
+ input: ['text'],
10544
+ output: ['text'],
10545
+ },
10546
+ toolCall: true,
10547
+ reasoningText: true,
10548
+ attachment: false,
10549
+ releaseDate: '2025-08-21',
10550
+ lastUpdated: '2026-01-19',
10551
+ openWeights: true,
10552
+ limit: {
10553
+ context: 163840,
10554
+ output: 163840,
10555
+ },
10556
+ },
10557
+ {
10558
+ id: 'deepseek-v3.2',
10559
+ label: 'deepseek-v3.2',
10560
+ modalities: {
10561
+ input: ['text'],
10562
+ output: ['text'],
10563
+ },
10564
+ toolCall: true,
10565
+ reasoningText: true,
10566
+ attachment: false,
10567
+ releaseDate: '2025-06-15',
10568
+ lastUpdated: '2026-01-19',
10569
+ openWeights: true,
10570
+ limit: {
10571
+ context: 163840,
10572
+ output: 65536,
10573
+ },
10574
+ },
10575
+ {
10576
+ id: 'devstral-2:123b',
10577
+ label: 'devstral-2:123b',
10578
+ modalities: {
10579
+ input: ['text'],
10580
+ output: ['text'],
10581
+ },
10582
+ toolCall: true,
10583
+ reasoningText: false,
10584
+ attachment: false,
10585
+ releaseDate: '2025-12-09',
10586
+ lastUpdated: '2026-01-19',
10587
+ openWeights: true,
10588
+ limit: {
10589
+ context: 262144,
10590
+ output: 262144,
10591
+ },
10592
+ },
10593
+ {
10594
+ id: 'devstral-small-2:24b',
10595
+ label: 'devstral-small-2:24b',
10596
+ modalities: {
10597
+ input: ['text', 'image'],
10598
+ output: ['text'],
10599
+ },
10600
+ toolCall: true,
10601
+ reasoningText: false,
10602
+ attachment: true,
10603
+ releaseDate: '2025-12-09',
10604
+ lastUpdated: '2026-01-19',
10605
+ openWeights: true,
10606
+ limit: {
10607
+ context: 262144,
10608
+ output: 262144,
10609
+ },
10610
+ },
10611
+ {
10612
+ id: 'gemini-3-flash-preview',
10613
+ ownedBy: 'google',
10614
+ label: 'gemini-3-flash-preview',
10615
+ modalities: {
10616
+ input: ['text', 'image'],
10617
+ output: ['text'],
10618
+ },
10619
+ toolCall: true,
10620
+ reasoningText: true,
10621
+ attachment: true,
10622
+ knowledge: '2025-01',
10623
+ releaseDate: '2025-12-17',
10624
+ lastUpdated: '2026-04-08',
10625
+ openWeights: true,
10626
+ limit: {
10627
+ context: 1048576,
10628
+ output: 65536,
10629
+ },
10630
+ },
10631
+ {
10632
+ id: 'gemma3:12b',
10633
+ label: 'gemma3:12b',
10634
+ modalities: {
10635
+ input: ['text', 'image'],
10636
+ output: ['text'],
10637
+ },
10638
+ toolCall: false,
10639
+ reasoningText: false,
10640
+ attachment: true,
10641
+ releaseDate: '2024-12-01',
10642
+ lastUpdated: '2026-01-19',
10643
+ openWeights: true,
10644
+ limit: {
10645
+ context: 131072,
10646
+ output: 131072,
10647
+ },
10648
+ },
10649
+ {
10650
+ id: 'gemma3:27b',
10651
+ label: 'gemma3:27b',
10652
+ modalities: {
10653
+ input: ['text', 'image'],
10654
+ output: ['text'],
10655
+ },
10656
+ toolCall: false,
10657
+ reasoningText: false,
10658
+ attachment: true,
10659
+ releaseDate: '2025-07-27',
10660
+ lastUpdated: '2026-01-19',
10661
+ openWeights: true,
10662
+ limit: {
10663
+ context: 131072,
10664
+ output: 131072,
10665
+ },
10666
+ },
10667
+ {
10668
+ id: 'gemma3:4b',
10669
+ label: 'gemma3:4b',
10670
+ modalities: {
10671
+ input: ['text', 'image'],
10672
+ output: ['text'],
10673
+ },
10674
+ toolCall: false,
10675
+ reasoningText: false,
10676
+ attachment: true,
10677
+ releaseDate: '2024-12-01',
10678
+ lastUpdated: '2026-01-19',
10679
+ openWeights: true,
10680
+ limit: {
10681
+ context: 131072,
10682
+ output: 131072,
10683
+ },
10684
+ },
10685
+ {
10686
+ id: 'gemma4:31b',
10687
+ label: 'gemma4:31b',
10688
+ modalities: {
10689
+ input: ['text', 'image'],
10690
+ output: ['text'],
10691
+ },
10692
+ toolCall: true,
10693
+ reasoningText: true,
10694
+ attachment: true,
10695
+ knowledge: '2025-01',
10696
+ releaseDate: '2026-04-02',
10697
+ lastUpdated: '2026-04-08',
10698
+ openWeights: true,
10699
+ limit: {
10700
+ context: 262144,
10701
+ output: 262144,
10702
+ },
10703
+ },
10704
+ {
10705
+ id: 'glm-4.6',
10706
+ ownedBy: 'zai',
10707
+ label: 'glm-4.6',
10708
+ modalities: {
10709
+ input: ['text'],
10710
+ output: ['text'],
10711
+ },
10712
+ toolCall: true,
10713
+ reasoningText: true,
10714
+ attachment: false,
10715
+ releaseDate: '2025-09-29',
10716
+ lastUpdated: '2026-01-19',
10717
+ openWeights: true,
10718
+ limit: {
10719
+ context: 202752,
10720
+ output: 131072,
10721
+ },
10722
+ },
10723
+ {
10724
+ id: 'glm-4.7',
10725
+ ownedBy: 'zai',
10726
+ label: 'glm-4.7',
10727
+ modalities: {
10728
+ input: ['text'],
10729
+ output: ['text'],
10730
+ },
10731
+ toolCall: true,
10732
+ reasoningText: true,
10733
+ attachment: false,
10734
+ releaseDate: '2025-12-22',
10735
+ lastUpdated: '2026-01-19',
10736
+ openWeights: true,
10737
+ limit: {
10738
+ context: 202752,
10739
+ output: 131072,
10740
+ },
10741
+ },
10742
+ {
10743
+ id: 'glm-5',
10744
+ ownedBy: 'zai',
10745
+ label: 'glm-5',
10746
+ modalities: {
10747
+ input: ['text'],
10748
+ output: ['text'],
10749
+ },
10750
+ toolCall: true,
10751
+ reasoningText: true,
10752
+ attachment: false,
10753
+ releaseDate: '2026-02-11',
10754
+ lastUpdated: '2026-02-11',
10755
+ openWeights: true,
10756
+ limit: {
10757
+ context: 202752,
10758
+ output: 131072,
10759
+ },
10760
+ },
10761
+ {
10762
+ id: 'glm-5.1',
10763
+ ownedBy: 'zai',
10764
+ label: 'glm-5.1',
10765
+ modalities: {
10766
+ input: ['text'],
10767
+ output: ['text'],
10768
+ },
10769
+ toolCall: true,
10770
+ reasoningText: true,
10771
+ attachment: false,
10772
+ releaseDate: '2026-03-27',
10773
+ lastUpdated: '2026-04-07',
10774
+ openWeights: true,
10775
+ limit: {
10776
+ context: 202752,
10777
+ output: 131072,
10778
+ },
10779
+ },
10780
+ {
10781
+ id: 'gpt-oss:120b',
10782
+ ownedBy: 'openai',
10783
+ label: 'gpt-oss:120b',
10784
+ modalities: {
10785
+ input: ['text'],
10786
+ output: ['text'],
10787
+ },
10788
+ toolCall: true,
10789
+ reasoningText: true,
10790
+ attachment: false,
10791
+ releaseDate: '2025-08-05',
10792
+ lastUpdated: '2026-01-19',
10793
+ openWeights: true,
10794
+ limit: {
10795
+ context: 131072,
10796
+ output: 32768,
10797
+ },
10798
+ },
10799
+ {
10800
+ id: 'gpt-oss:20b',
10801
+ ownedBy: 'openai',
10802
+ label: 'gpt-oss:20b',
10803
+ modalities: {
10804
+ input: ['text'],
10805
+ output: ['text'],
10806
+ },
10807
+ toolCall: true,
10808
+ reasoningText: true,
10809
+ attachment: false,
10810
+ releaseDate: '2025-08-05',
10811
+ lastUpdated: '2026-01-19',
10812
+ openWeights: true,
10813
+ limit: {
10814
+ context: 131072,
10815
+ output: 32768,
10816
+ },
10817
+ },
10818
+ {
10819
+ id: 'kimi-k2-thinking',
10820
+ ownedBy: 'moonshot',
10821
+ label: 'kimi-k2-thinking',
10822
+ modalities: {
10823
+ input: ['text'],
10824
+ output: ['text'],
10825
+ },
10826
+ toolCall: true,
10827
+ reasoningText: true,
10828
+ attachment: false,
10829
+ knowledge: '2024-08',
10830
+ releaseDate: '2025-11-06',
10831
+ lastUpdated: '2026-01-19',
10832
+ openWeights: true,
10833
+ limit: {
10834
+ context: 262144,
10835
+ output: 262144,
10836
+ },
10837
+ },
10838
+ {
10839
+ id: 'kimi-k2:1t',
10840
+ ownedBy: 'moonshot',
10841
+ label: 'kimi-k2:1t',
10842
+ modalities: {
10843
+ input: ['text'],
10844
+ output: ['text'],
10845
+ },
10846
+ toolCall: true,
10847
+ reasoningText: false,
10848
+ attachment: false,
10849
+ knowledge: '2024-10',
10850
+ releaseDate: '2025-07-11',
10851
+ lastUpdated: '2026-01-19',
10852
+ openWeights: true,
10853
+ limit: {
10854
+ context: 262144,
10855
+ output: 262144,
10856
+ },
10857
+ },
10858
+ {
10859
+ id: 'kimi-k2.5',
10860
+ ownedBy: 'moonshot',
10861
+ label: 'kimi-k2.5',
10862
+ modalities: {
10863
+ input: ['text', 'image'],
10864
+ output: ['text'],
10865
+ },
10866
+ toolCall: true,
10867
+ reasoningText: true,
10868
+ attachment: true,
10869
+ releaseDate: '2026-01-27',
10870
+ lastUpdated: '2026-01-27',
10871
+ openWeights: true,
10872
+ limit: {
10873
+ context: 262144,
10874
+ output: 262144,
10875
+ },
10876
+ },
10877
+ {
10878
+ id: 'kimi-k2.6:cloud',
10879
+ ownedBy: 'moonshot',
10880
+ label: 'kimi-k2.6:cloud',
10881
+ modalities: {
10882
+ input: ['text', 'image'],
10883
+ output: ['text'],
10884
+ },
10885
+ toolCall: true,
10886
+ reasoningText: true,
10887
+ attachment: true,
10888
+ releaseDate: '2026-04-20',
10889
+ lastUpdated: '2026-04-20',
10890
+ openWeights: true,
10891
+ limit: {
10892
+ context: 262144,
10893
+ output: 262144,
10894
+ },
10895
+ },
10896
+ {
10897
+ id: 'minimax-m2',
10898
+ ownedBy: 'minimax',
10899
+ label: 'minimax-m2',
10900
+ modalities: {
10901
+ input: ['text'],
10902
+ output: ['text'],
10903
+ },
10904
+ toolCall: true,
10905
+ reasoningText: false,
10906
+ attachment: false,
10907
+ releaseDate: '2025-10-23',
10908
+ lastUpdated: '2026-01-19',
10909
+ openWeights: true,
10910
+ limit: {
10911
+ context: 204800,
10912
+ output: 128000,
10913
+ },
10914
+ },
10915
+ {
10916
+ id: 'minimax-m2.1',
10917
+ ownedBy: 'minimax',
10918
+ label: 'minimax-m2.1',
10919
+ modalities: {
10920
+ input: ['text'],
10921
+ output: ['text'],
10922
+ },
10923
+ toolCall: true,
10924
+ reasoningText: true,
10925
+ attachment: false,
10926
+ releaseDate: '2025-12-23',
10927
+ lastUpdated: '2026-01-19',
10928
+ openWeights: true,
10929
+ limit: {
10930
+ context: 204800,
10931
+ output: 131072,
10932
+ },
10933
+ },
10934
+ {
10935
+ id: 'minimax-m2.5',
10936
+ ownedBy: 'minimax',
10937
+ label: 'minimax-m2.5',
10938
+ modalities: {
10939
+ input: ['text'],
10940
+ output: ['text'],
10941
+ },
10942
+ toolCall: true,
10943
+ reasoningText: true,
10944
+ attachment: false,
10945
+ knowledge: '2025-01',
10946
+ releaseDate: '2026-02-12',
10947
+ lastUpdated: '2026-02-12',
10948
+ openWeights: true,
10949
+ limit: {
10950
+ context: 204800,
10951
+ output: 131072,
10952
+ },
10953
+ },
10954
+ {
10955
+ id: 'minimax-m2.7',
10956
+ ownedBy: 'minimax',
10957
+ label: 'minimax-m2.7',
10958
+ modalities: {
10959
+ input: ['text'],
10960
+ output: ['text'],
10961
+ },
10962
+ toolCall: true,
10963
+ reasoningText: true,
10964
+ attachment: false,
10965
+ releaseDate: '2026-03-18',
10966
+ lastUpdated: '2026-03-18',
10967
+ openWeights: true,
10968
+ limit: {
10969
+ context: 204800,
10970
+ output: 131072,
10971
+ },
10972
+ },
10973
+ {
10974
+ id: 'ministral-3:14b',
10975
+ label: 'ministral-3:14b',
10976
+ modalities: {
10977
+ input: ['text', 'image'],
10978
+ output: ['text'],
10979
+ },
10980
+ toolCall: true,
10981
+ reasoningText: false,
10982
+ attachment: true,
10983
+ releaseDate: '2024-12-01',
10984
+ lastUpdated: '2026-01-19',
10985
+ openWeights: true,
10986
+ limit: {
10987
+ context: 262144,
10988
+ output: 128000,
10989
+ },
10990
+ },
10991
+ {
10992
+ id: 'ministral-3:3b',
10993
+ label: 'ministral-3:3b',
10994
+ modalities: {
10995
+ input: ['text', 'image'],
10996
+ output: ['text'],
10997
+ },
10998
+ toolCall: true,
10999
+ reasoningText: false,
11000
+ attachment: true,
11001
+ releaseDate: '2024-10-22',
11002
+ lastUpdated: '2026-01-19',
11003
+ openWeights: true,
11004
+ limit: {
11005
+ context: 262144,
11006
+ output: 128000,
11007
+ },
11008
+ },
11009
+ {
11010
+ id: 'ministral-3:8b',
11011
+ label: 'ministral-3:8b',
11012
+ modalities: {
11013
+ input: ['text', 'image'],
11014
+ output: ['text'],
11015
+ },
11016
+ toolCall: true,
11017
+ reasoningText: false,
11018
+ attachment: true,
11019
+ releaseDate: '2024-12-01',
11020
+ lastUpdated: '2026-01-19',
11021
+ openWeights: true,
11022
+ limit: {
11023
+ context: 262144,
11024
+ output: 128000,
11025
+ },
11026
+ },
11027
+ {
11028
+ id: 'mistral-large-3:675b',
11029
+ label: 'mistral-large-3:675b',
11030
+ modalities: {
11031
+ input: ['text', 'image'],
11032
+ output: ['text'],
11033
+ },
11034
+ toolCall: true,
11035
+ reasoningText: false,
11036
+ attachment: true,
11037
+ releaseDate: '2025-12-02',
11038
+ lastUpdated: '2026-01-19',
11039
+ openWeights: true,
11040
+ limit: {
11041
+ context: 262144,
11042
+ output: 262144,
11043
+ },
11044
+ },
11045
+ {
11046
+ id: 'nemotron-3-nano:30b',
11047
+ label: 'nemotron-3-nano:30b',
11048
+ modalities: {
11049
+ input: ['text'],
11050
+ output: ['text'],
11051
+ },
11052
+ toolCall: true,
11053
+ reasoningText: true,
11054
+ attachment: false,
11055
+ releaseDate: '2025-12-15',
11056
+ lastUpdated: '2026-01-19',
11057
+ openWeights: true,
11058
+ limit: {
11059
+ context: 1048576,
11060
+ output: 131072,
11061
+ },
11062
+ },
11063
+ {
11064
+ id: 'nemotron-3-super',
11065
+ label: 'nemotron-3-super',
11066
+ modalities: {
11067
+ input: ['text'],
11068
+ output: ['text'],
11069
+ },
11070
+ toolCall: true,
11071
+ reasoningText: true,
11072
+ attachment: false,
11073
+ releaseDate: '2026-03-11',
11074
+ lastUpdated: '2026-03-12',
11075
+ openWeights: true,
11076
+ limit: {
11077
+ context: 262144,
11078
+ output: 65536,
11079
+ },
11080
+ },
11081
+ {
11082
+ id: 'qwen3-coder-next',
11083
+ label: 'qwen3-coder-next',
11084
+ modalities: {
11085
+ input: ['text'],
11086
+ output: ['text'],
11087
+ },
11088
+ toolCall: true,
11089
+ reasoningText: false,
11090
+ attachment: false,
11091
+ releaseDate: '2026-02-02',
11092
+ lastUpdated: '2026-02-08',
11093
+ openWeights: true,
11094
+ limit: {
11095
+ context: 262144,
11096
+ output: 65536,
11097
+ },
11098
+ },
11099
+ {
11100
+ id: 'qwen3-coder:480b',
11101
+ label: 'qwen3-coder:480b',
11102
+ modalities: {
11103
+ input: ['text'],
11104
+ output: ['text'],
11105
+ },
11106
+ toolCall: true,
11107
+ reasoningText: false,
11108
+ attachment: false,
11109
+ releaseDate: '2025-07-22',
11110
+ lastUpdated: '2026-01-19',
11111
+ openWeights: true,
11112
+ limit: {
11113
+ context: 262144,
11114
+ output: 65536,
11115
+ },
11116
+ },
11117
+ {
11118
+ id: 'qwen3-next:80b',
11119
+ label: 'qwen3-next:80b',
11120
+ modalities: {
11121
+ input: ['text'],
11122
+ output: ['text'],
11123
+ },
11124
+ toolCall: true,
11125
+ reasoningText: true,
11126
+ attachment: false,
11127
+ releaseDate: '2025-09-15',
11128
+ lastUpdated: '2026-01-19',
11129
+ openWeights: true,
11130
+ limit: {
11131
+ context: 262144,
11132
+ output: 32768,
11133
+ },
11134
+ },
11135
+ {
11136
+ id: 'qwen3-vl:235b',
11137
+ label: 'qwen3-vl:235b',
11138
+ modalities: {
11139
+ input: ['text', 'image'],
11140
+ output: ['text'],
11141
+ },
11142
+ toolCall: true,
11143
+ reasoningText: true,
11144
+ attachment: true,
11145
+ releaseDate: '2025-09-22',
11146
+ lastUpdated: '2026-01-19',
11147
+ openWeights: true,
11148
+ limit: {
11149
+ context: 262144,
11150
+ output: 32768,
11151
+ },
11152
+ },
11153
+ {
11154
+ id: 'qwen3-vl:235b-instruct',
11155
+ label: 'qwen3-vl:235b-instruct',
11156
+ modalities: {
11157
+ input: ['text', 'image'],
11158
+ output: ['text'],
11159
+ },
11160
+ toolCall: true,
11161
+ reasoningText: false,
11162
+ attachment: true,
11163
+ releaseDate: '2025-09-22',
11164
+ lastUpdated: '2026-01-19',
11165
+ openWeights: true,
11166
+ limit: {
11167
+ context: 262144,
11168
+ output: 131072,
11169
+ },
11170
+ },
11171
+ {
11172
+ id: 'qwen3.5:397b',
11173
+ label: 'qwen3.5:397b',
11174
+ modalities: {
11175
+ input: ['text', 'image'],
11176
+ output: ['text'],
11177
+ },
11178
+ toolCall: true,
11179
+ reasoningText: true,
11180
+ attachment: true,
11181
+ releaseDate: '2026-02-15',
11182
+ lastUpdated: '2026-02-17',
11183
+ openWeights: true,
11184
+ limit: {
11185
+ context: 262144,
11186
+ output: 81920,
11187
+ },
11188
+ },
11189
+ {
11190
+ id: 'rnj-1:8b',
11191
+ label: 'rnj-1:8b',
11192
+ modalities: {
11193
+ input: ['text'],
11194
+ output: ['text'],
11195
+ },
11196
+ toolCall: true,
11197
+ reasoningText: false,
11198
+ attachment: false,
11199
+ releaseDate: '2025-12-06',
11200
+ lastUpdated: '2026-01-19',
11201
+ openWeights: true,
11202
+ limit: {
11203
+ context: 32768,
11204
+ output: 4096,
11205
+ },
11206
+ },
11207
+ ],
11208
+ label: 'Ollama Cloud',
11209
+ env: ['OLLAMA_API_KEY'],
11210
+ npm: 'ai-sdk-ollama',
11211
+ api: 'https://ollama.com',
11212
+ doc: 'https://docs.ollama.com/cloud',
11213
+ },
10468
11214
  } as const satisfies Partial<Record<BuiltInProviderId, ProviderCatalogEntry>>;
@@ -1,5 +1,19 @@
1
1
  export { isProviderAuthorized, ensureProviderEnv } from './authorization.ts';
2
2
  export { catalog } from './catalog-merged.ts';
3
+ export {
4
+ DEFAULT_REMOTE_MODEL_CATALOG_URL,
5
+ getCachedProviderCatalogEntry,
6
+ getModelCatalogCachePath,
7
+ mergeCachedModelCatalog,
8
+ normalizeModelCatalogPayload,
9
+ readCachedModelCatalog,
10
+ readCachedModelCatalogSync,
11
+ writeCachedModelCatalog,
12
+ } from './model-catalog-cache.ts';
13
+ export type {
14
+ CachedModelCatalog,
15
+ CachedProviderCatalogEntry,
16
+ } from './model-catalog-cache.ts';
3
17
  export type {
4
18
  BuiltInProviderId,
5
19
  ProviderId,
@@ -0,0 +1,147 @@
1
+ import { getGlobalConfigDir, joinPath } from '../../config/src/paths.ts';
2
+ import type { ModelInfo, ProviderId } from '../../types/src/index.ts';
3
+
4
+ export type CachedProviderCatalogEntry = {
5
+ id: ProviderId;
6
+ label?: string;
7
+ models: ModelInfo[];
8
+ };
9
+
10
+ export type CachedModelCatalog = {
11
+ version: 1;
12
+ updatedAt: string;
13
+ providers: Record<string, CachedProviderCatalogEntry>;
14
+ };
15
+
16
+ export const DEFAULT_REMOTE_MODEL_CATALOG_URL =
17
+ 'https://ottocode.io/catalog/models.json';
18
+
19
+ export function getModelCatalogCachePath(): string {
20
+ return joinPath(getGlobalConfigDir(), 'models-catalog.json');
21
+ }
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ async function loadFsPromises(): Promise<typeof import('node:fs/promises')> {
28
+ return Function('specifier', 'return import(specifier)')('node:fs/promises');
29
+ }
30
+
31
+ function readFileSyncCompat(path: string): string | null {
32
+ try {
33
+ const req = Function(
34
+ 'return typeof require === "function" ? require : null',
35
+ )() as ((specifier: string) => typeof import('node:fs')) | null;
36
+ return req?.('node:fs').readFileSync(path, 'utf8') ?? null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function normalizeProviderEntry(
43
+ id: string,
44
+ value: unknown,
45
+ ): CachedProviderCatalogEntry | null {
46
+ if (!isRecord(value)) return null;
47
+ const models = Array.isArray(value.models)
48
+ ? value.models.filter(
49
+ (model): model is ModelInfo =>
50
+ isRecord(model) && typeof model.id === 'string',
51
+ )
52
+ : [];
53
+ return {
54
+ id: (typeof value.id === 'string' ? value.id : id) as ProviderId,
55
+ label: typeof value.label === 'string' ? value.label : undefined,
56
+ models,
57
+ };
58
+ }
59
+
60
+ export function normalizeModelCatalogPayload(
61
+ payload: unknown,
62
+ ): Record<string, CachedProviderCatalogEntry> {
63
+ if (!isRecord(payload)) return {};
64
+ const source = isRecord(payload.providers) ? payload.providers : payload;
65
+ const providers: Record<string, CachedProviderCatalogEntry> = {};
66
+ for (const [id, value] of Object.entries(source)) {
67
+ const entry = normalizeProviderEntry(id, value);
68
+ if (entry) providers[id] = entry;
69
+ }
70
+ return providers;
71
+ }
72
+
73
+ export async function readCachedModelCatalog(): Promise<CachedModelCatalog | null> {
74
+ try {
75
+ const { readFile } = await loadFsPromises();
76
+ const payload = JSON.parse(
77
+ await readFile(getModelCatalogCachePath(), 'utf8'),
78
+ );
79
+ const providers = normalizeModelCatalogPayload(payload);
80
+ return {
81
+ version: 1,
82
+ updatedAt:
83
+ isRecord(payload) && typeof payload.updatedAt === 'string'
84
+ ? payload.updatedAt
85
+ : new Date(0).toISOString(),
86
+ providers,
87
+ };
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ export function readCachedModelCatalogSync(): CachedModelCatalog | null {
94
+ try {
95
+ const text = readFileSyncCompat(getModelCatalogCachePath());
96
+ if (!text) return null;
97
+ const payload = JSON.parse(text);
98
+ const providers = normalizeModelCatalogPayload(payload);
99
+ return {
100
+ version: 1,
101
+ updatedAt:
102
+ isRecord(payload) && typeof payload.updatedAt === 'string'
103
+ ? payload.updatedAt
104
+ : new Date(0).toISOString(),
105
+ providers,
106
+ };
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ export function getCachedProviderCatalogEntry(
113
+ provider: ProviderId,
114
+ ): CachedProviderCatalogEntry | undefined {
115
+ return readCachedModelCatalogSync()?.providers[provider];
116
+ }
117
+
118
+ export async function writeCachedModelCatalog(
119
+ providers: Record<string, CachedProviderCatalogEntry>,
120
+ ): Promise<void> {
121
+ const path = getModelCatalogCachePath();
122
+ const dir = path.slice(0, path.lastIndexOf('/'));
123
+ const { mkdir, writeFile } = await loadFsPromises();
124
+ await mkdir(dir, { recursive: true });
125
+ await writeFile(
126
+ path,
127
+ JSON.stringify(
128
+ {
129
+ version: 1,
130
+ updatedAt: new Date().toISOString(),
131
+ providers,
132
+ },
133
+ null,
134
+ 2,
135
+ ),
136
+ );
137
+ }
138
+
139
+ export async function mergeCachedModelCatalog(
140
+ providers: Record<string, CachedProviderCatalogEntry>,
141
+ ): Promise<void> {
142
+ const existing = await readCachedModelCatalog();
143
+ await writeCachedModelCatalog({
144
+ ...(existing?.providers ?? {}),
145
+ ...providers,
146
+ });
147
+ }
@@ -1,5 +1,6 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
2
  import { providerEnvVar } from './env.ts';
3
+ import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
3
4
  import { getUnderlyingProviderKey, providerIds } from './utils.ts';
4
5
  import type {
5
6
  BuiltInProviderId,
@@ -127,17 +128,21 @@ export function getProviderDefinition(
127
128
  if (isBuiltInProviderId(provider)) {
128
129
  const entry = catalog[provider];
129
130
  if (!entry) return undefined;
131
+ const cachedEntry = getCachedProviderCatalogEntry(provider);
132
+ const models = cachedEntry?.models.length
133
+ ? cachedEntry.models
134
+ : entry.models;
130
135
  return {
131
136
  id: provider,
132
- label: settings?.label ?? entry.label ?? provider,
137
+ label: settings?.label ?? cachedEntry?.label ?? entry.label ?? provider,
133
138
  source: 'built-in',
134
139
  compatibility: BUILTIN_COMPATIBILITY[provider],
135
140
  family: BUILTIN_FAMILY[provider],
136
- baseURL: normalizeOptionalText(settings?.baseURL),
141
+ baseURL: normalizeOptionalText(settings?.baseURL) ?? entry.api,
137
142
  apiKey: normalizeOptionalText(settings?.apiKey),
138
143
  apiKeyEnv:
139
144
  normalizeOptionalText(settings?.apiKeyEnv) ?? providerEnvVar(provider),
140
- models: entry.models,
145
+ models,
141
146
  allowAnyModel: provider === 'ollama-cloud',
142
147
  };
143
148
  }
@@ -1,4 +1,5 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
+ import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
2
3
  import type {
3
4
  BuiltInProviderId,
4
5
  ProviderId,
@@ -17,11 +18,11 @@ export function isProviderId(value: unknown): value is BuiltInProviderId {
17
18
  }
18
19
 
19
20
  export function defaultModelFor(provider: ProviderId): string | undefined {
20
- return catalog[provider]?.models?.[0]?.id;
21
+ return getProviderModels(provider)[0]?.id;
21
22
  }
22
23
 
23
24
  export function listModels(provider: ProviderId): string[] {
24
- return (catalog[provider]?.models ?? []).map((m) => m.id);
25
+ return getProviderModels(provider).map((m) => m.id);
25
26
  }
26
27
 
27
28
  export function hasModel(
@@ -49,7 +50,7 @@ const PREFERRED_FAST_MODELS_OAUTH: Partial<Record<ProviderId, string[]>> = {
49
50
  };
50
51
 
51
52
  export function getFastModel(provider: ProviderId): string | undefined {
52
- const providerModels = catalog[provider]?.models ?? [];
53
+ const providerModels = getProviderModels(provider);
53
54
  if (!providerModels.length) return undefined;
54
55
 
55
56
  const preferred = PREFERRED_FAST_MODELS[provider] ?? [];
@@ -70,7 +71,7 @@ export function getFastModelForAuth(
70
71
  provider: ProviderId,
71
72
  authType: 'api' | 'oauth' | 'wallet' | undefined,
72
73
  ): string | undefined {
73
- const providerModels = catalog[provider]?.models ?? [];
74
+ const providerModels = getProviderModels(provider);
74
75
  if (!providerModels.length) return undefined;
75
76
 
76
77
  const filteredModels = filterModelsForAuthType(
@@ -106,13 +107,13 @@ export function getModelNpmBinding(
106
107
  model: string,
107
108
  ): string | undefined {
108
109
  const entry = catalog[provider];
109
- const modelInfo = entry?.models?.find((m) => m.id === model);
110
+ const modelInfo = getProviderModels(provider).find((m) => m.id === model);
110
111
  if (modelInfo?.provider?.npm) return modelInfo.provider.npm;
111
112
  if (entry?.npm) return entry.npm;
112
113
 
113
114
  for (const key of Object.keys(catalog) as ProviderId[]) {
114
115
  const e = catalog[key];
115
- const m = e?.models?.find((x) => x.id === model);
116
+ const m = getProviderModels(key).find((x) => x.id === model);
116
117
  if (m?.provider?.npm) return m.provider.npm;
117
118
  if (m && e?.npm) return e.npm;
118
119
  }
@@ -234,7 +235,15 @@ export function getModelInfo(
234
235
  ): ModelInfo | undefined {
235
236
  const entry = catalog[provider];
236
237
  if (!entry) return undefined;
237
- return entry.models?.find((m) => m.id === model);
238
+ return getProviderModels(provider).find((m) => m.id === model);
239
+ }
240
+
241
+ function getProviderModels(provider: ProviderId): ModelInfo[] {
242
+ return (
243
+ getCachedProviderCatalogEntry(provider)?.models ??
244
+ catalog[provider]?.models ??
245
+ []
246
+ );
238
247
  }
239
248
 
240
249
  export function modelSupportsReasoning(
@@ -1,4 +1,5 @@
1
1
  import { catalog } from './catalog-merged.ts';
2
+ import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
2
3
  import type { OttoConfig, ProviderId } from '../../types/src/index.ts';
3
4
  import {
4
5
  getProviderDefinition,
@@ -51,14 +52,15 @@ export function validateProviderModel(
51
52
  if (!catalog[p]) {
52
53
  throw new Error(`Provider not supported: ${provider}`);
53
54
  }
54
- const entry = catalog[p].models.find((m) => m.id === model);
55
+ const models = getCachedProviderCatalogEntry(p)?.models ?? catalog[p].models;
56
+ const entry = models.find((m) => m.id === model);
55
57
  if (!entry) {
56
- const list = catalog[p].models
58
+ const list = models
57
59
  .slice(0, 10)
58
60
  .map((m) => m.id)
59
61
  .join(', ');
60
62
  throw new Error(
61
- `Model not found for provider ${provider}: ${model}. Example models: ${list}${catalog[p].models.length > 10 ? ', ...' : ''}`,
63
+ `Model not found for provider ${provider}: ${model}. Example models: ${list}${models.length > 10 ? ', ...' : ''}`,
62
64
  );
63
65
  }
64
66
  applyCapabilityValidation(model, entry, effectiveCap, { strict: true });