@flame0510/project-aether 1.5.2 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/gateway/provider/route.ts +16 -10
- package/app/api/models/details/route.ts +30 -0
- package/app/api/system-health/route.ts +3 -3
- package/app/api/update-check/route.ts +27 -11
- package/app/components/SessionDrawer.tsx +4 -4
- package/app/components/VersionBanner.tsx +101 -56
- package/app/components/ui/Metric.tsx +7 -2
- package/app/gateway/ModelDetailsModal.tsx +322 -0
- package/app/gateway/PageClient.tsx +28 -10
- package/app/globals.css +4 -1
- package/app/lib/model-format.ts +21 -0
- package/docs/FRONTEND-ARCHITECTURE.md +2 -1
- package/docs/dev/API-REFERENCE.md +41 -2
- package/docs/dev/GATEWAY.md +58 -0
- package/lib/memory-context.ts +2 -2
- package/lib/model-catalogue.ts +17 -0
- package/lib/model-details.ts +124 -0
- package/lib/provider-labels.ts +21 -0
- package/model-details.json +16360 -0
- package/model-pricing.json +59 -59
- package/models.config.json +75 -9
- package/package.json +4 -2
- package/scripts/check-language.mjs +76 -0
- package/scripts/lib/model-upstream.mjs +61 -0
- package/scripts/model-info-suggest.mjs +149 -0
- package/scripts/refresh-model-pricing.mjs +175 -16
package/model-pricing.json
CHANGED
|
@@ -176,8 +176,8 @@
|
|
|
176
176
|
"output": 0
|
|
177
177
|
},
|
|
178
178
|
"openrouter/deepseek/deepseek-v4-flash": {
|
|
179
|
-
"input": 0.
|
|
180
|
-
"output": 0.
|
|
179
|
+
"input": 0.084,
|
|
180
|
+
"output": 0.168
|
|
181
181
|
},
|
|
182
182
|
"openrouter/google/gemini-2.5-flash": {
|
|
183
183
|
"input": 0.3,
|
|
@@ -192,8 +192,8 @@
|
|
|
192
192
|
"output": 15
|
|
193
193
|
},
|
|
194
194
|
"openrouter/deepseek/deepseek-v4-pro": {
|
|
195
|
-
"input": 0.
|
|
196
|
-
"output": 1.
|
|
195
|
+
"input": 0.953172,
|
|
196
|
+
"output": 1.906344
|
|
197
197
|
},
|
|
198
198
|
"openrouter/qwen/qwen3.7-max": {
|
|
199
199
|
"input": 1.475,
|
|
@@ -504,100 +504,100 @@
|
|
|
504
504
|
"output": 1.2
|
|
505
505
|
},
|
|
506
506
|
"openrouter/deepseek/deepseek-v4.1-flash": {
|
|
507
|
-
"input": 0.
|
|
508
|
-
"output": 0.
|
|
507
|
+
"input": 0.1,
|
|
508
|
+
"output": 0.5
|
|
509
509
|
},
|
|
510
510
|
"openrouter/deepseek/deepseek-v4-flash-vision-exp": {
|
|
511
511
|
"input": 0.22,
|
|
512
512
|
"output": 0.66
|
|
513
513
|
},
|
|
514
514
|
"openrouter/openai/gpt-5-image": {
|
|
515
|
-
"input": 10
|
|
516
|
-
"output": 10
|
|
515
|
+
"input": 10,
|
|
516
|
+
"output": 10
|
|
517
517
|
},
|
|
518
518
|
"openrouter/openai/gpt-5-image-mini": {
|
|
519
519
|
"input": 2.5,
|
|
520
|
-
"output": 2
|
|
520
|
+
"output": 2
|
|
521
521
|
},
|
|
522
522
|
"openrouter/openai/gpt-5-mini": {
|
|
523
523
|
"input": 0.25,
|
|
524
|
-
"output": 2
|
|
524
|
+
"output": 2
|
|
525
525
|
},
|
|
526
526
|
"openrouter/openai/gpt-5-nano": {
|
|
527
527
|
"input": 0.05,
|
|
528
528
|
"output": 0.4
|
|
529
529
|
},
|
|
530
530
|
"openrouter/openai/gpt-5-pro": {
|
|
531
|
-
"input": 15
|
|
532
|
-
"output": 120
|
|
531
|
+
"input": 15,
|
|
532
|
+
"output": 120
|
|
533
533
|
},
|
|
534
534
|
"openrouter/openai/gpt-5.2": {
|
|
535
535
|
"input": 1.75,
|
|
536
|
-
"output": 14
|
|
536
|
+
"output": 14
|
|
537
537
|
},
|
|
538
538
|
"openrouter/openai/gpt-5.2-chat": {
|
|
539
539
|
"input": 1.75,
|
|
540
|
-
"output": 14
|
|
540
|
+
"output": 14
|
|
541
541
|
},
|
|
542
542
|
"openrouter/openai/gpt-5.2-codex": {
|
|
543
543
|
"input": 1.75,
|
|
544
|
-
"output": 14
|
|
544
|
+
"output": 14
|
|
545
545
|
},
|
|
546
546
|
"openrouter/openai/gpt-5.2-pro": {
|
|
547
|
-
"input": 21
|
|
548
|
-
"output": 168
|
|
547
|
+
"input": 21,
|
|
548
|
+
"output": 168
|
|
549
549
|
},
|
|
550
550
|
"openrouter/openai/gpt-5.3-codex": {
|
|
551
551
|
"input": 1.75,
|
|
552
|
-
"output": 14
|
|
552
|
+
"output": 14
|
|
553
553
|
},
|
|
554
554
|
"openrouter/openai/gpt-5.4": {
|
|
555
555
|
"input": 2.5,
|
|
556
|
-
"output": 15
|
|
556
|
+
"output": 15
|
|
557
557
|
},
|
|
558
558
|
"openrouter/openai/gpt-5.4-image-2": {
|
|
559
|
-
"input": 8
|
|
560
|
-
"output": 15
|
|
559
|
+
"input": 8,
|
|
560
|
+
"output": 15
|
|
561
561
|
},
|
|
562
562
|
"openrouter/openai/gpt-5.4-nano": {
|
|
563
563
|
"input": 0.2,
|
|
564
564
|
"output": 1.25
|
|
565
565
|
},
|
|
566
566
|
"openrouter/openai/gpt-5.4-pro": {
|
|
567
|
-
"input": 30
|
|
568
|
-
"output": 180
|
|
567
|
+
"input": 30,
|
|
568
|
+
"output": 180
|
|
569
569
|
},
|
|
570
570
|
"openrouter/openai/gpt-5.5": {
|
|
571
|
-
"input": 5
|
|
572
|
-
"output": 30
|
|
571
|
+
"input": 5,
|
|
572
|
+
"output": 30
|
|
573
573
|
},
|
|
574
574
|
"openrouter/openai/gpt-5.5-pro": {
|
|
575
|
-
"input": 30
|
|
576
|
-
"output": 180
|
|
575
|
+
"input": 30,
|
|
576
|
+
"output": 180
|
|
577
577
|
},
|
|
578
578
|
"openrouter/openai/gpt-5.6-luna-pro": {
|
|
579
579
|
"input": 0.2,
|
|
580
580
|
"output": 1.2
|
|
581
581
|
},
|
|
582
582
|
"openrouter/openai/gpt-5.6-sol-pro": {
|
|
583
|
-
"input": 2
|
|
584
|
-
"output": 10
|
|
583
|
+
"input": 2,
|
|
584
|
+
"output": 10
|
|
585
585
|
},
|
|
586
586
|
"openrouter/openai/gpt-5.6-terra": {
|
|
587
|
-
"input": 2
|
|
588
|
-
"output": 12
|
|
587
|
+
"input": 2,
|
|
588
|
+
"output": 12
|
|
589
589
|
},
|
|
590
590
|
"openrouter/openai/gpt-5.6-terra-pro": {
|
|
591
|
-
"input": 2
|
|
592
|
-
"output": 12
|
|
591
|
+
"input": 2,
|
|
592
|
+
"output": 12
|
|
593
593
|
},
|
|
594
594
|
"openrouter/openai/gpt-6-astra": {
|
|
595
|
-
"input": 10
|
|
596
|
-
"output": 50
|
|
595
|
+
"input": 10,
|
|
596
|
+
"output": 50
|
|
597
597
|
},
|
|
598
598
|
"openrouter/openai/gpt-6-astra-pro": {
|
|
599
|
-
"input": 10
|
|
600
|
-
"output": 50
|
|
599
|
+
"input": 10,
|
|
600
|
+
"output": 50
|
|
601
601
|
},
|
|
602
602
|
"openrouter/openai/gpt-6-luna": {
|
|
603
603
|
"input": 0.1,
|
|
@@ -608,36 +608,36 @@
|
|
|
608
608
|
"output": 0.5
|
|
609
609
|
},
|
|
610
610
|
"openrouter/openai/gpt-6-sol": {
|
|
611
|
-
"input": 2
|
|
612
|
-
"output": 10
|
|
611
|
+
"input": 2,
|
|
612
|
+
"output": 10
|
|
613
613
|
},
|
|
614
614
|
"openrouter/openai/gpt-6-sol-pro": {
|
|
615
|
-
"input": 2
|
|
616
|
-
"output": 10
|
|
615
|
+
"input": 2,
|
|
616
|
+
"output": 10
|
|
617
617
|
},
|
|
618
618
|
"openrouter/anthropic/claude-fable-5.1": {
|
|
619
|
-
"input": 10
|
|
620
|
-
"output": 50
|
|
619
|
+
"input": 10,
|
|
620
|
+
"output": 50
|
|
621
621
|
},
|
|
622
622
|
"openrouter/anthropic/claude-opus-4.5": {
|
|
623
|
-
"input": 5
|
|
624
|
-
"output": 25
|
|
623
|
+
"input": 5,
|
|
624
|
+
"output": 25
|
|
625
625
|
},
|
|
626
626
|
"openrouter/anthropic/claude-opus-4.6": {
|
|
627
|
-
"input": 5
|
|
628
|
-
"output": 25
|
|
627
|
+
"input": 5,
|
|
628
|
+
"output": 25
|
|
629
629
|
},
|
|
630
630
|
"openrouter/anthropic/claude-opus-4.8": {
|
|
631
|
-
"input": 5
|
|
632
|
-
"output": 25
|
|
631
|
+
"input": 5,
|
|
632
|
+
"output": 25
|
|
633
633
|
},
|
|
634
634
|
"openrouter/anthropic/claude-opus-5.5": {
|
|
635
|
-
"input": 4
|
|
636
|
-
"output": 20
|
|
635
|
+
"input": 4,
|
|
636
|
+
"output": 20
|
|
637
637
|
},
|
|
638
638
|
"openrouter/google/gemini-3.5-flash": {
|
|
639
639
|
"input": 1.5,
|
|
640
|
-
"output": 9
|
|
640
|
+
"output": 9
|
|
641
641
|
},
|
|
642
642
|
"openrouter/google/gemini-3.5-flash-lite": {
|
|
643
643
|
"input": 0.3,
|
|
@@ -684,20 +684,20 @@
|
|
|
684
684
|
"output": 1.8
|
|
685
685
|
},
|
|
686
686
|
"openrouter/qwen/qwen3.8-2.4t-a95b": {
|
|
687
|
-
"input": 2
|
|
688
|
-
"output": 6
|
|
687
|
+
"input": 2,
|
|
688
|
+
"output": 6
|
|
689
689
|
},
|
|
690
690
|
"openrouter/qwen/qwen3.8-27b": {
|
|
691
691
|
"input": 0.42,
|
|
692
|
-
"output": 3
|
|
692
|
+
"output": 3
|
|
693
693
|
},
|
|
694
694
|
"openrouter/qwen/qwen3.8-flash": {
|
|
695
695
|
"input": 0.15,
|
|
696
696
|
"output": 0.47
|
|
697
697
|
},
|
|
698
698
|
"openrouter/qwen/qwen3.8-max-0902": {
|
|
699
|
-
"input": 2
|
|
700
|
-
"output": 6
|
|
699
|
+
"input": 2,
|
|
700
|
+
"output": 6
|
|
701
701
|
},
|
|
702
702
|
"openrouter/qwen/qwen3.8-omni-flash": {
|
|
703
703
|
"input": 0.15,
|
|
@@ -716,8 +716,8 @@
|
|
|
716
716
|
"output": 2.25
|
|
717
717
|
},
|
|
718
718
|
"openrouter/x-ai/grok-4.6": {
|
|
719
|
-
"input": 2
|
|
720
|
-
"output": 6
|
|
719
|
+
"input": 2,
|
|
720
|
+
"output": 6
|
|
721
721
|
},
|
|
722
722
|
"openrouter/x-ai/grok-4.7": {
|
|
723
723
|
"input": 1.6,
|
package/models.config.json
CHANGED
|
@@ -166,7 +166,11 @@
|
|
|
166
166
|
"name": "Kimi K3",
|
|
167
167
|
"provider": "kimi",
|
|
168
168
|
"enabled": true,
|
|
169
|
-
"modality": "text+image->text"
|
|
169
|
+
"modality": "text+image->text",
|
|
170
|
+
"info": {
|
|
171
|
+
"params": "2.78T total · 104B activated · 93 layers",
|
|
172
|
+
"notes": "Activated parameters and layers per the model card."
|
|
173
|
+
}
|
|
170
174
|
},
|
|
171
175
|
{
|
|
172
176
|
"id": "kimi/kimi-k2.7-code",
|
|
@@ -194,14 +198,37 @@
|
|
|
194
198
|
"name": "GLM-5.3",
|
|
195
199
|
"provider": "glm",
|
|
196
200
|
"enabled": false,
|
|
197
|
-
"modality": "text->text"
|
|
201
|
+
"modality": "text->text",
|
|
202
|
+
"info": {
|
|
203
|
+
"docUrl": "https://docs.z.ai/guides/llm/glm-5.3",
|
|
204
|
+
"benchmarks": [
|
|
205
|
+
{
|
|
206
|
+
"name": "DeepSWE (v1.1)",
|
|
207
|
+
"value": 66.9,
|
|
208
|
+
"source": "https://huggingface.co/zai-org/GLM-5.3",
|
|
209
|
+
"asOf": "2026-09-04"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "FrontierSWE",
|
|
213
|
+
"value": 78.1,
|
|
214
|
+
"source": "https://huggingface.co/zai-org/GLM-5.3",
|
|
215
|
+
"asOf": "2026-09-04"
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
"notes": "Benchmarks per the model card."
|
|
219
|
+
}
|
|
198
220
|
},
|
|
199
221
|
{
|
|
200
222
|
"id": "glm/glm-5.3-flash",
|
|
201
223
|
"name": "GLM-5.3 Flash",
|
|
202
224
|
"provider": "glm",
|
|
203
225
|
"enabled": false,
|
|
204
|
-
"modality": "text+image+video->text"
|
|
226
|
+
"modality": "text+image+video->text",
|
|
227
|
+
"info": {
|
|
228
|
+
"params": "320B total · 18B activated",
|
|
229
|
+
"docUrl": "https://docs.z.ai/guides/vlm/glm-5.3-flash",
|
|
230
|
+
"notes": "Sizes per the vendor documentation."
|
|
231
|
+
}
|
|
205
232
|
},
|
|
206
233
|
{
|
|
207
234
|
"id": "glm/glm-5.3-flashx",
|
|
@@ -474,14 +501,37 @@
|
|
|
474
501
|
"name": "GLM 5.3 (OR)",
|
|
475
502
|
"provider": "openrouter",
|
|
476
503
|
"enabled": true,
|
|
477
|
-
"modality": "text->text"
|
|
504
|
+
"modality": "text->text",
|
|
505
|
+
"info": {
|
|
506
|
+
"docUrl": "https://docs.z.ai/guides/llm/glm-5.3",
|
|
507
|
+
"benchmarks": [
|
|
508
|
+
{
|
|
509
|
+
"name": "DeepSWE (v1.1)",
|
|
510
|
+
"value": 66.9,
|
|
511
|
+
"source": "https://huggingface.co/zai-org/GLM-5.3",
|
|
512
|
+
"asOf": "2026-09-04"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"name": "FrontierSWE",
|
|
516
|
+
"value": 78.1,
|
|
517
|
+
"source": "https://huggingface.co/zai-org/GLM-5.3",
|
|
518
|
+
"asOf": "2026-09-04"
|
|
519
|
+
}
|
|
520
|
+
],
|
|
521
|
+
"notes": "Benchmarks per the model card."
|
|
522
|
+
}
|
|
478
523
|
},
|
|
479
524
|
{
|
|
480
525
|
"id": "openrouter/z-ai/glm-5.3-flash",
|
|
481
526
|
"name": "GLM 5.3 Flash (OR)",
|
|
482
527
|
"provider": "openrouter",
|
|
483
528
|
"enabled": true,
|
|
484
|
-
"modality": "text+image+video->text"
|
|
529
|
+
"modality": "text+image+video->text",
|
|
530
|
+
"info": {
|
|
531
|
+
"params": "320B total · 18B activated",
|
|
532
|
+
"docUrl": "https://docs.z.ai/guides/vlm/glm-5.3-flash",
|
|
533
|
+
"notes": "Sizes per the vendor documentation."
|
|
534
|
+
}
|
|
485
535
|
},
|
|
486
536
|
{
|
|
487
537
|
"id": "openrouter/z-ai/glm-5.3-flashx",
|
|
@@ -836,7 +886,11 @@
|
|
|
836
886
|
"name": "Kimi K3 (OR)",
|
|
837
887
|
"provider": "openrouter",
|
|
838
888
|
"enabled": false,
|
|
839
|
-
"modality": "text+image+video->text"
|
|
889
|
+
"modality": "text+image+video->text",
|
|
890
|
+
"info": {
|
|
891
|
+
"params": "2.78T total · 104B activated · 93 layers",
|
|
892
|
+
"notes": "Activated parameters and layers per the model card."
|
|
893
|
+
}
|
|
840
894
|
},
|
|
841
895
|
{
|
|
842
896
|
"id": "openrouter/amazon/nova-premier-v1",
|
|
@@ -885,14 +939,22 @@
|
|
|
885
939
|
"name": "DeepSeek Flash (V4.1)",
|
|
886
940
|
"provider": "deepseek",
|
|
887
941
|
"enabled": true,
|
|
888
|
-
"modality": "text+image->text"
|
|
942
|
+
"modality": "text+image->text",
|
|
943
|
+
"info": {
|
|
944
|
+
"params": "552B backbone · 8–16B activated · 384 routed experts (6 per token) + 1 shared",
|
|
945
|
+
"notes": "Sizes per the model card."
|
|
946
|
+
}
|
|
889
947
|
},
|
|
890
948
|
{
|
|
891
949
|
"id": "openrouter/deepseek/deepseek-v4.1-flash",
|
|
892
950
|
"name": "DeepSeek V4.1 Flash (OR)",
|
|
893
951
|
"provider": "openrouter",
|
|
894
952
|
"enabled": false,
|
|
895
|
-
"modality": "text+image->text"
|
|
953
|
+
"modality": "text+image->text",
|
|
954
|
+
"info": {
|
|
955
|
+
"params": "552B backbone · 8–16B activated · 384 routed experts (6 per token) + 1 shared",
|
|
956
|
+
"notes": "Sizes per the model card."
|
|
957
|
+
}
|
|
896
958
|
},
|
|
897
959
|
{
|
|
898
960
|
"id": "openrouter/deepseek/deepseek-v4-flash-vision-exp",
|
|
@@ -1208,7 +1270,11 @@
|
|
|
1208
1270
|
"name": "Qwen3.8 2.4T A95B (OR)",
|
|
1209
1271
|
"provider": "openrouter",
|
|
1210
1272
|
"enabled": false,
|
|
1211
|
-
"modality": "text->text"
|
|
1273
|
+
"modality": "text->text",
|
|
1274
|
+
"info": {
|
|
1275
|
+
"params": "2.4T total · 95B activated",
|
|
1276
|
+
"notes": "Sizes per the model card."
|
|
1277
|
+
}
|
|
1212
1278
|
},
|
|
1213
1279
|
{
|
|
1214
1280
|
"id": "openrouter/qwen/qwen3.8-27b",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flame0510/project-aether",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"*.css",
|
|
47
47
|
"*.png",
|
|
48
48
|
"model-pricing.json",
|
|
49
|
+
"model-details.json",
|
|
49
50
|
"models.config.json"
|
|
50
51
|
],
|
|
51
52
|
"scripts": {
|
|
@@ -55,7 +56,8 @@
|
|
|
55
56
|
"start:all": "rev4a serve",
|
|
56
57
|
"refresh:pricing": "node scripts/refresh-model-pricing.mjs",
|
|
57
58
|
"smoke:db-bootstrap": "node scripts/smoke-db-bootstrap.mjs",
|
|
58
|
-
"postinstall": "node bin/postinstall.js"
|
|
59
|
+
"postinstall": "node bin/postinstall.js",
|
|
60
|
+
"check:language": "node scripts/check-language.mjs"
|
|
59
61
|
},
|
|
60
62
|
"dependencies": {
|
|
61
63
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fails when Italian words appear in tracked files: the project is 100% English — code,
|
|
4
|
+
* identifiers, comments, docs, UI strings (AGENTS.md §2.1).
|
|
5
|
+
*
|
|
6
|
+
* The word list is deliberately conservative: only words that are **not** English words.
|
|
7
|
+
* `come`, `per`, `non`, `la`, `al`, `tra` are left out on purpose, they would fire on
|
|
8
|
+
* English text; the list is content words plus unmistakable function words. A line
|
|
9
|
+
* carrying `lang-ok` is skipped, for the legitimate case where the Italian is the point
|
|
10
|
+
* (a fixture, a note about the rule):
|
|
11
|
+
*
|
|
12
|
+
* // lang-ok: the exact text a user reported
|
|
13
|
+
*
|
|
14
|
+
* It catches Italian *words*, not Italian thinking written in English, and it never
|
|
15
|
+
* translates: the fix is manual. Wired into the release workflow (a release cannot ship
|
|
16
|
+
* Italian) and available locally as `npm run check:language`.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node scripts/check-language.mjs # every tracked file
|
|
20
|
+
* node scripts/check-language.mjs --staged # only what is staged (a pre-commit hook)
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
|
|
25
|
+
const WORDS = [
|
|
26
|
+
// Content words, where English and Italian cannot be confused.
|
|
27
|
+
'listino', 'prezzo', 'prezzi', 'catalogo', 'modello', 'modelli', 'parametri', 'dimensione',
|
|
28
|
+
'sessione', 'sessioni', 'caricamento', 'schermata', 'impostazioni', 'cronologia', 'velocita',
|
|
29
|
+
'chiudi', 'annulla', 'elimina', 'aggiungi', 'rimuovi', 'salva', 'avvia', 'ferma',
|
|
30
|
+
'errore', 'attenzione', 'conferma', 'avviso', 'messaggio', 'messaggi', 'scheda', 'schede',
|
|
31
|
+
'utente', 'utenti', 'righe', 'tasto', 'tasti', 'finestra', 'finestre', 'valore', 'valori',
|
|
32
|
+
'numero', 'numeri', 'giornata', 'lavoro', 'sotto', 'sopra', 'tra', 'fra', 'senza', 'anche',
|
|
33
|
+
'questo', 'questa', 'questi', 'queste', 'perche', 'quando', 'molto', 'poco', 'deve', 'devono',
|
|
34
|
+
'possono', 'viene', 'vengono', 'fatto', 'fatta', 'gia', 'solo', 'tutti', 'tutte', 'ogni',
|
|
35
|
+
'essere', 'fare', 'dove', 'nuovo', 'nuova', 'vecchio', 'vecchia', 'davvero', 'invece',
|
|
36
|
+
'inoltre', 'quindi', 'della', 'delle', 'degli', 'nella', 'nelle', 'negli', 'dalla', 'dalle',
|
|
37
|
+
'dallo', 'allo', 'alla', 'sul', 'sulla', 'nessuno', 'nessuna', 'adesso', 'subito', 'oggi', 'ieri',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.md', '.json', '.sh', '.yml', '.yaml', '.css', '.html'];
|
|
41
|
+
/** Lockfiles: generated, huge, and never the place a word is written by hand. */
|
|
42
|
+
const IGNORED = new Set(['package-lock.json', 'scripts/check-language.mjs']);
|
|
43
|
+
const MARKER = 'lang-ok';
|
|
44
|
+
|
|
45
|
+
const staged = process.argv.includes('--staged');
|
|
46
|
+
const gitArgs = staged
|
|
47
|
+
? ['diff', '--cached', '--name-only', '--diff-filter=ACM']
|
|
48
|
+
: ['ls-files'];
|
|
49
|
+
const files = execFileSync('git', gitArgs, { encoding: 'utf-8' }).split('\n').filter(Boolean);
|
|
50
|
+
const pattern = new RegExp(`\\b(${WORDS.join('|')})\\b`, 'i');
|
|
51
|
+
|
|
52
|
+
const hits = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (IGNORED.has(file) || !EXTENSIONS.some((ext) => file.endsWith(ext))) continue;
|
|
55
|
+
let text;
|
|
56
|
+
try {
|
|
57
|
+
text = readFileSync(file, 'utf-8');
|
|
58
|
+
} catch {
|
|
59
|
+
continue; // unreadable or binary
|
|
60
|
+
}
|
|
61
|
+
text.split('\n').forEach((line, index) => {
|
|
62
|
+
if (line.includes(MARKER)) return;
|
|
63
|
+
const match = line.match(pattern);
|
|
64
|
+
if (match) hits.push({ file, line: index + 1, word: match[0].toLowerCase(), text: line.trim().slice(0, 120) });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hits.length === 0) {
|
|
69
|
+
console.log(`✓ No Italian words in ${files.length} tracked file(s).`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.error(`✗ ${hits.length} line(s) contain Italian words (the project is 100% English):\n`);
|
|
74
|
+
for (const hit of hits) console.error(` ${hit.file}:${hit.line} [${hit.word}] ${hit.text}`);
|
|
75
|
+
console.error('\n Rewrite the line in English, or add `lang-ok` to it when the Italian is the point.');
|
|
76
|
+
process.exit(1);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream (OpenRouter) catalogue lookup, shared by `refresh-model-pricing.mjs` and
|
|
3
|
+
* `model-info-suggest.mjs`: one place decides which upstream entry a catalogue id
|
|
4
|
+
* corresponds to, so a model is never matched two different ways.
|
|
5
|
+
*
|
|
6
|
+
* No dependencies, public endpoint, no API key (see docs/dev/GATEWAY.md).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const CATALOGUE_URL = 'https://openrouter.ai/api/v1/models';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A direct provider's OpenRouter vendor name, when it differs from ours:
|
|
13
|
+
* `glm/glm-5.3` is `z-ai/glm-5.3` upstream, `kimi/kimi-k3` is `moonshotai/kimi-k3`.
|
|
14
|
+
* For details and docs only — prices for direct entries come from the vendor's own list.
|
|
15
|
+
*/
|
|
16
|
+
export const VENDOR_ALIAS = { glm: 'z-ai', kimi: 'moonshotai' };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Same model, different name on OpenRouter. Only exact same-model cases belong here: a
|
|
20
|
+
* different tier (`-highspeed`) or another vendor's copy of the weights is a curated
|
|
21
|
+
* `info` block, not an alias, because description and benchmarks would be someone else's.
|
|
22
|
+
*/
|
|
23
|
+
export const MODEL_ALIAS = {
|
|
24
|
+
'deepseek/deepseek-flash': 'deepseek/deepseek-v4.1-flash', // our name for DeepSeek's V4.1
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Our catalogue namespaces OpenRouter entries as `openrouter/<id>`; the router itself is `openrouter/auto`. */
|
|
28
|
+
export function upstreamCandidates(id) {
|
|
29
|
+
const stripped = id.slice('openrouter/'.length);
|
|
30
|
+
return stripped === id ? [id] : [stripped, id];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Try the id with its version separators swapped: `claude-haiku-4-5` ↔ `claude-haiku-4.5`. */
|
|
34
|
+
export function separatorVariants(id) {
|
|
35
|
+
return [
|
|
36
|
+
id.replace(/-(\d+)-(\d+)$/, '-$1.$2'),
|
|
37
|
+
id.replace(/-(\d+)\.(\d+)$/, '-$1-$2'),
|
|
38
|
+
].filter((v) => v !== id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The upstream entry a catalogue id corresponds to, or null. */
|
|
42
|
+
export function matchUpstream(live, id) {
|
|
43
|
+
for (const candidate of upstreamCandidates(id)) if (live.has(candidate)) return live.get(candidate);
|
|
44
|
+
const slash = id.indexOf('/');
|
|
45
|
+
const alias = slash > 0 ? VENDOR_ALIAS[id.slice(0, slash)] : undefined;
|
|
46
|
+
const rest = slash > 0 ? id.slice(slash + 1) : '';
|
|
47
|
+
if (alias && live.has(`${alias}/${rest}`)) return live.get(`${alias}/${rest}`);
|
|
48
|
+
const mapped = MODEL_ALIAS[id];
|
|
49
|
+
if (mapped && live.has(mapped)) return live.get(mapped);
|
|
50
|
+
for (const variant of separatorVariants(id)) if (live.has(variant)) return live.get(variant);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Fetch the whole upstream catalogue once. Throws when the response is not a catalogue. */
|
|
55
|
+
export async function fetchUpstream() {
|
|
56
|
+
const res = await fetch(CATALOGUE_URL, { headers: { accept: 'application/json' } });
|
|
57
|
+
if (!res.ok) throw new Error(`Catalogue returned HTTP ${res.status}`);
|
|
58
|
+
const body = await res.json();
|
|
59
|
+
if (!Array.isArray(body?.data) || body.data.length === 0) throw new Error('Unexpected catalogue shape');
|
|
60
|
+
return new Map(body.data.map((m) => [m.id, m]));
|
|
61
|
+
}
|