首次转发成功

This commit is contained in:
2026-04-23 20:50:35 +08:00
parent a1587b8d12
commit 3e6fe3a6a1
20 changed files with 2884 additions and 4 deletions

108
src/direct-adapter-chat.ts Normal file
View File

@@ -0,0 +1,108 @@
import { randomUUID } from 'node:crypto';
import type { CanonicalEvent, CanonicalMetadata, CanonicalState } from './direct-types';
function toChatFinishReason(finishReason: string | null): string | null {
return finishReason === null ? null : finishReason;
}
export function collectChatCompletionStreamFrames(metadata: CanonicalMetadata, events: CanonicalEvent[]): string[] {
return [
...events.flatMap((event) => chatCompletionStreamFramesForEvent(metadata, event)),
'data: [DONE]',
];
}
export function chatCompletionStreamFramesForEvent(metadata: CanonicalMetadata, event: CanonicalEvent): string[] {
if (event.type === 'text_delta') {
return [`data: ${JSON.stringify({
id: metadata.id,
object: 'chat.completion.chunk',
created: metadata.created,
model: metadata.model,
choices: [{ index: 0, delta: { content: event.text }, finish_reason: null }],
})}`];
}
if (event.type === 'reasoning_delta') {
return [`data: ${JSON.stringify({
id: metadata.id,
object: 'chat.completion.chunk',
created: metadata.created,
model: metadata.model,
choices: [{ index: 0, delta: { reasoning_content: event.text }, finish_reason: null }],
})}`];
}
if (event.type === 'tool_call_delta') {
return [`data: ${JSON.stringify({
id: metadata.id,
object: 'chat.completion.chunk',
created: metadata.created,
model: metadata.model,
choices: [{
index: 0,
delta: {
tool_calls: [{
index: event.index,
id: event.id,
type: event.id ? 'function' : undefined,
function: {
name: event.name,
arguments: event.arguments,
},
}],
},
finish_reason: null,
}],
})}`];
}
if (event.type === 'finish') {
return [`data: ${JSON.stringify({
id: metadata.id,
object: 'chat.completion.chunk',
created: metadata.created,
model: metadata.model,
choices: [{ index: 0, delta: {}, finish_reason: toChatFinishReason(event.finishReason) }],
})}`];
}
if (event.type === 'usage') {
return [`data: ${JSON.stringify({
id: metadata.id,
object: 'chat.completion.chunk',
created: metadata.created,
model: metadata.model,
choices: [{ index: 0, delta: {}, finish_reason: null }],
usage: event.usage,
})}`];
}
return [];
}
export function buildChatCompletionResponse(state: CanonicalState) {
const toolCalls = state.toolCalls
.filter((toolCall) => toolCall.function.name || toolCall.function.arguments)
.map((toolCall) => ({
id: toolCall.id ?? `call_${toolCall.index}`,
type: 'function',
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments,
},
}));
return {
id: `chatcmpl-${randomUUID()}`,
object: 'chat.completion',
created: state.created,
model: state.model,
choices: [{
index: 0,
message: {
role: 'assistant',
content: state.text,
reasoning_content: state.reasoning || undefined,
tool_calls: toolCalls.length ? toolCalls : undefined,
},
finish_reason: state.finishReason ?? 'stop',
}],
usage: state.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
};
}

View File

@@ -0,0 +1,208 @@
import { randomUUID } from 'node:crypto';
import type { CanonicalEvent, CanonicalMetadata, CanonicalState, CanonicalToolCall } from './direct-types';
function mapStopReason(finishReason: string | null): string {
if (finishReason === 'length') return 'max_tokens';
if (finishReason === 'tool_calls') return 'tool_use';
return 'end_turn';
}
export function collectAnthropicStreamEvents(metadata: CanonicalMetadata, events: CanonicalEvent[]) {
const encoder = new AnthropicStreamEncoder(metadata);
return [
...encoder.start(),
...events.flatMap((event) => encoder.push(event)),
...encoder.finish(),
];
}
export class AnthropicStreamEncoder {
private readonly messageId: string;
private nextIndex = 0;
private currentBlock: 'thinking' | 'text' | 'tool_use' | null = null;
private currentBlockIndex: number | null = null;
private finalStopReason = 'end_turn';
private finalUsage: Record<string, unknown> = { output_tokens: 0 };
private finished = false;
private toolIndexes = new Map<number, number>();
constructor(private readonly metadata: CanonicalMetadata) {
this.messageId = `msg_${metadata.id.replace(/[^a-zA-Z0-9_]/g, '')}`;
}
start(): Array<{ event: string; data: unknown }> {
return [{
event: 'message_start',
data: {
type: 'message_start',
message: {
id: this.messageId,
type: 'message',
role: 'assistant',
model: this.metadata.model,
content: [],
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 0, output_tokens: 0 },
},
},
}];
}
push(event: CanonicalEvent): Array<{ event: string; data: unknown }> {
const output: Array<{ event: string; data: unknown }> = [];
if (event.type === 'reasoning_delta') {
output.push(...this.openBlock('thinking', { type: 'thinking', thinking: '' }));
output.push({ event: 'content_block_delta', data: { type: 'content_block_delta', index: this.currentBlockIndex, delta: { type: 'thinking_delta', thinking: event.text } } });
return output;
}
if (event.type === 'text_delta') {
output.push(...this.openBlock('text', { type: 'text', text: '' }));
output.push({ event: 'content_block_delta', data: { type: 'content_block_delta', index: this.currentBlockIndex, delta: { type: 'text_delta', text: event.text } } });
return output;
}
if (event.type === 'function_call_name') {
output.push(...this.openToolBlock(0, undefined, event.name));
return output;
}
if (event.type === 'function_call_arguments_delta') {
output.push(...this.openToolBlock(0));
output.push({ event: 'content_block_delta', data: { type: 'content_block_delta', index: this.toolIndexes.get(0), delta: { type: 'input_json_delta', partial_json: event.text } } });
return output;
}
if (event.type === 'tool_call_delta') {
output.push(...this.openToolBlock(event.index, event.id, event.name));
if (event.arguments) {
output.push({ event: 'content_block_delta', data: { type: 'content_block_delta', index: this.toolIndexes.get(event.index), delta: { type: 'input_json_delta', partial_json: event.arguments } } });
}
return output;
}
if (event.type === 'finish') {
this.finalStopReason = mapStopReason(event.finishReason);
output.push(...this.closeCurrentBlock());
return output;
}
if (event.type === 'usage') {
this.finalUsage = {
input_tokens: event.usage.prompt_tokens ?? 0,
output_tokens: event.usage.completion_tokens ?? 0,
cache_read_input_tokens: event.usage.cache_read_input_tokens ?? 0,
};
return output;
}
return output;
}
finish(): Array<{ event: string; data: unknown }> {
if (this.finished) return [];
this.finished = true;
return [
...this.closeCurrentBlock(),
{
event: 'message_delta',
data: {
type: 'message_delta',
delta: { stop_reason: this.finalStopReason, stop_sequence: null },
usage: this.finalUsage,
},
},
{ event: 'message_stop', data: { type: 'message_stop' } },
];
}
private openBlock(type: 'thinking' | 'text', contentBlock: Record<string, unknown>): Array<{ event: string; data: unknown }> {
if (this.currentBlock === type && this.currentBlockIndex !== null) return [];
const output = this.closeCurrentBlock();
const index = this.nextIndex++;
this.currentBlock = type;
this.currentBlockIndex = index;
output.push({ event: 'content_block_start', data: { type: 'content_block_start', index, content_block: contentBlock } });
return output;
}
private openToolBlock(index: number, id?: string, name?: string): Array<{ event: string; data: unknown }> {
const existing = this.toolIndexes.get(index);
if (existing !== undefined) return [];
const output = this.closeCurrentBlock();
const blockIndex = this.nextIndex++;
this.toolIndexes.set(index, blockIndex);
this.currentBlock = 'tool_use';
this.currentBlockIndex = blockIndex;
output.push({
event: 'content_block_start',
data: {
type: 'content_block_start',
index: blockIndex,
content_block: {
type: 'tool_use',
id: id ?? `toolu_${this.metadata.id}_${index}`,
name: name ?? '',
input: {},
},
},
});
return output;
}
private closeCurrentBlock(): Array<{ event: string; data: unknown }> {
if (this.currentBlockIndex === null) return [];
const index = this.currentBlockIndex;
this.currentBlock = null;
this.currentBlockIndex = null;
return [{ event: 'content_block_stop', data: { type: 'content_block_stop', index } }];
}
}
export function buildAnthropicMessageResponse(state: CanonicalState) {
const content: Array<Record<string, unknown>> = [];
if (state.reasoning) {
content.push({ type: 'thinking', thinking: state.reasoning });
}
if (state.text) {
content.push({ type: 'text', text: state.text });
}
for (const toolCall of state.toolCalls) {
content.push(toolCallToAnthropicBlock(toolCall));
}
return {
id: `msg_${randomUUID().replaceAll('-', '')}`,
type: 'message',
role: 'assistant',
model: state.model,
content,
stop_reason: mapStopReason(state.finishReason),
stop_sequence: null,
usage: {
input_tokens: state.usage?.prompt_tokens ?? 0,
output_tokens: state.usage?.completion_tokens ?? 0,
cache_read_input_tokens: state.usage?.cache_read_input_tokens ?? 0,
},
};
}
function toolCallToAnthropicBlock(toolCall: CanonicalToolCall): Record<string, unknown> {
return {
type: 'tool_use',
id: toolCall.id ?? `toolu_${toolCall.index}`,
name: toolCall.function.name,
input: parseToolInput(toolCall.function.arguments),
};
}
function parseToolInput(value: string): unknown {
if (!value) return {};
try {
return JSON.parse(value) as unknown;
} catch {
return {};
}
}

