@lobehub/chat 1.111.0 → 1.111.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/.cursor/rules/code-review.mdc +2 -19
- package/.cursor/rules/cursor-ux.mdc +0 -72
- package/.cursor/rules/project-introduce.mdc +5 -5
- package/.cursor/rules/react-component.mdc +92 -73
- package/.cursor/rules/rules-attach.mdc +28 -61
- package/.cursor/rules/system-role.mdc +8 -20
- package/.cursor/rules/typescript.mdc +55 -14
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/package.json +1 -1
- package/packages/types/src/aiModel.ts +67 -46
- package/packages/types/src/llm.ts +3 -3
- package/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Details/Overview/ProviderList/index.tsx +23 -12
- package/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Details/Overview/ModelList/index.tsx +23 -10
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +21 -12
- package/src/config/aiModels/ai21.ts +8 -4
- package/src/config/aiModels/ai360.ts +28 -14
- package/src/config/aiModels/aihubmix.ts +174 -86
- package/src/config/aiModels/anthropic.ts +97 -38
- package/src/config/aiModels/azure.ts +54 -32
- package/src/config/aiModels/azureai.ts +63 -37
- package/src/config/aiModels/baichuan.ts +24 -12
- package/src/config/aiModels/bedrock.ts +60 -30
- package/src/config/aiModels/cohere.ts +60 -30
- package/src/config/aiModels/deepseek.ts +10 -6
- package/src/config/aiModels/fal.ts +43 -6
- package/src/config/aiModels/fireworksai.ts +88 -44
- package/src/config/aiModels/giteeai.ts +1 -1
- package/src/config/aiModels/github.ts +44 -26
- package/src/config/aiModels/google.ts +119 -68
- package/src/config/aiModels/groq.ts +48 -24
- package/src/config/aiModels/higress.ts +617 -310
- package/src/config/aiModels/hunyuan.ts +105 -54
- package/src/config/aiModels/infiniai.ts +104 -52
- package/src/config/aiModels/internlm.ts +16 -8
- package/src/config/aiModels/jina.ts +4 -2
- package/src/config/aiModels/minimax.ts +11 -10
- package/src/config/aiModels/mistral.ts +40 -20
- package/src/config/aiModels/moonshot.ts +42 -22
- package/src/config/aiModels/novita.ts +196 -98
- package/src/config/aiModels/openai.ts +270 -137
- package/src/config/aiModels/openrouter.ts +205 -100
- package/src/config/aiModels/perplexity.ts +36 -6
- package/src/config/aiModels/ppio.ts +76 -38
- package/src/config/aiModels/qwen.ts +257 -133
- package/src/config/aiModels/sambanova.ts +56 -28
- package/src/config/aiModels/sensenova.ts +100 -50
- package/src/config/aiModels/siliconcloud.ts +224 -112
- package/src/config/aiModels/stepfun.ts +44 -22
- package/src/config/aiModels/taichu.ts +8 -4
- package/src/config/aiModels/tencentcloud.ts +12 -6
- package/src/config/aiModels/upstage.ts +8 -4
- package/src/config/aiModels/v0.ts +15 -12
- package/src/config/aiModels/vertexai.ts +49 -27
- package/src/config/aiModels/volcengine.ts +110 -51
- package/src/config/aiModels/wenxin.ts +179 -73
- package/src/config/aiModels/xai.ts +33 -19
- package/src/config/aiModels/zeroone.ts +48 -24
- package/src/config/aiModels/zhipu.ts +118 -69
- package/src/config/modelProviders/ai21.ts +0 -8
- package/src/config/modelProviders/ai360.ts +0 -20
- package/src/config/modelProviders/anthropic.ts +0 -56
- package/src/config/modelProviders/baichuan.ts +0 -30
- package/src/config/modelProviders/bedrock.ts +0 -74
- package/src/config/modelProviders/deepseek.ts +0 -13
- package/src/config/modelProviders/fireworksai.ts +0 -88
- package/src/config/modelProviders/google.ts +0 -59
- package/src/config/modelProviders/groq.ts +0 -48
- package/src/config/modelProviders/higress.ts +0 -727
- package/src/config/modelProviders/hunyuan.ts +0 -45
- package/src/config/modelProviders/infiniai.ts +0 -60
- package/src/config/modelProviders/internlm.ts +0 -8
- package/src/config/modelProviders/mistral.ts +0 -48
- package/src/config/modelProviders/modelscope.ts +2 -1
- package/src/config/modelProviders/openai.ts +5 -100
- package/src/config/modelProviders/openrouter.ts +0 -77
- package/src/config/modelProviders/ppio.ts +0 -95
- package/src/config/modelProviders/qwen.ts +0 -165
- package/src/config/modelProviders/sensenova.ts +0 -45
- package/src/config/modelProviders/siliconcloud.ts +0 -266
- package/src/config/modelProviders/stepfun.ts +0 -60
- package/src/config/modelProviders/taichu.ts +0 -10
- package/src/config/modelProviders/wenxin.ts +0 -90
- package/src/config/modelProviders/xai.ts +0 -16
- package/src/config/modelProviders/zeroone.ts +0 -60
- package/src/config/modelProviders/zhipu.ts +0 -80
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +4 -3
- package/src/features/Conversation/Extras/Usage/UsageDetail/pricing.ts +25 -15
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +7 -5
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +6 -5
- package/src/libs/model-runtime/fal/index.ts +8 -2
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +54 -8
- package/src/server/routers/lambda/agent.ts +2 -2
- package/src/server/routers/lambda/config/__snapshots__/index.test.ts.snap +0 -28
- package/src/server/services/discover/index.ts +7 -6
- package/src/server/services/user/index.ts +1 -2
- package/src/utils/__snapshots__/parseModels.test.ts.snap +28 -4
- package/src/utils/_deprecated/__snapshots__/parseModels.test.ts.snap +0 -8
- package/src/utils/parseModels.test.ts +60 -9
- package/src/utils/pricing.test.ts +183 -0
- package/src/utils/pricing.ts +90 -0
@@ -732,9 +732,26 @@ describe('transformToChatModelCards', () => {
|
|
732
732
|
id: 'gpt-4o',
|
733
733
|
maxOutput: 4096,
|
734
734
|
pricing: {
|
735
|
-
|
736
|
-
|
737
|
-
|
735
|
+
units: [
|
736
|
+
{
|
737
|
+
name: 'textInput_cacheRead',
|
738
|
+
rate: 1.25,
|
739
|
+
strategy: 'fixed',
|
740
|
+
unit: 'millionTokens',
|
741
|
+
},
|
742
|
+
{
|
743
|
+
name: 'textInput',
|
744
|
+
rate: 2.5,
|
745
|
+
strategy: 'fixed',
|
746
|
+
unit: 'millionTokens',
|
747
|
+
},
|
748
|
+
{
|
749
|
+
name: 'textOutput',
|
750
|
+
rate: 10,
|
751
|
+
strategy: 'fixed',
|
752
|
+
unit: 'millionTokens',
|
753
|
+
},
|
754
|
+
],
|
738
755
|
},
|
739
756
|
providerId: 'azure',
|
740
757
|
releasedAt: '2024-05-13',
|
@@ -753,9 +770,26 @@ describe('transformToChatModelCards', () => {
|
|
753
770
|
id: 'gpt-4o-mini',
|
754
771
|
maxOutput: 4096,
|
755
772
|
pricing: {
|
756
|
-
|
757
|
-
|
758
|
-
|
773
|
+
units: [
|
774
|
+
{
|
775
|
+
name: 'textInput_cacheRead',
|
776
|
+
rate: 0.075,
|
777
|
+
strategy: 'fixed',
|
778
|
+
unit: 'millionTokens',
|
779
|
+
},
|
780
|
+
{
|
781
|
+
name: 'textInput',
|
782
|
+
rate: 0.15,
|
783
|
+
strategy: 'fixed',
|
784
|
+
unit: 'millionTokens',
|
785
|
+
},
|
786
|
+
{
|
787
|
+
name: 'textOutput',
|
788
|
+
rate: 0.6,
|
789
|
+
strategy: 'fixed',
|
790
|
+
unit: 'millionTokens',
|
791
|
+
},
|
792
|
+
],
|
759
793
|
},
|
760
794
|
type: 'chat',
|
761
795
|
},
|
@@ -772,9 +806,26 @@ describe('transformToChatModelCards', () => {
|
|
772
806
|
id: 'o1-mini',
|
773
807
|
maxOutput: 65536,
|
774
808
|
pricing: {
|
775
|
-
|
776
|
-
|
777
|
-
|
809
|
+
units: [
|
810
|
+
{
|
811
|
+
name: 'textInput_cacheRead',
|
812
|
+
rate: 0.55,
|
813
|
+
strategy: 'fixed',
|
814
|
+
unit: 'millionTokens',
|
815
|
+
},
|
816
|
+
{
|
817
|
+
name: 'textInput',
|
818
|
+
rate: 1.1,
|
819
|
+
strategy: 'fixed',
|
820
|
+
unit: 'millionTokens',
|
821
|
+
},
|
822
|
+
{
|
823
|
+
name: 'textOutput',
|
824
|
+
rate: 4.4,
|
825
|
+
strategy: 'fixed',
|
826
|
+
unit: 'millionTokens',
|
827
|
+
},
|
828
|
+
],
|
778
829
|
},
|
779
830
|
releasedAt: '2024-09-12',
|
780
831
|
type: 'chat',
|
@@ -0,0 +1,183 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { Pricing } from '@/types/aiModel';
|
4
|
+
|
5
|
+
import {
|
6
|
+
getAudioInputUnitRate,
|
7
|
+
getAudioOutputUnitRate,
|
8
|
+
getCachedAudioInputUnitRate,
|
9
|
+
getCachedTextInputUnitRate,
|
10
|
+
getTextInputUnitRate,
|
11
|
+
getTextOutputUnitRate,
|
12
|
+
getUnitRateByName,
|
13
|
+
getWriteCacheInputUnitRate,
|
14
|
+
} from './pricing';
|
15
|
+
|
16
|
+
describe('pricing utilities (new)', () => {
|
17
|
+
describe('getUnitRateByName', () => {
|
18
|
+
it('returns undefined when pricing or unitName is missing', () => {
|
19
|
+
expect(getUnitRateByName()).toBeUndefined();
|
20
|
+
const p = { units: [] } as Pricing;
|
21
|
+
expect(getUnitRateByName(p)).toBeUndefined();
|
22
|
+
});
|
23
|
+
|
24
|
+
it('returns undefined when unit not found', () => {
|
25
|
+
const pricing: Pricing = {
|
26
|
+
units: [{ name: 'textOutput', strategy: 'fixed', unit: 'millionTokens', rate: 0.002 }],
|
27
|
+
};
|
28
|
+
expect(getUnitRateByName(pricing, 'textInput')).toBeUndefined();
|
29
|
+
});
|
30
|
+
|
31
|
+
it('handles fixed strategy', () => {
|
32
|
+
const pricing: Pricing = {
|
33
|
+
units: [{ name: 'textInput', strategy: 'fixed', unit: 'millionTokens', rate: 0.001 }],
|
34
|
+
};
|
35
|
+
expect(getUnitRateByName(pricing, 'textInput')).toBe(0.001);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('handles tiered strategy (first tier)', () => {
|
39
|
+
const pricing: Pricing = {
|
40
|
+
units: [
|
41
|
+
{
|
42
|
+
name: 'audioInput',
|
43
|
+
strategy: 'tiered',
|
44
|
+
unit: 'second',
|
45
|
+
tiers: [
|
46
|
+
{ rate: 0.01, upTo: 3600 },
|
47
|
+
{ rate: 0.008, upTo: 'infinity' },
|
48
|
+
],
|
49
|
+
},
|
50
|
+
],
|
51
|
+
};
|
52
|
+
expect(getUnitRateByName(pricing, 'audioInput')).toBe(0.01);
|
53
|
+
});
|
54
|
+
|
55
|
+
it('returns undefined when tiered.tiers is absent or empty', () => {
|
56
|
+
const noTiers: Pricing = {
|
57
|
+
units: [
|
58
|
+
{ name: 'textInput', strategy: 'tiered', unit: 'millionTokens', tiers: undefined as any },
|
59
|
+
],
|
60
|
+
};
|
61
|
+
const emptyTiers: Pricing = {
|
62
|
+
units: [{ name: 'textInput', strategy: 'tiered', unit: 'millionTokens', tiers: [] }],
|
63
|
+
};
|
64
|
+
expect(getUnitRateByName(noTiers, 'textInput')).toBeUndefined();
|
65
|
+
expect(getUnitRateByName(emptyTiers, 'textInput')).toBeUndefined();
|
66
|
+
});
|
67
|
+
|
68
|
+
it('handles lookup strategy (first price value)', () => {
|
69
|
+
const pricing: Pricing = {
|
70
|
+
units: [
|
71
|
+
{
|
72
|
+
name: 'textInput',
|
73
|
+
strategy: 'lookup',
|
74
|
+
unit: 'millionTokens',
|
75
|
+
lookup: {
|
76
|
+
pricingParams: ['model'],
|
77
|
+
prices: { a: 0.001, b: 0.03 },
|
78
|
+
},
|
79
|
+
},
|
80
|
+
],
|
81
|
+
};
|
82
|
+
expect(getUnitRateByName(pricing, 'textInput')).toBe(0.001);
|
83
|
+
});
|
84
|
+
|
85
|
+
it('returns undefined when lookup missing or has no prices', () => {
|
86
|
+
const missingLookup: Pricing = {
|
87
|
+
units: [
|
88
|
+
{
|
89
|
+
name: 'textInput',
|
90
|
+
strategy: 'lookup',
|
91
|
+
unit: 'millionTokens',
|
92
|
+
lookup: undefined as any,
|
93
|
+
},
|
94
|
+
],
|
95
|
+
} as any;
|
96
|
+
const emptyPrices: Pricing = {
|
97
|
+
units: [
|
98
|
+
{
|
99
|
+
name: 'textInput',
|
100
|
+
strategy: 'lookup',
|
101
|
+
unit: 'millionTokens',
|
102
|
+
lookup: { pricingParams: ['model'], prices: {} },
|
103
|
+
},
|
104
|
+
],
|
105
|
+
};
|
106
|
+
expect(getUnitRateByName(missingLookup, 'textInput')).toBeUndefined();
|
107
|
+
expect(getUnitRateByName(emptyPrices, 'textInput')).toBeUndefined();
|
108
|
+
});
|
109
|
+
|
110
|
+
it('works with multiple units', () => {
|
111
|
+
const pricing: Pricing = {
|
112
|
+
units: [
|
113
|
+
{ name: 'textInput', strategy: 'fixed', unit: 'millionTokens', rate: 0.001 },
|
114
|
+
{ name: 'textOutput', strategy: 'fixed', unit: 'millionTokens', rate: 0.002 },
|
115
|
+
{
|
116
|
+
name: 'audioInput',
|
117
|
+
strategy: 'tiered',
|
118
|
+
unit: 'second',
|
119
|
+
tiers: [
|
120
|
+
{ rate: 0.01, upTo: 3600 },
|
121
|
+
{ rate: 0.008, upTo: 'infinity' },
|
122
|
+
],
|
123
|
+
},
|
124
|
+
],
|
125
|
+
};
|
126
|
+
expect(getUnitRateByName(pricing, 'textInput')).toBe(0.001);
|
127
|
+
expect(getUnitRateByName(pricing, 'textOutput')).toBe(0.002);
|
128
|
+
expect(getUnitRateByName(pricing, 'audioInput')).toBe(0.01);
|
129
|
+
});
|
130
|
+
});
|
131
|
+
|
132
|
+
describe('wrapper helpers', () => {
|
133
|
+
it('return the same values as getUnitRateByName for each unit', () => {
|
134
|
+
const pricing: Pricing = {
|
135
|
+
units: [
|
136
|
+
{ name: 'textInput', strategy: 'fixed', unit: 'millionTokens', rate: 0.001 },
|
137
|
+
{ name: 'textOutput', strategy: 'fixed', unit: 'millionTokens', rate: 0.002 },
|
138
|
+
{
|
139
|
+
name: 'audioInput',
|
140
|
+
strategy: 'tiered',
|
141
|
+
unit: 'second',
|
142
|
+
tiers: [
|
143
|
+
{ rate: 0.01, upTo: 3600 },
|
144
|
+
{ rate: 0.008, upTo: 'infinity' },
|
145
|
+
],
|
146
|
+
},
|
147
|
+
{ name: 'audioOutput', strategy: 'fixed', unit: 'second', rate: 0.015 },
|
148
|
+
{ name: 'textInput_cacheRead', strategy: 'fixed', unit: 'millionTokens', rate: 0.0005 },
|
149
|
+
{
|
150
|
+
name: 'textInput_cacheWrite',
|
151
|
+
strategy: 'lookup',
|
152
|
+
unit: 'millionTokens',
|
153
|
+
lookup: { pricingParams: ['ttl'], prices: { '5': 0.2, '60': 0.6 } },
|
154
|
+
},
|
155
|
+
{ name: 'audioInput_cacheRead', strategy: 'fixed', unit: 'second', rate: 0.005 },
|
156
|
+
],
|
157
|
+
};
|
158
|
+
|
159
|
+
expect(getTextInputUnitRate(pricing)).toBe(getUnitRateByName(pricing, 'textInput'));
|
160
|
+
expect(getTextOutputUnitRate(pricing)).toBe(getUnitRateByName(pricing, 'textOutput'));
|
161
|
+
expect(getAudioInputUnitRate(pricing)).toBe(getUnitRateByName(pricing, 'audioInput'));
|
162
|
+
expect(getAudioOutputUnitRate(pricing)).toBe(getUnitRateByName(pricing, 'audioOutput'));
|
163
|
+
expect(getCachedTextInputUnitRate(pricing)).toBe(
|
164
|
+
getUnitRateByName(pricing, 'textInput_cacheRead'),
|
165
|
+
);
|
166
|
+
expect(getWriteCacheInputUnitRate(pricing)).toBe(
|
167
|
+
getUnitRateByName(pricing, 'textInput_cacheWrite'),
|
168
|
+
);
|
169
|
+
expect(getCachedAudioInputUnitRate(pricing)).toBe(
|
170
|
+
getUnitRateByName(pricing, 'audioInput_cacheRead'),
|
171
|
+
);
|
172
|
+
|
173
|
+
// also validate expected concrete values for clarity
|
174
|
+
expect(getTextInputUnitRate(pricing)).toBe(0.001);
|
175
|
+
expect(getTextOutputUnitRate(pricing)).toBe(0.002);
|
176
|
+
expect(getAudioInputUnitRate(pricing)).toBe(0.01);
|
177
|
+
expect(getAudioOutputUnitRate(pricing)).toBe(0.015);
|
178
|
+
expect(getCachedTextInputUnitRate(pricing)).toBe(0.0005);
|
179
|
+
expect(getWriteCacheInputUnitRate(pricing)).toBe(0.2);
|
180
|
+
expect(getCachedAudioInputUnitRate(pricing)).toBe(0.005);
|
181
|
+
});
|
182
|
+
});
|
183
|
+
});
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import { Pricing, PricingUnit, PricingUnitName } from '@/types/aiModel';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Internal helper to extract the displayed unit rate from a pricing unit by strategy
|
5
|
+
* - fixed → rate
|
6
|
+
* - tiered → tiers[0].rate
|
7
|
+
* - lookup → first price value
|
8
|
+
*/
|
9
|
+
const getRateFromUnit = (unit: PricingUnit): number | undefined => {
|
10
|
+
switch (unit.strategy) {
|
11
|
+
case 'fixed': {
|
12
|
+
return unit.rate;
|
13
|
+
}
|
14
|
+
case 'tiered': {
|
15
|
+
return unit.tiers?.[0]?.rate;
|
16
|
+
}
|
17
|
+
case 'lookup': {
|
18
|
+
const prices = Object.values(unit.lookup?.prices || {});
|
19
|
+
return prices[0];
|
20
|
+
}
|
21
|
+
default: {
|
22
|
+
return undefined;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Get unit rate by unit name, used to deduplicate logic across helpers
|
29
|
+
*/
|
30
|
+
export const getUnitRateByName = (
|
31
|
+
pricing?: Pricing,
|
32
|
+
unitName?: PricingUnitName,
|
33
|
+
): number | undefined => {
|
34
|
+
if (!pricing?.units || !unitName) return undefined;
|
35
|
+
const unit = pricing.units.find((u) => u.name === unitName);
|
36
|
+
if (!unit) return undefined;
|
37
|
+
return getRateFromUnit(unit);
|
38
|
+
};
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Get text input unit rate from pricing
|
42
|
+
* - fixed → rate
|
43
|
+
* - tiered → tiers[0].rate
|
44
|
+
* - lookup → Object.values(lookup.prices)[0]
|
45
|
+
*/
|
46
|
+
export function getTextInputUnitRate(pricing?: Pricing): number | undefined {
|
47
|
+
return getUnitRateByName(pricing, 'textInput');
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Get text output unit rate from pricing
|
52
|
+
*/
|
53
|
+
export function getTextOutputUnitRate(pricing?: Pricing): number | undefined {
|
54
|
+
return getUnitRateByName(pricing, 'textOutput');
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Get audio input unit rate from pricing
|
59
|
+
*/
|
60
|
+
export function getAudioInputUnitRate(pricing?: Pricing): number | undefined {
|
61
|
+
return getUnitRateByName(pricing, 'audioInput');
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Get audio output unit rate from pricing
|
66
|
+
*/
|
67
|
+
export function getAudioOutputUnitRate(pricing?: Pricing): number | undefined {
|
68
|
+
return getUnitRateByName(pricing, 'audioOutput');
|
69
|
+
}
|
70
|
+
|
71
|
+
/**
|
72
|
+
* Get cached text input unit rate from pricing
|
73
|
+
*/
|
74
|
+
export function getCachedTextInputUnitRate(pricing?: Pricing): number | undefined {
|
75
|
+
return getUnitRateByName(pricing, 'textInput_cacheRead');
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Get write cache input unit rate from pricing (TextInputCacheWrite)
|
80
|
+
*/
|
81
|
+
export function getWriteCacheInputUnitRate(pricing?: Pricing): number | undefined {
|
82
|
+
return getUnitRateByName(pricing, 'textInput_cacheWrite');
|
83
|
+
}
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Get cached audio input unit rate from pricing
|
87
|
+
*/
|
88
|
+
export function getCachedAudioInputUnitRate(pricing?: Pricing): number | undefined {
|
89
|
+
return getUnitRateByName(pricing, 'audioInput_cacheRead');
|
90
|
+
}
|