Skip to content

Commit 6cc2ba4

Browse files
author
linyuan.yang
committed
模型配置
1 parent d623694 commit 6cc2ba4

12 files changed

Lines changed: 140 additions & 63 deletions

File tree

packages/admin/src/i18n/en.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ export default {
218218
thinking: 'Thinking',
219219
thinking_none: 'Default (off)',
220220
thinking_budget: 'Budget Tokens',
221+
prompt_caching: 'Prompt Caching',
222+
prompt_caching_desc: 'Enable prompt caching (saves ~90% input cost for multi-turn conversations)',
221223
},
222224
embeddings: {
223225
add: '+ Add Embedding',
@@ -638,11 +640,11 @@ export default {
638640
usage: {
639641
title: 'Token Usage Stats',
640642
date: 'Date',
641-
input_tokens: 'Input',
642-
output_tokens: 'Output',
643-
total_tokens: 'Total',
644-
cache_read: 'Cache Hit',
645-
cache_creation: 'Cache Create',
643+
input_tokens: 'Input Tokens',
644+
output_tokens: 'Output Tokens',
645+
total_tokens: 'Total Tokens',
646+
cache_read: 'Cache Read Tokens',
647+
cache_creation: 'Cache Write Tokens',
646648
no_data: 'No data',
647649
last: 'Last',
648650
total: 'Cumulative',

packages/admin/src/i18n/zh.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ export default {
218218
thinking: '思考模式',
219219
thinking_none: '默认(关闭)',
220220
thinking_budget: '思考 Token 预算',
221+
prompt_caching: '提示缓存',
222+
prompt_caching_desc: '启用 Prompt Caching(多轮对话可节省约 90% 输入费用)',
221223
},
222224
embeddings: {
223225
add: '+ 添加向量模型',
@@ -638,11 +640,11 @@ export default {
638640
usage: {
639641
title: 'Token 用量统计',
640642
date: '日期',
641-
input_tokens: '输入',
642-
output_tokens: '输出',
643-
total_tokens: '合计',
644-
cache_read: '缓存命中',
645-
cache_creation: '缓存创建',
643+
input_tokens: '输入 Token',
644+
output_tokens: '输出 Token',
645+
total_tokens: '总 Token',
646+
cache_read: '缓存读取 Token',
647+
cache_creation: '缓存写入 Token',
646648
no_data: '暂无数据',
647649
last: '最近',
648650
total: '累计',

packages/admin/src/views/ModelsView.vue

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,34 @@ const showModal = ref(false)
1919
const editingName = ref<string | null>(null)
2020
const showApiKey = ref(false)
2121
const form = ref<ModelConfig>({
22-
name: '', provider: ModelProvider.OpenAI, baseURL: '', apiKey: '', model: '', apiVersion: undefined, temperature: undefined, maxTokens: undefined, contextWindow: undefined, thinking: undefined,
22+
name: '', provider: ModelProvider.OpenAI, baseURL: '', apiKey: '', model: '', temperature: undefined, maxTokens: undefined, contextWindow: undefined,
2323
})
2424
2525
const isOllama = computed(() => form.value.provider === ModelProvider.Ollama)
2626
const isAnthropic = computed(() => form.value.provider === ModelProvider.Anthropic)
2727
const isGemini = computed(() => form.value.provider === ModelProvider.Gemini || form.value.provider === ModelProvider.GeminiImage)
2828
2929
const thinkingType = computed({
30-
get: () => form.value.thinking?.type ?? '',
30+
get: () => form.value.anthropic?.thinking?.type ?? '',
3131
set: (v: string) => {
32-
if (!v) { form.value.thinking = undefined; return }
33-
form.value.thinking = { type: v as any, ...(v === 'enabled' ? { budgetTokens: form.value.thinking?.budgetTokens ?? 8192 } : {}) }
32+
if (!v) { form.value.anthropic = { ...form.value.anthropic, thinking: undefined }; return }
33+
form.value.anthropic = {
34+
...form.value.anthropic,
35+
thinking: { type: v as any, ...(v === 'enabled' ? { budgetTokens: form.value.anthropic?.thinking?.budgetTokens ?? 8192 } : {}) },
36+
}
3437
},
3538
})
3639
const thinkingBudget = computed({
37-
get: () => form.value.thinking?.budgetTokens,
38-
set: (v: number | undefined) => { if (form.value.thinking) form.value.thinking.budgetTokens = v },
40+
get: () => form.value.anthropic?.thinking?.budgetTokens,
41+
set: (v: number | undefined) => { if (form.value.anthropic?.thinking) form.value.anthropic.thinking.budgetTokens = v },
42+
})
43+
const promptCaching = computed({
44+
get: () => form.value.anthropic?.promptCaching ?? false,
45+
set: (v: boolean) => { form.value.anthropic = { ...form.value.anthropic, promptCaching: v || undefined } },
46+
})
47+
const geminiApiVersion = computed({
48+
get: () => form.value.gemini?.apiVersion ?? '',
49+
set: (v: string) => { form.value.gemini = { ...form.value.gemini, apiVersion: v || undefined } },
3950
})
4051
4152
@@ -76,7 +87,7 @@ function pickModel(m: string) {
7687
function openAdd() {
7788
editingName.value = null
7889
showApiKey.value = false
79-
form.value = { name: '', provider: ModelProvider.OpenAI, baseURL: '', apiKey: '', model: '', apiVersion: undefined, temperature: undefined, maxTokens: undefined, contextWindow: undefined }
90+
form.value = { name: '', provider: ModelProvider.OpenAI, baseURL: '', apiKey: '', model: '', temperature: undefined, maxTokens: undefined, contextWindow: undefined }
8091
showModal.value = true
8192
}
8293
@@ -90,10 +101,11 @@ function openEdit(id: string) {
90101
baseURL: m.baseURL,
91102
apiKey: m.apiKey,
92103
model: m.model,
93-
apiVersion: m.apiVersion,
94104
temperature: m.temperature,
95105
maxTokens: m.maxTokens,
96106
contextWindow: m.contextWindow,
107+
anthropic: m.anthropic ? { ...m.anthropic } : undefined,
108+
gemini: m.gemini ? { ...m.gemini } : undefined,
97109
}
98110
showModal.value = true
99111
}
@@ -108,9 +120,16 @@ async function save() {
108120
if (body.temperature === undefined || body.temperature === null) delete body.temperature
109121
if (body.maxTokens === undefined || body.maxTokens === null) delete body.maxTokens
110122
if (body.contextWindow === undefined || body.contextWindow === null) delete body.contextWindow
111-
if (!body.apiVersion) delete body.apiVersion
112-
if (!body.thinking) delete body.thinking
113-
else if (body.thinking.type !== 'enabled') delete body.thinking.budgetTokens
123+
if (body.anthropic) {
124+
if (!body.anthropic.thinking) delete body.anthropic.thinking
125+
else if (body.anthropic.thinking.type !== 'enabled') delete body.anthropic.thinking.budgetTokens
126+
if (!body.anthropic.promptCaching) delete body.anthropic.promptCaching
127+
if (!Object.keys(body.anthropic).length) delete body.anthropic
128+
}
129+
if (body.gemini) {
130+
if (!body.gemini.apiVersion) delete body.gemini.apiVersion
131+
if (!Object.keys(body.gemini).length) delete body.gemini
132+
}
114133
const id = editingName.value
115134
const res = id
116135
? await apiFetch(`/api/settings/models/${encodeURIComponent(id)}`, 'PUT', body)
@@ -257,7 +276,7 @@ async function refresh() {
257276
</div>
258277
<div v-if="isGemini" class="form-group">
259278
<label>{{ t('models.api_version') }}</label>
260-
<input v-model="form.apiVersion" placeholder="v1beta" />
279+
<input v-model="geminiApiVersion" placeholder="v1beta" />
261280
</div>
262281
<div class="form-group">
263282
<label>{{ t('models.temperature') }}</label>
@@ -284,6 +303,10 @@ async function refresh() {
284303
<label>{{ t('models.thinking_budget') }}</label>
285304
<input v-model.number="thinkingBudget" type="number" step="1024" placeholder="8192" />
286305
</div>
306+
<div v-if="isAnthropic" class="form-group">
307+
<label>{{ t('models.prompt_caching') }}</label>
308+
<label class="checkbox-label"><input type="checkbox" v-model="promptCaching" /> {{ t('models.prompt_caching_desc') }}</label>
309+
</div>
287310
</div>
288311
<div class="modal-footer">
289312
<button class="btn-outline" @click="showModal = false">{{ t('common.cancel') }}</button>

packages/channel.xiaoai/src/mi/account.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const SID = 'micoapi';
66
const LOGIN_URL = 'https://account.xiaomi.com/pass/serviceLogin';
77
const AUTH_URL = 'https://account.xiaomi.com/pass/serviceLoginAuth2';
88
const USER_AGENT =
9-
'MiHome/6.0.103 (com.xiaomi.mihome; build:6.0.103.1; Android 14) Alamofire/2.0.1 Channel/stable';
9+
'Dalvik/2.1.0 (Linux; U; Android 10; RMX2111 Build/QP1A.190711.020) APP/xiaomi.mico APPV/2004040 MK/Uk1YMjExMQ== PassportSDK/3.8.3 passport-ui/3.8.3';
1010

1111
function md5(str: string): string {
1212
return crypto.createHash('md5').update(str).digest('hex').toUpperCase();
@@ -26,53 +26,73 @@ function randomDeviceId(): string {
2626
}
2727

2828
function parseAuthResponse(text: string): any {
29-
const cleaned = text.replace('&&&START&&&', '');
29+
const cleaned = text
30+
.replace('&&&START&&&', '')
31+
.replace(/:(\d{9,})/g, ':"$1"');
3032
return JSON.parse(cleaned);
3133
}
3234

35+
function buildCookies(account: MiAccount, deviceId: string): string {
36+
const parts: string[] = [
37+
`userId=${account.userId}`,
38+
`deviceId=${deviceId}`,
39+
'sdkVersion=3.9',
40+
];
41+
if (account.passToken) {
42+
parts.push(`passToken=${account.passToken}`);
43+
}
44+
return parts.join('; ');
45+
}
46+
3347
export async function login(account: MiAccount): Promise<AuthedAccount> {
3448
const deviceId = account.deviceId || randomDeviceId();
35-
const cookies = `userId=${account.userId}; deviceId=${deviceId}`;
49+
const cookies = buildCookies(account, deviceId);
3650

3751
const step1 = await axios.get(LOGIN_URL, {
3852
params: { sid: SID, _json: 'true', _locale: 'zh_CN' },
3953
headers: { 'User-Agent': USER_AGENT, Cookie: cookies },
4054
transformResponse: [(data) => data],
4155
});
42-
const challenge = parseAuthResponse(step1.data);
43-
44-
const formData = new URLSearchParams({
45-
_json: 'true',
46-
qs: challenge.qs,
47-
sid: SID,
48-
_sign: challenge._sign,
49-
callback: challenge.callback,
50-
user: account.userId,
51-
hash: md5(account.password),
52-
});
56+
let pass = parseAuthResponse(step1.data);
5357

54-
const step2 = await axios.post(AUTH_URL, formData.toString(), {
55-
headers: {
56-
'User-Agent': USER_AGENT,
57-
'Content-Type': 'application/x-www-form-urlencoded',
58-
Cookie: cookies,
59-
},
60-
transformResponse: [(data) => data],
61-
});
62-
const authResult = parseAuthResponse(step2.data);
58+
if (pass.code !== 0) {
59+
if (!account.password) {
60+
throw new Error('XiaoAi login failed: password required for re-authentication');
61+
}
62+
const formData = new URLSearchParams({
63+
_json: 'true',
64+
qs: pass.qs,
65+
sid: SID,
66+
_sign: pass._sign,
67+
callback: pass.callback,
68+
user: account.userId,
69+
hash: md5(account.password),
70+
});
71+
72+
const step2 = await axios.post(AUTH_URL, formData.toString(), {
73+
headers: {
74+
'User-Agent': USER_AGENT,
75+
'Content-Type': 'application/x-www-form-urlencoded',
76+
Cookie: cookies,
77+
},
78+
transformResponse: [(data) => data],
79+
});
80+
pass = parseAuthResponse(step2.data);
81+
}
82+
83+
if (pass.location?.includes('identity/authStart')) {
84+
throw new Error('XiaoAi login failed: verification code required, check passToken');
85+
}
6386

64-
if (!authResult.location) {
65-
throw new Error(`XiaoAi login failed: ${authResult.desc || 'unknown error'}`);
87+
if (!pass.location || !pass.nonce || !pass.ssecurity) {
88+
throw new Error(`XiaoAi login failed: ${pass.description || pass.desc || 'invalid credentials'}`);
6689
}
6790

68-
const nonce = authResult.nonce;
69-
const ssecurity = authResult.ssecurity;
70-
const clientSign = sha1(`nonce=${nonce}&${ssecurity}`);
71-
const tokenUrl = `${authResult.location}&clientSign=${encodeURIComponent(clientSign)}`;
91+
const clientSign = sha1(`nonce=${pass.nonce}&${pass.ssecurity}`);
92+
const tokenUrl = `${pass.location}&clientSign=${encodeURIComponent(clientSign)}`;
7293

7394
const step3 = await axios.get(tokenUrl, {
7495
headers: { 'User-Agent': USER_AGENT },
75-
maxRedirects: 0,
7696
validateStatus: (s) => s >= 200 && s < 400,
7797
});
7898

packages/channel.xiaoai/src/mi/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface MiAccount {
22
userId: string;
33
password: string;
4+
passToken?: string;
45
serviceToken?: string;
56
deviceId?: string;
67
}

packages/channel.xiaoai/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
0 Bytes
Binary file not shown.

packages/sbot.commons/src/settings.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,26 @@ export interface ThinkingConfig {
3131
budgetTokens?: number
3232
}
3333

34+
export interface AnthropicConfig {
35+
thinking?: ThinkingConfig
36+
promptCaching?: boolean
37+
}
38+
39+
export interface GeminiConfig {
40+
apiVersion?: string
41+
}
42+
3443
export interface ModelConfig {
3544
name: string
3645
provider: ModelProvider
3746
baseURL: string
3847
apiKey: string
3948
model: string
40-
apiVersion?: string
4149
temperature?: number
4250
maxTokens?: number
4351
contextWindow?: number
44-
thinking?: ThinkingConfig
52+
anthropic?: AnthropicConfig
53+
gemini?: GeminiConfig
4554
}
4655

4756
export enum EmbeddingProvider {

packages/scorpio.ai/src/Model/AnthropicModelService.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import { toChatMessage, toBaseMessages } from "../Saver/messageConverter";
1212
export class AnthropicModelService implements IModelService {
1313
private model?: ChatAnthropic;
1414
private boundModel?: any;
15+
private cacheControl?: { type: "ephemeral" };
1516

16-
constructor(private config: ModelConfig) {}
17+
constructor(private config: ModelConfig) {
18+
if (config.anthropic?.promptCaching) {
19+
this.cacheControl = { type: "ephemeral" };
20+
}
21+
}
1722

1823
get contextWindow(): number | undefined { return this.config.contextWindow; }
1924

@@ -24,7 +29,7 @@ export class AnthropicModelService implements IModelService {
2429
model: this.config.model,
2530
temperature: this.config.temperature,
2631
maxTokens: this.config.maxTokens,
27-
...(this.config.thinking && { thinking: this.config.thinking as any }),
32+
...(this.config.anthropic?.thinking && { thinking: this.config.anthropic.thinking as any }),
2833
});
2934
}
3035

@@ -36,7 +41,10 @@ export class AnthropicModelService implements IModelService {
3641
async invoke(prompt: string | ChatMessage[], options?: { signal?: AbortSignal }): Promise<ChatMessage> {
3742
const m = this.boundModel ?? this.model!;
3843
const input = typeof prompt === 'string' ? prompt : toBaseMessages(prompt);
39-
const result = await m.invoke(input, options?.signal ? { signal: options.signal } : undefined);
44+
const result = await m.invoke(input, {
45+
...(options?.signal && { signal: options.signal }),
46+
...(this.cacheControl && { cache_control: this.cacheControl }),
47+
});
4048
return toChatMessage(result);
4149
}
4250

@@ -52,7 +60,10 @@ export class AnthropicModelService implements IModelService {
5260
async stream(messages: string | ChatMessage[], options?: { signal?: AbortSignal }): Promise<AsyncIterable<ChatMessage>> {
5361
const m = this.boundModel ?? this.model!;
5462
const input = typeof messages === 'string' ? messages : toBaseMessages(messages);
55-
const lcStream = await m.stream(input, options?.signal ? { signal: options.signal } : undefined);
63+
const lcStream = await m.stream(input, {
64+
...(options?.signal && { signal: options.signal }),
65+
...(this.cacheControl && { cache_control: this.cacheControl }),
66+
});
5667
return (async function* () {
5768
let accumulated: AIMessageChunk | undefined;
5869
for await (const chunk of lcStream) {

packages/scorpio.ai/src/Model/GeminiModelService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class GeminiModelService implements IModelService {
2121
apiKey: this.config.apiKey,
2222
baseUrl: this.config.baseURL,
2323
model: this.config.model,
24-
apiVersion: this.config.apiVersion ?? "v1",
24+
apiVersion: this.config.gemini?.apiVersion ?? "v1",
2525
};
2626
if (this.config.temperature != null) opts.temperature = this.config.temperature;
2727
if (this.config.maxTokens != null) opts.maxOutputTokens = this.config.maxTokens;

0 commit comments

Comments
 (0)