View File

@@ -0,0 +1,271 @@
import { randomUUID } from 'node:crypto';
import type { CanonicalEvent, CanonicalMetadata, CanonicalState } from './direct-types';
function makeMessageItem(itemId: string, text: string) {
return {
type: 'message',
id: itemId,
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text }],
};
}
function makeReasoningItem(itemId: string, text: string) {
return {
type: 'reasoning',
id: itemId,
summary: [{ type: 'summary_text', text }],
};
}
function makeFunctionCallItem(itemId: string, callId: string, name: string, argumentsText: string) {
return {
type: 'function_call',
id: itemId,
call_id: callId,
name,
arguments: argumentsText,
status: 'completed',
};
}
export function collectResponsesStreamEvents(metadata: CanonicalMetadata, events: CanonicalEvent[]) {
const encoder = new ResponsesStreamEncoder(metadata);
return [
...encoder.start(),
...events.flatMap((event) => encoder.push(event)),
...encoder.finish(),
];
}
export class ResponsesStreamEncoder {
private readonly messageItemId: string;
private readonly reasoningItemId: string;
private sequenceNumber = 0;
private nextOutputIndex = 0;
private openedMessage = false;
private openedReasoning = false;
private messageOutputIndex: number | null = null;
private reasoningOutputIndex: number | null = null;
private completed = false;
private toolItems = new Map<number, { itemId: string; callId: string; name: string; opened: boolean; outputIndex: number }>();
constructor(private readonly metadata: CanonicalMetadata) {
this.messageItemId = `item_${metadata.id}_message`;
this.reasoningItemId = `item_${metadata.id}_reasoning`;
}
start(): Array<Record<string, unknown>> {
return [{
type: 'response.created',
sequence_number: this.sequenceNumber++,
response: { id: this.metadata.id, object: 'response', model: this.metadata.model, status: 'in_progress', output: [] },
}];
}
push(event: CanonicalEvent): Array<Record<string, unknown>> {
const output: Array<Record<string, unknown>> = [];
if (event.type === 'reasoning_delta') {
if (!this.openedReasoning) {
this.reasoningOutputIndex = this.allocateOutputIndex();
output.push({
type: 'response.output_item.added',
sequence_number: this.sequenceNumber++,
output_index: this.reasoningOutputIndex,
item: { type: 'reasoning', id: this.reasoningItemId },
});
this.openedReasoning = true;
}
output.push({
type: 'response.reasoning_summary_text.delta',
sequence_number: this.sequenceNumber++,
output_index: this.reasoningOutputIndex,
summary_index: 0,
item_id: this.reasoningItemId,
delta: event.text,
});
return output;
}
if (event.type === 'text_delta') {
if (!this.openedMessage) {
this.messageOutputIndex = this.allocateOutputIndex();
output.push({
type: 'response.output_item.added',
sequence_number: this.sequenceNumber++,
output_index: this.messageOutputIndex,
item: { type: 'message', id: this.messageItemId, role: 'assistant', status: 'in_progress' },
});
this.openedMessage = true;
}
output.push({
type: 'response.output_text.delta',
sequence_number: this.sequenceNumber++,
output_index: this.messageOutputIndex,
content_index: 0,
item_id: this.messageItemId,
delta: event.text,
});
return output;
}
if (event.type === 'function_call_name') {
output.push(...this.ensureToolItem(0, undefined, event.name));
return output;
}
if (event.type === 'function_call_arguments_delta') {
output.push(...this.ensureToolItem(0));
output.push(this.toolArgumentsDelta(0, event.text));
return output;
}
if (event.type === 'tool_call_delta') {
output.push(...this.ensureToolItem(event.index, event.id, event.name));
if (event.arguments) output.push(this.toolArgumentsDelta(event.index, event.arguments));
return output;
}
if (event.type === 'finish') {
output.push(...this.closeOpenItems());
return output;
}
if (event.type === 'usage') {
output.push({
type: 'response.completed',
sequence_number: this.sequenceNumber++,
response: {
id: this.metadata.id,
object: 'response',
model: this.metadata.model,
status: 'completed',
output: [],
usage: event.usage,
},
});
this.completed = true;
}
return output;
}
finish(): Array<Record<string, unknown>> {
if (this.completed) return [];
return [{
type: 'response.completed',
sequence_number: this.sequenceNumber++,
response: {
id: this.metadata.id,
object: 'response',
model: this.metadata.model,
status: 'completed',
output: [],
},
}];
}
private ensureToolItem(index: number, id?: string, name?: string): Array<Record<string, unknown>> {
const item = this.getToolItem(index, id, name);
if (name) item.name = name;
if (id) item.callId = id;
if (item.opened) return [];
item.opened = true;
return [{
type: 'response.output_item.added',
sequence_number: this.sequenceNumber++,
output_index: item.outputIndex,
item: { type: 'function_call', id: item.itemId, call_id: item.callId, name: item.name, status: 'in_progress' },
}];
}
private toolArgumentsDelta(index: number, delta: string): Record<string, unknown> {
const item = this.getToolItem(index);
return {
type: 'response.function_call_arguments.delta',
sequence_number: this.sequenceNumber++,
output_index: item.outputIndex,
item_id: item.itemId,
call_id: item.callId,
name: item.name,
delta,
};
}
private closeOpenItems(): Array<Record<string, unknown>> {
const output: Array<Record<string, unknown>> = [];
if (this.openedReasoning) {
output.push({ type: 'response.reasoning_summary_text.done', sequence_number: this.sequenceNumber++, output_index: this.reasoningOutputIndex, summary_index: 0, item_id: this.reasoningItemId });
output.push({ type: 'response.output_item.done', sequence_number: this.sequenceNumber++, output_index: this.reasoningOutputIndex, item: { type: 'reasoning', id: this.reasoningItemId, status: 'completed' } });
this.openedReasoning = false;
}
if (this.openedMessage) {
output.push({ type: 'response.output_text.done', sequence_number: this.sequenceNumber++, output_index: this.messageOutputIndex, content_index: 0, item_id: this.messageItemId });
output.push({ type: 'response.output_item.done', sequence_number: this.sequenceNumber++, output_index: this.messageOutputIndex, item: { type: 'message', id: this.messageItemId, role: 'assistant', status: 'completed' } });
this.openedMessage = false;
}
for (const item of [...this.toolItems.values()].sort((left, right) => left.outputIndex - right.outputIndex)) {
if (!item.opened) continue;
output.push({ type: 'response.function_call_arguments.done', sequence_number: this.sequenceNumber++, output_index: item.outputIndex, item_id: item.itemId, call_id: item.callId, name: item.name });
output.push({ type: 'response.output_item.done', sequence_number: this.sequenceNumber++, output_index: item.outputIndex, item: { type: 'function_call', id: item.itemId, status: 'completed' } });
item.opened = false;
}
return output;
}
private allocateOutputIndex(): number {
return this.nextOutputIndex++;
}
private getToolItem(index: number, id?: string, name?: string) {
let item = this.toolItems.get(index);
if (!item) {
const itemId = `item_${this.metadata.id}_function_${index}`;
item = {
itemId,
callId: id ?? `call_${itemId}`,
name: name ?? '',
opened: false,
outputIndex: this.allocateOutputIndex(),
};
this.toolItems.set(index, item);
}
return item;
}
}
export function buildResponsesResponse(state: CanonicalState) {
const output: Array<Record<string, unknown>> = [];
if (state.reasoning) {
output.push(makeReasoningItem(`item_${randomUUID().replaceAll('-', '')}`, state.reasoning));
}
if (state.text) {
output.push(makeMessageItem(`item_${randomUUID().replaceAll('-', '')}`, state.text));
}
if (state.functionCallName || state.functionCallArguments) {
const itemId = `item_${randomUUID().replaceAll('-', '')}`;
output.push(makeFunctionCallItem(itemId, `call_${itemId}`, state.functionCallName, state.functionCallArguments));
}
for (const toolCall of state.toolCalls) {
const itemId = `item_${randomUUID().replaceAll('-', '')}`;
output.push(makeFunctionCallItem(
itemId,
toolCall.id ?? `call_${itemId}`,
toolCall.function.name,
toolCall.function.arguments,
));
}
return {
id: state.id,
object: 'response',
model: state.model,
status: state.finishReason === 'length' ? 'incomplete' : 'completed',
output,
usage: state.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
};
}

