Skip to content

Commit 7cd762a

Browse files
Add z.ai chat completions (#2874)
1 parent 1b39bd2 commit 7cd762a

3 files changed

Lines changed: 425 additions & 0 deletions

File tree

src/backend/drivers/ai-chat/ChatCompletionDriver.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { OpenAiResponsesChatProvider } from './providers/openai/OpenAiChatRespon
2020
import { OpenRouterProvider } from './providers/openrouter/OpenRouterProvider.js';
2121
import { TogetherAIProvider } from './providers/together/TogetherAIProvider.js';
2222
import { XAIProvider } from './providers/xai/XAIProvider.js';
23+
import { ZAIProvider } from './providers/zai/ZAIProvider.js';
2324
import type {
2425
IChatCompleteResult,
2526
IChatModel,
@@ -588,6 +589,18 @@ export class ChatCompletionDriver extends PuterDriver {
588589
);
589590
}
590591

592+
const zai = providers['zai'];
593+
const zaiKey = readKey(zai);
594+
if (zaiKey) {
595+
this.#providers['zai'] = new ZAIProvider(
596+
{
597+
apiKey: zaiKey,
598+
apiBaseUrl: zai?.apiBaseUrl as string | undefined,
599+
},
600+
metering,
601+
);
602+
}
603+
591604
const openrouter = providers['openrouter'];
592605
const openrouterKey = readKey(openrouter);
593606
if (openrouterKey) {
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import { OpenAI } from 'openai';
21+
import { ChatCompletionCreateParams } from 'openai/resources/index.js';
22+
import { Context } from '../../../../core/context.js';
23+
import type { MeteringService } from '../../../../services/metering/MeteringService.js';
24+
import type { IChatProvider, ICompleteArguments } from '../../types.js';
25+
import * as OpenAIUtil from '../../utils/OpenAIUtil.js';
26+
import { ZAI_MODELS } from './models.js';
27+
28+
type ZAIConfig = {
29+
apiBaseUrl?: string;
30+
apiKey: string;
31+
};
32+
33+
type ZAICustomParams = {
34+
do_sample?: boolean;
35+
request_id?: string;
36+
response_format?: unknown;
37+
stop?: string[];
38+
thinking?: {
39+
type?: 'enabled' | 'disabled';
40+
clear_thinking?: boolean;
41+
};
42+
tool_stream?: boolean;
43+
user_id?: string;
44+
};
45+
46+
const asRecord = (value: unknown): Record<string, unknown> =>
47+
value && typeof value === 'object' && !Array.isArray(value)
48+
? (value as Record<string, unknown>)
49+
: {};
50+
51+
export class ZAIProvider implements IChatProvider {
52+
#openai: OpenAI;
53+
54+
#meteringService: MeteringService;
55+
56+
#defaultModel = 'glm-5.1';
57+
58+
constructor(config: ZAIConfig, meteringService: MeteringService) {
59+
this.#openai = new OpenAI({
60+
apiKey: config.apiKey,
61+
baseURL: config.apiBaseUrl ?? 'https://api.z.ai/api/paas/v4',
62+
});
63+
this.#meteringService = meteringService;
64+
}
65+
66+
getDefaultModel() {
67+
return this.#defaultModel;
68+
}
69+
70+
models() {
71+
return ZAI_MODELS;
72+
}
73+
74+
list() {
75+
const modelIds: string[] = [];
76+
for (const model of this.models()) {
77+
modelIds.push(model.id);
78+
if (model.aliases) {
79+
modelIds.push(...model.aliases);
80+
}
81+
}
82+
return modelIds;
83+
}
84+
85+
async complete(
86+
params: ICompleteArguments,
87+
): ReturnType<IChatProvider['complete']> {
88+
const {
89+
custom,
90+
max_tokens,
91+
stream,
92+
temperature,
93+
tools,
94+
tool_choice,
95+
top_p,
96+
} = params;
97+
let { messages, model } = params;
98+
const actor = Context.get('actor');
99+
const availableModels = this.models();
100+
const modelUsed =
101+
availableModels.find((m) =>
102+
[m.id, ...(m.aliases || [])].includes(model),
103+
) || availableModels.find((m) => m.id === this.getDefaultModel())!;
104+
105+
messages = await OpenAIUtil.process_input_messages(messages);
106+
messages = messages.map((message) => {
107+
delete message.cache_control;
108+
return message;
109+
});
110+
111+
const customParams = asRecord(custom) as ZAICustomParams;
112+
const userId =
113+
customParams.user_id ??
114+
(actor?.user?.id
115+
? `puter-${actor.user.id}${actor.app?.uid ? `-${actor.app.uid}` : ''}`.slice(
116+
0,
117+
128,
118+
)
119+
: undefined);
120+
121+
const completionParams: ChatCompletionCreateParams = {
122+
messages,
123+
model: modelUsed.id,
124+
...(tools ? { tools } : {}),
125+
...(tool_choice !== undefined ? { tool_choice } : {}),
126+
...(max_tokens !== undefined ? { max_tokens } : {}),
127+
...(temperature !== undefined ? { temperature } : {}),
128+
...(top_p !== undefined ? { top_p } : {}),
129+
...(customParams.do_sample !== undefined
130+
? { do_sample: customParams.do_sample }
131+
: {}),
132+
...(customParams.request_id
133+
? { request_id: customParams.request_id }
134+
: {}),
135+
...(customParams.response_format
136+
? { response_format: customParams.response_format }
137+
: {}),
138+
...(customParams.stop ? { stop: customParams.stop } : {}),
139+
...(customParams.thinking
140+
? { thinking: customParams.thinking }
141+
: {}),
142+
...(customParams.tool_stream !== undefined
143+
? { tool_stream: customParams.tool_stream }
144+
: {}),
145+
...(userId ? { user_id: userId } : {}),
146+
stream: !!stream,
147+
...(stream
148+
? {
149+
stream_options: { include_usage: true },
150+
}
151+
: {}),
152+
} as ChatCompletionCreateParams;
153+
154+
const completion =
155+
await this.#openai.chat.completions.create(completionParams);
156+
157+
const result = await OpenAIUtil.handle_completion_output({
158+
usage_calculator: ({ usage }) => {
159+
const trackedUsage = usage
160+
? OpenAIUtil.extractMeteredUsage(usage)
161+
: {
162+
prompt_tokens: 0,
163+
completion_tokens: 0,
164+
cached_tokens: 0,
165+
};
166+
const costsOverrideFromModel = Object.fromEntries(
167+
Object.entries(trackedUsage).map(([key, value]) => {
168+
return [key, value * Number(modelUsed.costs[key] ?? 0)];
169+
}),
170+
);
171+
this.#meteringService.utilRecordUsageObject(
172+
trackedUsage,
173+
actor,
174+
`zai:${modelUsed.id}`,
175+
costsOverrideFromModel,
176+
);
177+
return trackedUsage;
178+
},
179+
stream,
180+
completion,
181+
});
182+
183+
this.#normalizeReasoningContent(result);
184+
return result;
185+
}
186+
187+
checkModeration(
188+
_text: string,
189+
): ReturnType<IChatProvider['checkModeration']> {
190+
throw new Error('Method not implemented.');
191+
}
192+
193+
#normalizeReasoningContent(
194+
result: Awaited<ReturnType<IChatProvider['complete']>>,
195+
) {
196+
if (!('message' in result) || !result.message) return;
197+
198+
const message = result.message as Record<string, unknown>;
199+
if (
200+
message.reasoning === undefined &&
201+
message.reasoning_content !== undefined
202+
) {
203+
message.reasoning = message.reasoning_content;
204+
}
205+
delete message.reasoning_content;
206+
207+
if (!Array.isArray(message.content)) return;
208+
209+
for (const contentPart of message.content) {
210+
const part = asRecord(contentPart);
211+
if (
212+
part.reasoning === undefined &&
213+
part.reasoning_content !== undefined
214+
) {
215+
part.reasoning = part.reasoning_content;
216+
}
217+
delete part.reasoning_content;
218+
}
219+
}
220+
}

0 commit comments

Comments
 (0)