Files
codebuddy2api1ts/src/direct-upstream.ts

132 lines
4.6 KiB
TypeScript

import { randomBytes, randomUUID } from 'node:crypto';
import { gzipSync } from 'node:zlib';
import { loadApiKey, loadCliUserContextBlocks, loadSystemPrompt } from './direct-config';
import { logDebug, logError, logInfo } from './direct-logger';
import type { DirectUpstreamMessage, DirectUpstreamOptions, UpstreamChunk } from './direct-types';
import { parseUpstreamChunk, type CanonicalEvent } from './direct-canonical';
const endpoint = process.env.CODEBUDDY_DIRECT_ENDPOINT ?? 'https://copilot.tencent.com/v2/chat/completions';
const defaultModel = process.env.CODEBUDDY_MODEL ?? 'minimax-m2.7';
export { loadApiKey, loadCliUserContextBlocks, loadSystemPrompt } from './direct-config';
export function buildDirectHeaders(apiKey = loadApiKey()): Record<string, string> {
const traceId = randomBytes(16).toString('hex');
const spanId = randomBytes(8).toString('hex');
return {
'content-type': 'application/json',
'content-encoding': 'gzip',
accept: 'application/json',
'x-requested-with': 'XMLHttpRequest',
authorization: `Bearer ${apiKey}`,
'x-api-key': apiKey,
'x-conversation-id': randomUUID(),
'x-conversation-request-id': randomBytes(16).toString('hex'),
'x-conversation-message-id': randomBytes(16).toString('hex'),
'x-agent-intent': 'craft',
'x-ide-type': 'CLI',
'x-ide-name': 'CLI',
'x-ide-version': '2.93.3',
'user-agent': process.env.CODEBUDDY_USER_AGENT ?? 'CLI/2.93.3 CodeBuddy/2.93.3 CodeBuddy Agent SDK/0.3.28 (Node.js/25.2.1) CodeBuddy Code/2.93.3',
'x-trace-id': traceId,
'x-request-id': traceId,
b3: `${traceId}-${spanId}-1-`,
'x-b3-traceid': traceId,
'x-b3-parentspanid': '',
'x-b3-spanid': spanId,
'x-b3-sampled': '1',
'x-codebuddy-request': '1',
'x-user-id': `anonymous_${apiKey.slice(-8)}`,
'x-product': 'SaaS',
};
}
export function buildDirectRequestBody(messages: DirectUpstreamMessage[], options: DirectUpstreamOptions = {}) {
return gzipSync(JSON.stringify({
model: options.model ?? defaultModel,
messages,
tools: options.tools,
tool_choice: options.tool_choice,
stream: true,
stream_options: { include_usage: true },
temperature: options.temperature ?? Number(process.env.CODEBUDDY_TEMPERATURE ?? 1),
max_tokens: options.max_tokens ?? Number(process.env.CODEBUDDY_MAX_TOKENS ?? 48000),
reasoning_effort: options.reasoning_effort ?? process.env.CODEBUDDY_REASONING_EFFORT ?? 'medium',
verbosity: options.verbosity ?? process.env.CODEBUDDY_VERBOSITY ?? 'high',
reasoning_summary: options.reasoning_summary ?? process.env.CODEBUDDY_REASONING_SUMMARY ?? 'auto',
}));
}
export async function* streamDirectCanonicalEvents(
messages: DirectUpstreamMessage[],
options: DirectUpstreamOptions = {},
): AsyncGenerator<CanonicalEvent> {
const startedAt = Date.now();
logInfo('upstream request started', {
endpoint,
model: options.model ?? defaultModel,
messages: messages.length,
tools: options.tools?.length ?? 0,
stream: true,
});
const response = await fetch(endpoint, {
method: 'POST',
headers: buildDirectHeaders(),
body: buildDirectRequestBody(messages, options),
});
if (!response.ok || !response.body) {
const responseText = await response.text();
logError('upstream request failed', {
status: response.status,
statusText: response.statusText,
durationMs: Date.now() - startedAt,
bodyPreview: responseText.slice(0, 1000),
});
throw new Error(`HTTP ${response.status}: ${responseText}`);
}
logInfo('upstream stream opened', {
status: response.status,
durationMs: Date.now() - startedAt,
});
const decoder = new TextDecoder();
let buffer = '';
let chunkCount = 0;
let eventCount = 0;
for await (const chunk of response.body) {
chunkCount += 1;
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const data = line.slice(5).trim();
if (!data || data === '[DONE]') continue;
try {
const parsed = JSON.parse(data) as UpstreamChunk;
for (const event of parseUpstreamChunk(parsed)) {
eventCount += 1;
yield event;
}
} catch (error) {
logDebug('ignored malformed upstream stream data', {
error: error instanceof Error ? error.message : String(error),
dataPreview: data.slice(0, 300),
});
continue;
}
}
}
logInfo('upstream stream finished', {
durationMs: Date.now() - startedAt,
chunks: chunkCount,
events: eventCount,
});
}