100
src/direct-canonical.ts Normal file
View File

@@ -0,0 +1,100 @@
import type { CanonicalEvent, CanonicalMetadata, CanonicalState, UpstreamChunk } from './direct-types';
export type { CanonicalEvent } from './direct-types';
export function createCanonicalState(metadata: CanonicalMetadata): CanonicalState {
return {
...metadata,
text: '',
reasoning: '',
functionCallName: '',
functionCallArguments: '',
toolCalls: [],
finishReason: null,
usage: null,
};
}
export function applyCanonicalEvent(state: CanonicalState, event: CanonicalEvent): void {
if (event.type === 'text_delta') {
state.text += event.text;
return;
}
if (event.type === 'reasoning_delta') {
state.reasoning += event.text;
return;
}
if (event.type === 'function_call_name') {
state.functionCallName = event.name;
return;
}
if (event.type === 'function_call_arguments_delta') {
state.functionCallArguments += event.text;
return;
}
if (event.type === 'tool_call_delta') {
const toolCall = getToolCall(state, event.index);
if (event.id) toolCall.id = event.id;
if (event.name) toolCall.function.name = event.name;
if (event.arguments) toolCall.function.arguments += event.arguments;
return;
}
if (event.type === 'finish') {
state.finishReason = event.finishReason;
return;
}
state.usage = event.usage;
}
export function parseUpstreamChunk(chunk: UpstreamChunk): CanonicalEvent[] {
const choice = chunk.choices?.[0];
const delta = choice?.delta;
const events: CanonicalEvent[] = [];
if (delta?.content) {
events.push({ type: 'text_delta', text: delta.content });
}
if (delta?.reasoning_content) {
events.push({ type: 'reasoning_delta', text: delta.reasoning_content });
}
if (delta?.function_call?.name) {
events.push({ type: 'function_call_name', name: delta.function_call.name });
}
if (delta?.function_call?.arguments) {
events.push({ type: 'function_call_arguments_delta', text: delta.function_call.arguments });
}
for (const toolCall of delta?.tool_calls ?? []) {
events.push({
type: 'tool_call_delta',
index: toolCall.index ?? 0,
id: toolCall.id,
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
});
}
if (typeof choice?.finish_reason !== 'undefined' && choice.finish_reason !== '') {
events.push({ type: 'finish', finishReason: choice.finish_reason ?? null });
}
if (chunk.usage) {
events.push({ type: 'usage', usage: chunk.usage });
}
return events;
}
function getToolCall(state: CanonicalState, index: number) {
let toolCall = state.toolCalls.find((item) => item.index === index);
if (!toolCall) {
toolCall = {
index,
type: 'function',
function: {
name: '',
arguments: '',
},
};
state.toolCalls.push(toolCall);
state.toolCalls.sort((left, right) => left.index - right.index);
}
return toolCall;
}

75
src/direct-config.ts Normal file
View File

@@ -0,0 +1,75 @@
import { existsSync, readFileSync } from 'node:fs';
import type { DirectBuilderContext, DirectModelConfig, DirectTextBlock } from './direct-types';
const defaultSystemPromptFile = 'config/system-prompt.txt';
const defaultModelsFile = 'config/models.json';
const defaultCliCaptureFile = 'captures/codebuddy-chat-completion-full.redacted.json';
export function loadApiKey(): string {
const fromEnv = process.env.CODEBUDDY_API_KEY?.trim();
if (fromEnv) return fromEnv;
const file = process.env.CODEBUDDY_APIKEY_FILE ?? 'apikey';
if (!existsSync(file)) {
throw new Error(`Missing API key. Set CODEBUDDY_API_KEY or create ${file}.`);
}
const key = readFileSync(file, 'utf8')
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line && !line.startsWith('#'));
if (!key) throw new Error(`${file} has no usable API key line.`);
return key;
}
export function loadSystemPrompt(): string {
if (process.env.CODEBUDDY_DISABLE_SYSTEM_PROMPT === '1') return '';
const inline = process.env.CODEBUDDY_SYSTEM_PROMPT?.trim();
if (inline) return inline;
const file = process.env.CODEBUDDY_SYSTEM_PROMPT_FILE ?? defaultSystemPromptFile;
if (!existsSync(file)) return '';
return readFileSync(file, 'utf8').trim();
}
export function loadSystemPromptMode(): DirectBuilderContext['systemPromptMode'] {
const mode = process.env.CODEBUDDY_SYSTEM_PROMPT_MODE?.trim().toLowerCase();
if (!mode || mode === 'original') return 'original';
if (mode === 'passthrough') return 'passthrough';
if (mode === 'hybrid') return 'hybrid';
throw new Error('CODEBUDDY_SYSTEM_PROMPT_MODE must be original, passthrough, or hybrid.');
}
export function loadModels(): DirectModelConfig[] {
const file = process.env.CODEBUDDY_MODELS_FILE ?? defaultModelsFile;
if (!existsSync(file)) return [];
const parsed = JSON.parse(readFileSync(file, 'utf8')) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed
.filter((item): item is DirectModelConfig => Boolean(item) && typeof item === 'object'
&& typeof (item as DirectModelConfig).id === 'string'
&& typeof (item as DirectModelConfig).name === 'string'
&& typeof (item as DirectModelConfig).credits_multiplier === 'number');
}
export function loadCliUserContextBlocks(): DirectTextBlock[] {
if (process.env.CODEBUDDY_DISABLE_CLI_USER_CONTEXT === '1') return [];
const file = process.env.CODEBUDDY_CLI_CAPTURE_FILE ?? defaultCliCaptureFile;
if (!existsSync(file)) return [];
try {
const capture = JSON.parse(readFileSync(file, 'utf8')) as {
request_body?: { messages?: Array<{ role?: string; content?: unknown }> };
};
const userMessage = capture.request_body?.messages?.find((message) => message.role === 'user');
if (!Array.isArray(userMessage?.content)) return [];
return userMessage.content
.filter((value): value is DirectTextBlock => Boolean(value) && typeof value === 'object' && (value as DirectTextBlock).type === 'text' && typeof (value as DirectTextBlock).text === 'string')
.filter((block) => !block.text.includes('<user_query>'))
.map((block) => ({ type: 'text', text: block.text }));
} catch {
return [];
}
}

View File

@@ -0,0 +1,742 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildUpstreamMessagesFromAnthropic,
buildUpstreamMessagesFromChat,
buildUpstreamMessagesFromResponses,
buildUpstreamOptionsFromAnthropic,
buildUpstreamOptionsFromChat,
buildUpstreamOptionsFromResponses,
} from './direct-request-builders';
import {
applyCanonicalEvent,
createCanonicalState,
parseUpstreamChunk,
type CanonicalEvent,
} from './direct-canonical';
import {
buildChatCompletionResponse,
collectChatCompletionStreamFrames,
} from './direct-adapter-chat';
import {
buildResponsesResponse,
collectResponsesStreamEvents,
} from './direct-adapter-responses';
import {
buildAnthropicMessageResponse,
collectAnthropicStreamEvents,
} from './direct-adapter-messages';
import { loadModels } from './direct-config';
import type { DirectTextBlock } from './direct-types';
const cliContextBlocks: DirectTextBlock[] = [
{ type: 'text', text: '<system-reminder>ctx</system-reminder>' },
{ type: 'text', text: '<cwd>/workspace</cwd>' },
];
function builderContext(systemPromptMode: 'original' | 'passthrough' | 'hybrid' = 'original') {
return {
systemPrompt: 'captured system prompt',
systemPromptMode,
cliUserContextBlocks: cliContextBlocks,
};
}
function accumulate(events: CanonicalEvent[]) {
const state = createCanonicalState({
id: 'resp_1',
model: 'minimax-m2.7',
created: 123,
});
for (const event of events) {
applyCanonicalEvent(state, event);
}
return state;
}
test('buildUpstreamMessagesFromChat uses original system prompt by default and wraps final user query', () => {
const messages = buildUpstreamMessagesFromChat(
{
messages: [
{ role: 'system', content: 'request system' },
{ role: 'assistant', content: 'older answer' },
{ role: 'user', content: 'hello world' },
],
},
builderContext(),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'assistant');
assert.equal(messages[2]?.role, 'user');
assert.equal(messages[2]?.agent, 'cli');
assert.ok(Array.isArray(messages[2]?.content));
assert.deepEqual(messages[2]?.content, [
...cliContextBlocks,
{ type: 'text', text: '<user_query>hello world</user_query>' },
]);
});
test('buildUpstreamMessagesFromChat can passthrough request system prompt instead of original', () => {
const messages = buildUpstreamMessagesFromChat(
{
messages: [
{ role: 'system', content: 'request system' },
{ role: 'user', content: 'hello world' },
],
},
builderContext('passthrough'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'request system');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'user');
});
test('buildUpstreamMessagesFromChat can send original then passthrough system prompts in hybrid mode', () => {
const messages = buildUpstreamMessagesFromChat(
{
messages: [
{ role: 'system', content: 'request system' },
{ role: 'user', content: 'hello world' },
],
},
builderContext('hybrid'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages[1]?.role, 'system');
assert.equal(messages[1]?.content, 'request system');
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
assert.equal(messages[2]?.role, 'user');
});
test('buildUpstreamMessagesFromChat preserves assistant tool calls and tool results', () => {
const messages = buildUpstreamMessagesFromChat(
{
messages: [
{
role: 'assistant',
content: '',
tool_calls: [{
id: 'call_1',
type: 'function',
function: { name: 'lookup', arguments: '{"q":"x"}' },
}],
},
{ role: 'tool', tool_call_id: 'call_1', content: 'result' },
{ role: 'user', content: 'continue' },
],
},
builderContext(),
);
assert.equal(messages[1]?.role, 'assistant');
assert.deepEqual(messages[1]?.tool_calls, [{
id: 'call_1',
type: 'function',
function: { name: 'lookup', arguments: '{"q":"x"}' },
}]);
assert.equal(messages[2]?.role, 'tool');
assert.equal(messages[2]?.tool_call_id, 'call_1');
assert.equal(messages[2]?.content, 'result');
});
test('buildUpstreamMessagesFromResponses uses original system prompt by default', () => {
const messages = buildUpstreamMessagesFromResponses(
{
instructions: 'be concise',
input: [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'say hi' }],
},
],
},
builderContext(),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'user');
assert.equal(messages[1]?.agent, 'cli');
assert.deepEqual(messages[1]?.content, [
...cliContextBlocks,
{ type: 'text', text: '<user_query>say hi</user_query>' },
]);
});
test('buildUpstreamMessagesFromResponses can passthrough request instructions instead of original', () => {
const messages = buildUpstreamMessagesFromResponses(
{
instructions: 'be concise',
input: 'say hi',
},
builderContext('passthrough'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'be concise');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'user');
});
test('buildUpstreamMessagesFromResponses can send original then instructions in hybrid mode', () => {
const messages = buildUpstreamMessagesFromResponses(
{
instructions: 'be concise',
input: 'say hi',
},
builderContext('hybrid'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages[1]?.role, 'system');
assert.equal(messages[1]?.content, 'be concise');
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
assert.equal(messages[2]?.role, 'user');
});
test('buildUpstreamMessagesFromResponses accepts string message content', () => {
const messages = buildUpstreamMessagesFromResponses(
{
input: [
{
type: 'message',
role: 'user',
content: 'say hi',
},
],
},
builderContext(),
);
assert.equal(messages[1]?.role, 'user');
assert.deepEqual(messages[1]?.content, [
...cliContextBlocks,
{ type: 'text', text: '<user_query>say hi</user_query>' },
]);
});
test('buildUpstreamMessagesFromResponses preserves function call continuation items', () => {
const messages = buildUpstreamMessagesFromResponses(
{
input: [
{ type: 'function_call', call_id: 'call_1', name: 'lookup', arguments: '{"q":"x"}' },
{ type: 'function_call_output', call_id: 'call_1', output: 'result' },
{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'continue' }] },
],
},
builderContext(),
);
assert.equal(messages[1]?.role, 'assistant');
assert.deepEqual(messages[1]?.tool_calls, [{
id: 'call_1',
type: 'function',
function: { name: 'lookup', arguments: '{"q":"x"}' },
}]);
assert.equal(messages[2]?.role, 'tool');
assert.equal(messages[2]?.tool_call_id, 'call_1');
assert.equal(messages[2]?.content, 'result');
});
test('buildUpstreamMessagesFromAnthropic uses original system prompt by default', () => {
const messages = buildUpstreamMessagesFromAnthropic(
{
system: 'anthropic system',
messages: [
{ role: 'assistant', content: [{ type: 'text', text: 'older answer' }] },
{ role: 'user', content: [{ type: 'text', text: 'new question' }] },
],
},
builderContext(),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'assistant');
assert.equal(messages[1]?.content, 'older answer');
assert.equal(messages[2]?.role, 'user');
assert.equal(messages[2]?.agent, 'cli');
});
test('buildUpstreamMessagesFromAnthropic can passthrough request system instead of original', () => {
const messages = buildUpstreamMessagesFromAnthropic(
{
system: 'anthropic system',
messages: [{ role: 'user', content: [{ type: 'text', text: 'new question' }] }],
},
builderContext('passthrough'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'anthropic system');
assert.equal(messages.filter((message) => message.role === 'system').length, 1);
assert.equal(messages[1]?.role, 'user');
});
test('buildUpstreamMessagesFromAnthropic can send original then request system in hybrid mode', () => {
const messages = buildUpstreamMessagesFromAnthropic(
{
system: 'anthropic system',
messages: [{ role: 'user', content: [{ type: 'text', text: 'new question' }] }],
},
builderContext('hybrid'),
);
assert.equal(messages[0]?.role, 'system');
assert.equal(messages[0]?.content, 'captured system prompt');
assert.equal(messages[1]?.role, 'system');
assert.equal(messages[1]?.content, 'anthropic system');
assert.equal(messages.filter((message) => message.role === 'system').length, 2);
assert.equal(messages[2]?.role, 'user');
});
test('buildUpstreamMessagesFromAnthropic preserves tool_use and tool_result blocks', () => {
const messages = buildUpstreamMessagesFromAnthropic(
{
messages: [
{
role: 'assistant',
content: [{ type: 'tool_use', id: 'call_1', name: 'lookup', input: { q: 'x' } }],
},
{
role: 'user',
content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'result' }],
},
],
},
builderContext(),
);
assert.equal(messages[1]?.role, 'assistant');
assert.deepEqual(messages[1]?.tool_calls, [{
id: 'call_1',
type: 'function',
function: { name: 'lookup', arguments: '{"q":"x"}' },
}]);
assert.equal(messages[2]?.role, 'tool');
assert.equal(messages[2]?.tool_call_id, 'call_1');
assert.equal(messages[2]?.content, 'result');
});
test('buildUpstreamOptionsFromChat passes through model tools and reasoning settings', () => {
const options = buildUpstreamOptionsFromChat({
model: 'kimi-k2.6',
tools: [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }],
temperature: 0.3,
max_tokens: 4096,
reasoning_effort: 'high',
verbosity: 'low',
reasoning_summary: 'detailed',
});
assert.equal(options.model, 'kimi-k2.6');
assert.equal(options.temperature, 0.3);
assert.equal(options.max_tokens, 4096);
assert.equal(options.reasoning_effort, 'high');
assert.equal(options.verbosity, 'low');
assert.equal(options.reasoning_summary, 'detailed');
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
});
test('buildUpstreamOptionsFromResponses passes through model tools and reasoning settings', () => {
const options = buildUpstreamOptionsFromResponses({
model: 'glm-5.1',
tools: [{ type: 'function', name: 'lookup', description: 'd', parameters: { type: 'object' } }],
temperature: 0.2,
max_output_tokens: 2048,
reasoning: { effort: 'medium', summary: 'auto' },
text: { verbosity: 'high' },
});
assert.equal(options.model, 'glm-5.1');
assert.equal(options.temperature, 0.2);
assert.equal(options.max_tokens, 2048);
assert.equal(options.reasoning_effort, 'medium');
assert.equal(options.reasoning_summary, 'auto');
assert.equal(options.verbosity, 'high');
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
});
test('buildUpstreamOptionsFromAnthropic passes through model tools and thinking budget', () => {
const options = buildUpstreamOptionsFromAnthropic({
model: 'hunyuan-2.0-thinking',
tools: [{ name: 'lookup', description: 'd', input_schema: { type: 'object' } }],
max_tokens: 1024,
thinking: { type: 'enabled', budget_tokens: 2048 },
});
assert.equal(options.model, 'hunyuan-2.0-thinking');
assert.equal(options.max_tokens, 1024);
assert.equal(options.reasoning_effort, 'high');
assert.deepEqual(options.tools, [{ type: 'function', function: { name: 'lookup', description: 'd', parameters: { type: 'object' } } }]);
});
test('loadModels reads config-backed model registry', () => {
const models = loadModels();
assert.ok(models.some((model) => model.id === 'kimi-k2.6' && model.name === 'Kimi-K2.6' && model.credits_multiplier === 0.59));
assert.ok(models.some((model) => model.id === 'hunyuan-2.0-thinking' && model.credits_multiplier === 0.04));
});
test('parseUpstreamChunk extracts content, reasoning, finish reason, and usage', () => {
const events = parseUpstreamChunk({
id: 'up_1',
model: 'ep-model',
object: 'chat.completion.chunk',
created: 456,
choices: [
{
index: 0,
delta: {
role: 'assistant',
content: 'hello',
reasoning_content: 'thinking',
function_call: { name: 'lookup', arguments: '{"q":"x"}' },
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
},
});
assert.deepEqual(events, [
{ type: 'text_delta', text: 'hello' },
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'function_call_name', name: 'lookup' },
{ type: 'function_call_arguments_delta', text: '{"q":"x"}' },
{ type: 'finish', finishReason: 'stop' },
{
type: 'usage',
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
},
]);
});
test('parseUpstreamChunk extracts tool call deltas', () => {
const events = parseUpstreamChunk({
choices: [
{
delta: {
tool_calls: [{
index: 0,
id: 'call_1',
type: 'function',
function: {
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
}],
},
finish_reason: 'tool_calls',
},
],
});
assert.deepEqual(events, [
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
]);
});
test('parseUpstreamChunk ignores empty upstream finish reason placeholders', () => {
const events = parseUpstreamChunk({
choices: [
{
delta: { reasoning_content: 'thinking' },
finish_reason: '',
},
],
});
assert.deepEqual(events, [{ type: 'reasoning_delta', text: 'thinking' }]);
});
test('buildChatCompletionResponse maps canonical state to non-stream OpenAI response', () => {
const state = accumulate([
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello world' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 2, completion_tokens: 3, total_tokens: 5 } },
]);
const response = buildChatCompletionResponse(state);
assert.equal(response.object, 'chat.completion');
assert.equal(response.choices[0]?.message?.content, 'hello world');
assert.equal(response.choices[0]?.message?.reasoning_content, 'thinking');
assert.equal(response.usage?.total_tokens, 5);
});
test('buildChatCompletionResponse maps tool calls to OpenAI message tool_calls', () => {
const state = accumulate([
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai","unit":"celsius"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
]);
const response = buildChatCompletionResponse(state);
assert.equal(response.choices[0]?.finish_reason, 'tool_calls');
assert.deepEqual(response.choices[0]?.message?.tool_calls, [{
id: 'call_1',
type: 'function',
function: {
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai","unit":"celsius"}',
},
}]);
});
test('collectChatCompletionStreamFrames emits OpenAI SSE chunks and DONE', () => {
const frames = collectChatCompletionStreamFrames(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
],
);
assert.equal(frames.at(-1), 'data: [DONE]');
assert.ok(frames.some((frame) => frame.includes('reasoning_content')));
assert.ok(frames.some((frame) => frame.includes('"content":"hello"')));
});
test('collectChatCompletionStreamFrames emits tool call deltas', () => {
const frames = collectChatCompletionStreamFrames(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
],
);
assert.ok(frames.some((frame) => frame.includes('"tool_calls"')));
assert.ok(frames.some((frame) => frame.includes('"name":"fake_lookup_weather"')));
assert.ok(frames.some((frame) => frame.includes('"finish_reason":"tool_calls"')));
assert.equal(frames.at(-1), 'data: [DONE]');
});
test('buildResponsesResponse maps canonical state to OpenAI responses payload', () => {
const state = accumulate([
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello world' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 4, completion_tokens: 5, total_tokens: 9 } },
]);
const response = buildResponsesResponse(state);
const messageItem = response.output[1] as { type?: string; content?: Array<{ text?: string }> };
assert.equal(response.object, 'response');
assert.equal(response.status, 'completed');
assert.equal(response.output[0]?.type, 'reasoning');
assert.equal(messageItem.type, 'message');
assert.equal(messageItem.content?.[0]?.text, 'hello world');
});
test('buildResponsesResponse maps tool calls to function_call output items', () => {
const state = accumulate([
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
]);
const response = buildResponsesResponse(state);
assert.deepEqual(response.output.at(-1), {
type: 'function_call',
id: (response.output.at(-1) as { id: string }).id,
call_id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
status: 'completed',
});
});
test('collectResponsesStreamEvents emits sub2api-style response events', () => {
const events = collectResponsesStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
],
);
assert.equal(events[0]?.type, 'response.created');
assert.ok(events.some((event) => event.type === 'response.reasoning_summary_text.delta'));
assert.ok(events.some((event) => event.type === 'response.output_text.delta'));
assert.equal(events.at(-1)?.type, 'response.completed');
});
test('collectResponsesStreamEvents starts text-only output at index zero', () => {
const events = collectResponsesStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{ type: 'text_delta', text: 'hello' },
{ type: 'finish', finishReason: 'stop' },
],
);
const added = events.find((event) => event.type === 'response.output_item.added') as { output_index?: number } | undefined;
const delta = events.find((event) => event.type === 'response.output_text.delta') as { output_index?: number } | undefined;
const done = events.find((event) => event.type === 'response.output_text.done') as { output_index?: number } | undefined;
assert.equal(added?.output_index, 0);
assert.equal(delta?.output_index, 0);
assert.equal(done?.output_index, 0);
});
test('collectResponsesStreamEvents emits function call stream events', () => {
const events = collectResponsesStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
],
);
assert.ok(events.some((event) => event.type === 'response.output_item.added' && JSON.stringify(event).includes('fake_lookup_weather')));
assert.ok(events.some((event) => event.type === 'response.function_call_arguments.delta' && JSON.stringify(event).includes('Shanghai')));
assert.ok(events.some((event) => event.type === 'response.output_item.done' && JSON.stringify(event).includes('function_call')));
});
test('collectResponsesStreamEvents starts tool-only output at index zero', () => {
const events = collectResponsesStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
],
);
const added = events.find((event) => event.type === 'response.output_item.added') as { output_index?: number } | undefined;
const delta = events.find((event) => event.type === 'response.function_call_arguments.delta') as { output_index?: number } | undefined;
const done = events.find((event) => event.type === 'response.function_call_arguments.done') as { output_index?: number } | undefined;
assert.equal(added?.output_index, 0);
assert.equal(delta?.output_index, 0);
assert.equal(done?.output_index, 0);
});
test('buildAnthropicMessageResponse includes thinking and text blocks', () => {
const state = accumulate([
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello world' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 7, completion_tokens: 8, total_tokens: 15 } },
]);
const response = buildAnthropicMessageResponse(state);
assert.equal(response.type, 'message');
assert.equal(response.content[0]?.type, 'thinking');
assert.equal(response.content[1]?.type, 'text');
assert.equal(response.content[1]?.text, 'hello world');
assert.equal(response.stop_reason, 'end_turn');
});
test('buildAnthropicMessageResponse maps tool calls to tool_use blocks', () => {
const state = accumulate([
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
]);
const response = buildAnthropicMessageResponse(state);
assert.equal(response.stop_reason, 'tool_use');
assert.deepEqual(response.content.at(-1), {
type: 'tool_use',
id: 'call_1',
name: 'fake_lookup_weather',
input: { city: 'Shanghai' },
});
});
test('collectAnthropicStreamEvents emits thinking and text SSE events', () => {
const events = collectAnthropicStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{ type: 'reasoning_delta', text: 'thinking' },
{ type: 'text_delta', text: 'hello' },
{ type: 'finish', finishReason: 'stop' },
{ type: 'usage', usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 } },
],
);
assert.equal(events[0]?.event, 'message_start');
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('thinking_delta')));
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('text_delta')));
assert.equal(events.at(-1)?.event, 'message_stop');
});
test('collectAnthropicStreamEvents emits tool_use stream events', () => {
const events = collectAnthropicStreamEvents(
{ id: 'resp_1', model: 'minimax-m2.7', created: 123 },
[
{
type: 'tool_call_delta',
index: 0,
id: 'call_1',
name: 'fake_lookup_weather',
arguments: '{"city":"Shanghai"}',
},
{ type: 'finish', finishReason: 'tool_calls' },
],
);
assert.ok(events.some((event) => event.event === 'content_block_start' && JSON.stringify(event.data).includes('tool_use')));
assert.ok(events.some((event) => event.event === 'content_block_delta' && JSON.stringify(event.data).includes('input_json_delta')));
assert.ok(events.some((event) => event.event === 'message_delta' && JSON.stringify(event.data).includes('tool_use')));
});

View File

@@ -0,0 +1,273 @@
import { contentToText } from './prompt';
import type {
AnthropicMessagesRequestLike,
AnthropicContentBlockLike,
DirectBuilderContext,
DirectMessageContent,
DirectTool,
DirectUpstreamMessage,
DirectUpstreamOptions,
OpenAIChatRequestLike,
ResponsesInputMessage,
ResponsesRequestLike,
} from './direct-types';
export function buildUpstreamMessagesFromChat(
request: OpenAIChatRequestLike,
context: DirectBuilderContext,
): DirectUpstreamMessage[] {
const messages = request.messages ?? [];
const normalized = messages.map((message, index) => normalizeConversationMessage(
message.role,
message.content,
index === messages.length - 1,
context,
{
tool_call_id: message.tool_call_id,
tool_calls: message.tool_calls,
},
));
const requestSystemPrompt = contentToText(messages.find((message) => message.role === 'system')?.content);
return prependSystemMessages(normalized, selectSystemPrompts(context, requestSystemPrompt));
}
export function buildUpstreamOptionsFromChat(request: OpenAIChatRequestLike): DirectUpstreamOptions {
return {
model: request.model,
tools: normalizeChatTools(request.tools),
temperature: request.temperature,
max_tokens: request.max_tokens,
reasoning_effort: request.reasoning_effort,
verbosity: request.verbosity,
reasoning_summary: request.reasoning_summary,
};
}
export function buildUpstreamMessagesFromResponses(
request: ResponsesRequestLike,
context: DirectBuilderContext,
): DirectUpstreamMessage[] {
const input = request.input;
const normalizedInputs = typeof input === 'string'
? [buildFinalUserMessage(input, context)]
: (input ?? [])
.flatMap((item, index, list) => normalizeResponsesInputItem(
item,
index === list.length - 1,
context,
));
return prependSystemMessages(normalizedInputs, selectSystemPrompts(context, contentToText(request.instructions)));
}
export function buildUpstreamOptionsFromResponses(request: ResponsesRequestLike): DirectUpstreamOptions {
return {
model: request.model,
tools: normalizeResponsesTools(request.tools),
temperature: request.temperature,
max_tokens: request.max_output_tokens,
reasoning_effort: request.reasoning?.effort,
verbosity: request.text?.verbosity,
reasoning_summary: request.reasoning?.summary,
};
}
export function buildUpstreamMessagesFromAnthropic(
request: AnthropicMessagesRequestLike,
context: DirectBuilderContext,
): DirectUpstreamMessage[] {
const messages = request.messages ?? [];
const normalized = messages.flatMap((message, index) => anthropicContentToUpstream(
message.role,
message.content,
index === messages.length - 1,
context,
));
return prependSystemMessages(normalized, selectSystemPrompts(context, contentToText(request.system)));
}
export function buildUpstreamOptionsFromAnthropic(request: AnthropicMessagesRequestLike): DirectUpstreamOptions {
return {
model: request.model,
tools: normalizeAnthropicTools(request.tools),
max_tokens: request.max_tokens,
reasoning_effort: request.thinking?.type === 'enabled' ? 'high' : undefined,
};
}
function selectSystemPrompts(context: DirectBuilderContext, requestSystemPrompt: string): string[] {
const original = context.systemPrompt.trim();
const passthrough = requestSystemPrompt.trim();
if (context.systemPromptMode === 'passthrough') return passthrough ? [passthrough] : [];
if (context.systemPromptMode === 'hybrid') return [original, passthrough].filter(Boolean);
return original ? [original] : [];
}
function prependSystemMessages(messages: DirectUpstreamMessage[], systemPrompts: string[]): DirectUpstreamMessage[] {
const nonSystemMessages = messages.filter((message) => message.role !== 'system');
return [
...systemPrompts.map((content) => ({ role: 'system', content }) as DirectUpstreamMessage),
...nonSystemMessages,
];
}
function normalizeConversationMessage(
role: string | undefined,
content: unknown,
isFinalMessage: boolean,
context: DirectBuilderContext,
extra: Pick<DirectUpstreamMessage, 'tool_call_id' | 'tool_calls'> = {},
): DirectUpstreamMessage {
const normalizedRole = role === 'assistant' ? 'assistant' : role === 'system' ? 'system' : role === 'tool' ? 'tool' : 'user';
const text = contentToText(content).trim();
if (normalizedRole === 'user' && isFinalMessage) {
return buildFinalUserMessage(text, context);
}
return {
role: normalizedRole,
content: text,
...extra,
};
}
function normalizeResponsesInputItem(
item: ResponsesInputMessage,
isFinalMessage: boolean,
context: DirectBuilderContext,
): DirectUpstreamMessage[] {
if (item.type === 'function_call') {
return [{
role: 'assistant',
content: '',
tool_calls: [{
id: item.call_id,
type: 'function',
function: {
name: item.name ?? '',
arguments: item.arguments ?? '{}',
},
}],
}];
}
if (item.type === 'function_call_output') {
return [{
role: 'tool',
content: item.output ?? '',
tool_call_id: item.call_id,
}];
}
if (item.type && item.type !== 'message') return [];
return [normalizeConversationMessage(
item.role,
item.content,
isFinalMessage,
context,
)];
}
function buildFinalUserMessage(text: string, context: DirectBuilderContext): DirectUpstreamMessage {
const content: DirectMessageContent = [
...context.cliUserContextBlocks,
{ type: 'text', text: `<user_query>${text}</user_query>` },
];
return {
role: 'user',
content,
agent: 'cli',
};
}
function normalizeChatTools(tools: OpenAIChatRequestLike['tools']): DirectTool[] | undefined {
const normalized = (tools ?? [])
.filter((tool): tool is NonNullable<OpenAIChatRequestLike['tools']>[number] => tool?.type === 'function' && Boolean(tool.function?.name))
.map((tool) => ({
type: 'function' as const,
function: {
name: tool.function!.name!,
description: tool.function?.description,
parameters: tool.function?.parameters,
},
}));
return normalized.length ? normalized : undefined;
}
function normalizeResponsesTools(tools: ResponsesRequestLike['tools']): DirectTool[] | undefined {
const normalized = (tools ?? [])
.filter((tool): tool is NonNullable<ResponsesRequestLike['tools']>[number] => (tool.type === 'function' || !tool.type) && typeof tool.name === 'string' && tool.name.length > 0)
.map((tool) => ({
type: 'function' as const,
function: {
name: tool.name!,
description: tool.description,
parameters: tool.parameters,
},
}));
return normalized.length ? normalized : undefined;
}
function normalizeAnthropicTools(tools: AnthropicMessagesRequestLike['tools']): DirectTool[] | undefined {
const normalized = (tools ?? [])
.filter((tool): tool is NonNullable<AnthropicMessagesRequestLike['tools']>[number] => typeof tool.name === 'string' && tool.name.length > 0)
.map((tool) => ({
type: 'function' as const,
function: {
name: tool.name!,
description: tool.description,
parameters: tool.input_schema,
},
}));
return normalized.length ? normalized : undefined;
}
function anthropicContentToUpstream(
role: string | undefined,
content: unknown,
isFinalMessage: boolean,
context: DirectBuilderContext,
): DirectUpstreamMessage[] {
if (!Array.isArray(content)) {
return [normalizeConversationMessage(role, content, isFinalMessage, context)];
}
const text = contentToText(content).trim();
const messages: DirectUpstreamMessage[] = [];
const toolCalls = content
.filter((block): block is AnthropicContentBlockLike => Boolean(block) && typeof block === 'object' && (block as AnthropicContentBlockLike).type === 'tool_use')
.map((block) => ({
id: block.id,
type: 'function' as const,
function: {
name: block.name ?? '',
arguments: JSON.stringify(block.input ?? {}),
},
}));
if (role === 'assistant') {
messages.push({
role: 'assistant',
content: text,
tool_calls: toolCalls.length ? toolCalls : undefined,
});
return messages;
}
for (const block of content) {
if (!block || typeof block !== 'object' || (block as AnthropicContentBlockLike).type !== 'tool_result') continue;
const result = block as AnthropicContentBlockLike;
messages.push({
role: 'tool',
content: contentToText(result.content),
tool_call_id: result.tool_use_id,
});
}
if (text || !messages.length) {
messages.push(normalizeConversationMessage(role, text, isFinalMessage, context));
}
return messages;
}

259
src/direct-server.ts Normal file
View File

@@ -0,0 +1,259 @@
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import {
AnthropicStreamEncoder,
buildAnthropicMessageResponse,
} from './direct-adapter-messages';
import {
buildChatCompletionResponse,
chatCompletionStreamFramesForEvent,
} from './direct-adapter-chat';
import {
buildResponsesResponse,
ResponsesStreamEncoder,
} from './direct-adapter-responses';
import {
applyCanonicalEvent,
createCanonicalState,
type CanonicalEvent,
} from './direct-canonical';
import {
buildUpstreamMessagesFromAnthropic,
buildUpstreamMessagesFromChat,
buildUpstreamMessagesFromResponses,
buildUpstreamOptionsFromAnthropic,
buildUpstreamOptionsFromChat,
buildUpstreamOptionsFromResponses,
} from './direct-request-builders';
import { loadCliUserContextBlocks, loadModels, loadSystemPrompt, loadSystemPromptMode } from './direct-config';
import { streamDirectCanonicalEvents } from './direct-upstream';
import type {
AnthropicMessagesRequestLike,
DirectUpstreamOptions,
OpenAIChatRequestLike,
ResponsesRequestLike,
} from './direct-types';
const port = Number(process.env.DIRECT_PORT ?? 3101);
const proxyApiKey = process.env.PROXY_API_KEY?.trim();
const systemPrompt = loadSystemPrompt();
const systemPromptMode = loadSystemPromptMode();
const cliUserContextBlocks = loadCliUserContextBlocks();
const models = loadModels();
const maxRequestBytes = Number(process.env.DIRECT_MAX_REQUEST_BYTES ?? 1024 * 1024);
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
try {
if (req.method === 'GET' && url.pathname === '/health') {
return writeJson(res, 200, { ok: true, mode: 'direct' });
}
if (!authorize(req)) {
writeEndpointError(res, url.pathname, 401, 'Unauthorized');
return;
}
if (req.method === 'GET' && url.pathname === '/v1/models') {
return writeJson(res, 200, {
object: 'list',
data: models.map((model) => ({
id: model.id,
object: 'model',
owned_by: 'codebuddy-direct',
name: model.name,
credits_multiplier: model.credits_multiplier,
})),
});
}
if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
await handleChat(req, res);
return;
}
if (req.method === 'POST' && url.pathname === '/v1/responses') {
await handleResponses(req, res);
return;
}
if (req.method === 'POST' && url.pathname === '/v1/messages') {
await handleMessages(req, res);
return;
}
writeJson(res, 404, { error: { message: 'Not found' } });
} catch (error) {
if (!res.headersSent) {
if (error instanceof RequestBodyError) {
writeEndpointError(res, url.pathname, error.statusCode, error.message);
return;
}
writeJson(res, 502, { error: { message: 'Upstream request failed' } });
} else {
res.end();
}
}
});
server.listen(port, '127.0.0.1', () => {
console.log(`codebuddy direct service listening on http://127.0.0.1:${port}`);
});
async function handleChat(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = await readJson<OpenAIChatRequestLike>(req);
const metadata = { id: `chatcmpl_${Date.now()}`, model: body.model ?? (process.env.CODEBUDDY_MODEL ?? 'minimax-m2.7'), created: Math.floor(Date.now() / 1000) };
const upstreamMessages = buildUpstreamMessagesFromChat(body, { systemPrompt, systemPromptMode, cliUserContextBlocks });
const upstreamOptions = buildUpstreamOptionsFromChat(body);
if (body.stream) {
writeSseHeaders(res);
for await (const event of streamDirectCanonicalEvents(upstreamMessages, upstreamOptions)) {
for (const frame of chatCompletionStreamFramesForEvent(metadata, event)) {
res.write(`${frame}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
return;
}
const events = await collectEvents(upstreamMessages, upstreamOptions);
const state = accumulate(metadata, events);
writeJson(res, 200, buildChatCompletionResponse(state));
}
async function handleResponses(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = await readJson<ResponsesRequestLike>(req);
const metadata = { id: `resp_${Date.now()}`, model: body.model ?? (process.env.CODEBUDDY_MODEL ?? 'minimax-m2.7'), created: Math.floor(Date.now() / 1000) };
const upstreamMessages = buildUpstreamMessagesFromResponses(body, { systemPrompt, systemPromptMode, cliUserContextBlocks });
const upstreamOptions = buildUpstreamOptionsFromResponses(body);
if (body.stream) {
writeSseHeaders(res);
const encoder = new ResponsesStreamEncoder(metadata);
for (const event of encoder.start()) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
}
for await (const upstreamEvent of streamDirectCanonicalEvents(upstreamMessages, upstreamOptions)) {
for (const event of encoder.push(upstreamEvent)) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
}
}
for (const event of encoder.finish()) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
}
res.end();
return;
}
const events = await collectEvents(upstreamMessages, upstreamOptions);
const state = accumulate(metadata, events);
writeJson(res, 200, buildResponsesResponse(state));
}
async function handleMessages(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = await readJson<AnthropicMessagesRequestLike>(req);
const metadata = { id: `msg_${Date.now()}`, model: body.model ?? (process.env.CODEBUDDY_MODEL ?? 'minimax-m2.7'), created: Math.floor(Date.now() / 1000) };
const upstreamMessages = buildUpstreamMessagesFromAnthropic(body, { systemPrompt, systemPromptMode, cliUserContextBlocks });
const upstreamOptions = buildUpstreamOptionsFromAnthropic(body);
if (body.stream) {
writeSseHeaders(res);
const encoder = new AnthropicStreamEncoder(metadata);
for (const event of encoder.start()) {
res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
for await (const upstreamEvent of streamDirectCanonicalEvents(upstreamMessages, upstreamOptions)) {
for (const event of encoder.push(upstreamEvent)) {
res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
}
for (const event of encoder.finish()) {
res.write(`event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
res.end();
return;
}
const events = await collectEvents(upstreamMessages, upstreamOptions);
const state = accumulate(metadata, events);
writeJson(res, 200, buildAnthropicMessageResponse(state));
}
async function collectEvents(
upstreamMessages: ReturnType<typeof buildUpstreamMessagesFromChat>,
upstreamOptions: DirectUpstreamOptions,
): Promise<CanonicalEvent[]> {
const events: CanonicalEvent[] = [];
for await (const event of streamDirectCanonicalEvents(upstreamMessages, upstreamOptions)) {
events.push(event);
}
return events;
}
function accumulate(metadata: { id: string; model: string; created: number }, events: CanonicalEvent[]) {
const state = createCanonicalState(metadata);
for (const event of events) {
applyCanonicalEvent(state, event);
}
return state;
}
function authorize(req: IncomingMessage): boolean {
if (!proxyApiKey) return true;
return req.headers.authorization === `Bearer ${proxyApiKey}`;
}
async function readJson<T>(req: IncomingMessage): Promise<T> {
const contentLengthHeader = req.headers['content-length'];
const contentLength = typeof contentLengthHeader === 'string' ? Number(contentLengthHeader) : NaN;
if (Number.isFinite(contentLength) && contentLength > maxRequestBytes) {
throw new RequestBodyError(413, 'Request body too large');
}
const chunks: Buffer[] = [];
let totalBytes = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
totalBytes += buffer.length;
if (totalBytes > maxRequestBytes) {
throw new RequestBodyError(413, 'Request body too large');
}
chunks.push(buffer);
}
if (!chunks.length) return {} as T;
try {
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T;
} catch {
throw new RequestBodyError(400, 'Invalid JSON body');
}
}
function writeJson(res: ServerResponse, statusCode: number, value: unknown): void {
res.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(value));
}
function writeSseHeaders(res: ServerResponse): void {
res.writeHead(200, {
'content-type': 'text/event-stream; charset=utf-8',
'cache-control': 'no-cache, no-transform',
connection: 'keep-alive',
});
}
function writeEndpointError(res: ServerResponse, endpoint: string, statusCode: number, message: string): void {
if (endpoint === '/v1/messages') {
writeJson(res, statusCode, { type: 'error', error: { type: 'invalid_request_error', message } });
return;
}
writeJson(res, statusCode, { error: { message } });
}
class RequestBodyError extends Error {
constructor(
readonly statusCode: number,
message: string,
) {
super(message);
}
}

229
src/direct-types.ts Normal file
View File

@@ -0,0 +1,229 @@
export type DirectTextBlock = {
type: 'text';
text: string;
};
export type DirectMessageContent = string | DirectTextBlock[];
export type DirectUpstreamToolCall = {
id?: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
export type DirectUpstreamMessage = {
role: 'system' | 'user' | 'assistant' | 'tool';
content: DirectMessageContent;
agent?: 'cli';
tool_call_id?: string;
tool_calls?: DirectUpstreamToolCall[];
};
export type DirectTool = {
type: 'function';
function: {
name: string;
description?: string;
parameters?: unknown;
};
};
export type DirectUsage = {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
cache_read_input_tokens?: number;
cache_creation_input_tokens?: number;
};
export type CanonicalToolCall = {
index: number;
id?: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
export type CanonicalEvent =
| { type: 'text_delta'; text: string }
| { type: 'reasoning_delta'; text: string }
| { type: 'function_call_name'; name: string }
| { type: 'function_call_arguments_delta'; text: string }
| { type: 'tool_call_delta'; index: number; id?: string; name?: string; arguments?: string }
| { type: 'finish'; finishReason: string | null }
| { type: 'usage'; usage: DirectUsage };
export type CanonicalState = {
id: string;
model: string;
created: number;
text: string;
reasoning: string;
functionCallName: string;
functionCallArguments: string;
toolCalls: CanonicalToolCall[];
finishReason: string | null;
usage: DirectUsage | null;
};
export type CanonicalMetadata = Pick<CanonicalState, 'id' | 'model' | 'created'>;
export type DirectBuilderContext = {
systemPrompt: string;
systemPromptMode: 'original' | 'passthrough' | 'hybrid';
cliUserContextBlocks: DirectTextBlock[];
};
export type DirectUpstreamOptions = {
model?: string;
tools?: DirectTool[];
temperature?: number;
max_tokens?: number;
reasoning_effort?: string;
verbosity?: string;
reasoning_summary?: string;
};
export type OpenAIChatInputMessage = {
role?: string;
content?: unknown;
tool_call_id?: string;
tool_calls?: DirectUpstreamToolCall[];
};
export type OpenAIChatToolLike = {
type?: string;
function?: {
name?: string;
description?: string;
parameters?: unknown;
};
};
export type OpenAIChatRequestLike = {
model?: string;
messages?: OpenAIChatInputMessage[];
tools?: OpenAIChatToolLike[];
temperature?: number;
max_tokens?: number;
reasoning_effort?: string;
verbosity?: string;
reasoning_summary?: string;
stream?: boolean;
};
export type ResponsesInputTextPart = {
type?: string;
text?: string;
};
export type ResponsesInputMessage = {
type?: string;
role?: string;
content?: string | ResponsesInputTextPart[];
call_id?: string;
name?: string;
arguments?: string;
output?: string;
};
export type ResponsesToolLike = {
type?: string;
name?: string;
description?: string;
parameters?: unknown;
};
export type ResponsesRequestLike = {
model?: string;
instructions?: unknown;
input?: string | ResponsesInputMessage[];
tools?: ResponsesToolLike[];
temperature?: number;
max_output_tokens?: number;
reasoning?: {
effort?: string;
summary?: string;
};
text?: {
verbosity?: string;
};
stream?: boolean;
};
export type AnthropicContentBlockLike = {
type?: string;
text?: string;
id?: string;
name?: string;
input?: unknown;
tool_use_id?: string;
content?: unknown;
};
export type AnthropicMessageLike = {
role?: string;
content?: string | AnthropicContentBlockLike[];
};
export type AnthropicToolLike = {
name?: string;
description?: string;
input_schema?: unknown;
};
export type AnthropicMessagesRequestLike = {
model?: string;
system?: unknown;
messages?: AnthropicMessageLike[];
tools?: AnthropicToolLike[];
max_tokens?: number;
thinking?: {
type?: string;
budget_tokens?: number;
};
stream?: boolean;
};
export type DirectModelConfig = {
id: string;
name: string;
credits_multiplier: number;
};
export type UpstreamChunkChoice = {
index?: number;
delta?: {
role?: string;
content?: string;
reasoning_content?: string;
function_call?: {
name?: string;
arguments?: string;
} | null;
tool_calls?: Array<{
index?: number;
id?: string;
type?: string;
function?: {
name?: string;
arguments?: string;
};
}>;
};
finish_reason?: string | null;
};
export type UpstreamChunk = {
id?: string;
model?: string;
object?: string;
created?: number;
choices?: UpstreamChunkChoice[];
usage?: DirectUsage | null;
};

94
src/direct-upstream.ts Normal file
View File

@@ -0,0 +1,94 @@
import { randomBytes, randomUUID } from 'node:crypto';
import { gzipSync } from 'node:zlib';
import { loadApiKey, loadCliUserContextBlocks, loadSystemPrompt } from './direct-config';
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,
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 response = await fetch(endpoint, {
method: 'POST',
headers: buildDirectHeaders(),
body: buildDirectRequestBody(messages, options),
});
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const decoder = new TextDecoder();
let buffer = '';
for await (const chunk of response.body) {
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)) {
yield event;
}
} catch {
continue;
}
}
}
}